● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0

Multi-protocol network egress assessment and DLP verification framework featuring automated L4 sweeps, corporate proxy traversal, and covert protocol smuggling across 20+ channels.

$ git clone https://shehzade.io/repos/contraband.git Copy
Source ZIP (1.93 MB)
Code 174 Commits 1
contraband / pkg / client / exfil / web / http3.go
← Back to directory
236 lines · 6.7 KB
Raw
1package web
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "crypto/tls"
8 "encoding/hex"
9 "encoding/json"
10 "fmt"
11 "io"
12 "net/http"
13 "time"
14
15 "github.com/quic-go/quic-go/http3"
16 "github.com/shehzade/contraband/pkg/client/evasion"
17 "github.com/shehzade/contraband/pkg/client/exfil"
18 "github.com/shehzade/contraband/pkg/common"
19)
20
21type HTTP3Exfiltrator struct{}
22
23func NewHTTP3Exfiltrator() *HTTP3Exfiltrator {
24 return &HTTP3Exfiltrator{}
25}
26
27func init() {
28 exfil.RegisterExfiltrator(common.ProtoHTTP3, NewHTTP3Exfiltrator())
29 exfil.RegisterExfiltrator("http3", NewHTTP3Exfiltrator())
30}
31
32func (e *HTTP3Exfiltrator) Protocol() string {
33 return common.ProtoHTTP3
34}
35
36func (e *HTTP3Exfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
37 if err := opts.Validate(); err != nil {
38 return nil, err
39 }
40
41 start := time.Now().UTC()
42 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
43 if err != nil {
44 return nil, err
45 }
46 result.Timestamps.StartTCP = start
47 result.TLSProfile = opts.TLSFingerprint
48 result.PayloadType = opts.PayloadType
49 result.CarrierType = opts.CarrierType
50
51 if opts.TotalBytes <= 0 {
52 if br, ok := reader.(*bytes.Reader); ok {
53 opts.TotalBytes = int64(br.Len())
54 } else if bb, ok := reader.(*bytes.Buffer); ok {
55 opts.TotalBytes = int64(bb.Len())
56 }
57 }
58
59 if opts.ExpectedHash == "" {
60 if br, ok := reader.(*bytes.Reader); ok {
61 cur, _ := br.Seek(0, io.SeekCurrent)
62 h := sha256.New()
63 _, _ = io.Copy(h, br)
64 opts.ExpectedHash = hex.EncodeToString(h.Sum(nil))
65 _, _ = br.Seek(cur, io.SeekStart)
66 }
67 }
68 result.PayloadHash = opts.ExpectedHash
69
70 if opts.SessionToken.IsZero() {
71 tok, tokErr := common.NewSessionToken()
72 if tokErr != nil {
73 return nil, tokErr
74 }
75 opts.SessionToken = tok
76 }
77
78 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
79 if err != nil {
80 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
81 }
82 defer chunker.Close()
83
84 totalChunks := int(chunker.TotalChunks())
85 result.TargetVolume = opts.TotalBytes
86
87 tr := &http3.Transport{
88 TLSClientConfig: &tls.Config{
89 InsecureSkipVerify: opts.InsecureTLS,
90 ServerName: opts.TargetHost,
91 },
92 }
93 defer tr.Close()
94
95 client := &http.Client{Transport: tr, Timeout: opts.Timeout}
96 targetURL := fmt.Sprintf("https://%s:%d/upload", opts.TargetHost, opts.TargetPort)
97
98 result.Timestamps.ConnectedTCP = time.Now().UTC()
99 result.Timestamps.HandshakeTLS = time.Now().UTC()
100 result.Timestamps.FirstByteSent = time.Now().UTC()
101
102 var lastRespBody []byte
103 var headEvidence []byte
104
105 for {
106 chunkFrame, seq, err := chunker.Next()
107 if err == io.EOF {
108 break
109 }
110 if err != nil {
111 result.Verdict = common.ClassifyError(err)
112 result.ErrorMessage = err.Error()
113 result.Timestamps.TeardownDone = time.Now().UTC()
114 e.calculateDurations(&result)
115 return &result, err
116 }
117
118 if len(headEvidence) < 768 && len(chunkFrame) > common.ChunkHeaderSize {
119 p := chunkFrame[common.ChunkHeaderSize:]
120 needed := 768 - len(headEvidence)
121 if needed > len(p) {
122 needed = len(p)
123 }
124 headEvidence = append(headEvidence, p[:needed]...)
125 }
126
127 if opts.Shaper != nil {
128 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
129 result.Verdict = common.ClassifyError(err)
130 result.ErrorMessage = err.Error()
131 result.Timestamps.TeardownDone = time.Now().UTC()
132 e.calculateDurations(&result)
133 return &result, err
134 }
135 }
136
137 if opts.Cadence != nil {
138 delay := opts.Cadence.NextDelay()
139 if delay > 0 {
140 select {
141 case <-time.After(delay):
142 case <-ctx.Done():
143 result.Verdict = common.ClassifyError(ctx.Err())
144 result.ErrorMessage = ctx.Err().Error()
145 result.Timestamps.TeardownDone = time.Now().UTC()
146 e.calculateDurations(&result)
147 return &result, ctx.Err()
148 }
149 }
150 }
151
152 req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(chunkFrame))
153 if err != nil {
154 result.Verdict = common.ClassifyError(err)
155 result.ErrorMessage = err.Error()
156 result.Timestamps.TeardownDone = time.Now().UTC()
157 e.calculateDurations(&result)
158 return &result, err
159 }
160 req.Header.Set("Content-Type", "application/octet-stream")
161 req.Header.Set(common.HeaderSessionID, opts.SessionToken.String())
162 req.Header.Set(common.HeaderSequence, fmt.Sprintf("%d", seq))
163 req.Header.Set(common.HeaderTotalChunks, fmt.Sprintf("%d", totalChunks))
164 req.Header.Set(common.HeaderPayloadLength, fmt.Sprintf("%d", len(chunkFrame)-common.ChunkHeaderSize))
165 req.Header.Set(common.HeaderPayloadHash, opts.ExpectedHash)
166
167 for k, v := range opts.Headers {
168 req.Header.Set(k, v)
169 }
170
171 resp, err := client.Do(req)
172 if err != nil {
173 result.Verdict = common.ClassifyError(err)
174 result.ErrorMessage = err.Error()
175 result.Timestamps.TeardownDone = time.Now().UTC()
176 e.calculateDurations(&result)
177 return &result, err
178 }
179
180 respBody, _ := io.ReadAll(resp.Body)
181 _ = resp.Body.Close()
182 lastRespBody = respBody
183 result.BytesSent += int64(len(chunkFrame))
184 result.BytesRecv += int64(len(respBody))
185
186 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
187 result.Timestamps.TeardownDone = time.Now().UTC()
188 e.calculateDurations(&result)
189
190 var ack common.VerificationACK
191 if jsonErr := json.Unmarshal(respBody, &ack); jsonErr == nil && ack.Verdict != "" {
192 result.Verdict = ack.Verdict
193 result.ErrorMessage = ack.ErrorMessage
194 } else {
195 result.Verdict = common.VerdictBlockedL7
196 result.ErrorMessage = fmt.Sprintf("server returned status %d: %s", resp.StatusCode, string(respBody))
197 }
198 return &result, fmt.Errorf("%s", result.ErrorMessage)
199 }
200 }
201
202 result.Timestamps.AckReceived = time.Now().UTC()
203 result.Timestamps.TeardownDone = time.Now().UTC()
204
205 // Parse and strictly verify cryptographic verification ACK
206 var ack common.VerificationACK
207 if err := json.Unmarshal(lastRespBody, &ack); err != nil {
208 result.Verdict = common.VerdictBlockedL7
209 result.ErrorMessage = fmt.Sprintf("failed to parse verification ACK: %v", err)
210 e.calculateDurations(&result)
211 return &result, fmt.Errorf("invalid verification ACK: %w", err)
212 }
213
214 verdict, verifyErr := ack.Verify(opts.ExpectedHash, totalChunks)
215 result.Verdict = verdict
216 if verifyErr != nil {
217 result.ErrorMessage = verifyErr.Error()
218 e.calculateDurations(&result)
219 return &result, verifyErr
220 }
221
222 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
223 e.calculateDurations(&result)
224 return &result, nil
225}
226
227func (e *HTTP3Exfiltrator) calculateDurations(r *common.ProbeResult) {
228 t := &r.Timestamps
229 if t.TeardownDone.IsZero() {
230 t.TeardownDone = time.Now().UTC()
231 }
232 t.ComputeDurations()
233 if t.TransferDuration > 0 {
234 r.ThroughputBps = float64(r.BytesSent*8) / (float64(t.TransferDuration) / float64(time.Second)) / 1000.0
235 }
236}