● 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 / http1.go
← Back to directory
333 lines · 9.5 KB
Raw
1package web
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "io"
11 "mime/multipart"
12 "net"
13 "net/http"
14 "time"
15
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 HTTP1Exfiltrator struct{}
22
23func NewHTTP1Exfiltrator() *HTTP1Exfiltrator {
24 return &HTTP1Exfiltrator{}
25}
26
27func init() {
28 exfil.RegisterExfiltrator(common.ProtoHTTP1, NewHTTP1Exfiltrator())
29 exfil.RegisterExfiltrator("http1", NewHTTP1Exfiltrator())
30}
31
32func (e *HTTP1Exfiltrator) Protocol() string {
33 return common.ProtoHTTP1
34}
35
36func (e *HTTP1Exfiltrator) 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 if opts.TotalBytes <= 0 {
51 if br, ok := reader.(*bytes.Reader); ok {
52 opts.TotalBytes = int64(br.Len())
53 } else if bb, ok := reader.(*bytes.Buffer); ok {
54 opts.TotalBytes = int64(bb.Len())
55 }
56 }
57
58 if opts.ExpectedHash == "" {
59 if br, ok := reader.(*bytes.Reader); ok {
60 cur, _ := br.Seek(0, io.SeekCurrent)
61 h := sha256.New()
62 _, _ = io.Copy(h, br)
63 opts.ExpectedHash = hex.EncodeToString(h.Sum(nil))
64 _, _ = br.Seek(cur, io.SeekStart)
65 }
66 }
67 result.PayloadHash = opts.ExpectedHash
68
69 if opts.SessionToken.IsZero() {
70 tok, tokErr := common.NewSessionToken()
71 if tokErr != nil {
72 return nil, tokErr
73 }
74 opts.SessionToken = tok
75 }
76
77 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
78 if err != nil {
79 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
80 }
81 defer chunker.Close()
82
83 totalChunks := int(chunker.TotalChunks())
84 result.TargetVolume = opts.TotalBytes
85
86 isTLS := opts.UseTLS || (opts.TargetPort != 80 && opts.InsecureTLS) || opts.TargetPort == 443 || opts.TargetPort == 8443 || opts.TargetPort == 9443 || opts.TargetPort == 10443
87 scheme := "http"
88 if isTLS {
89 scheme = "https"
90 }
91 targetURL := fmt.Sprintf("%s://%s:%d/upload", scheme, opts.TargetHost, opts.TargetPort)
92
93 // Configure client and transport
94 var client *http.Client
95 if isTLS {
96 tlsCfg := exfil.TLSConfig{
97 ServerName: opts.TargetHost,
98 InsecureSkipVerify: opts.InsecureTLS,
99 Fingerprint: opts.TLSFingerprint,
100 ALPN: []string{"http/1.1"},
101 }
102 tr := exfil.NewHTTPTransport(tlsCfg)
103 if opts.ProxyDialer != nil {
104 tr.DialContext = opts.ProxyDialer.DialContext
105 }
106 client = &http.Client{Transport: tr, Timeout: opts.Timeout}
107 } else {
108 tr := &http.Transport{
109 MaxIdleConns: 100,
110 IdleConnTimeout: 90 * time.Second,
111 TLSHandshakeTimeout: 10 * time.Second,
112 }
113 if opts.ProxyDialer != nil {
114 tr.DialContext = opts.ProxyDialer.DialContext
115 }
116 client = &http.Client{Transport: tr, Timeout: opts.Timeout}
117 }
118 defer client.CloseIdleConnections()
119
120 result.Timestamps.ConnectedTCP = time.Now().UTC()
121 if isTLS {
122 result.Timestamps.HandshakeTLS = time.Now().UTC()
123 }
124
125 result.Timestamps.FirstByteSent = time.Now().UTC()
126 var lastRespBody []byte
127 var headEvidence []byte
128 var tailEvidence []byte
129 var totalPayloadBytes int64
130
131 for {
132 chunkFrame, seq, err := chunker.Next()
133 if err == io.EOF {
134 break
135 }
136 if err != nil {
137 result.Verdict = common.ClassifyError(err)
138 result.ErrorMessage = err.Error()
139 result.Timestamps.TeardownDone = time.Now().UTC()
140 e.calculateDurations(&result)
141 return &result, err
142 }
143
144 if len(chunkFrame) > common.ChunkHeaderSize {
145 p := chunkFrame[common.ChunkHeaderSize:]
146 totalPayloadBytes += int64(len(p))
147
148 if len(headEvidence) < 768 {
149 needed := 768 - len(headEvidence)
150 if needed > len(p) {
151 needed = len(p)
152 }
153 headEvidence = append(headEvidence, p[:needed]...)
154 }
155
156 if len(p) >= 256 {
157 tailEvidence = append(tailEvidence[:0], p[len(p)-256:]...)
158 } else {
159 tailEvidence = append(tailEvidence, p...)
160 if len(tailEvidence) > 256 {
161 tailEvidence = tailEvidence[len(tailEvidence)-256:]
162 }
163 }
164 }
165
166 // Shaper pacing
167 if opts.Shaper != nil {
168 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
169 result.Verdict = common.ClassifyError(err)
170 result.ErrorMessage = err.Error()
171 result.Timestamps.TeardownDone = time.Now().UTC()
172 e.calculateDurations(&result)
173 return &result, err
174 }
175 }
176
177 // Cadence jitter
178 if opts.Cadence != nil {
179 delay := opts.Cadence.NextDelay()
180 if delay > 0 {
181 select {
182 case <-time.After(delay):
183 case <-ctx.Done():
184 result.Verdict = common.ClassifyError(ctx.Err())
185 result.ErrorMessage = ctx.Err().Error()
186 result.Timestamps.TeardownDone = time.Now().UTC()
187 e.calculateDurations(&result)
188 return &result, ctx.Err()
189 }
190 }
191 }
192
193 var req *http.Request
194 if opts.HTTPMode == "multipart" {
195 req, err = e.createMultipartRequest(ctx, targetURL, opts, int(seq), totalChunks, chunkFrame)
196 } else {
197 req, err = e.createBinaryRequest(ctx, targetURL, opts, int(seq), totalChunks, chunkFrame)
198 }
199 if err != nil {
200 result.Verdict = common.ClassifyError(err)
201 result.ErrorMessage = err.Error()
202 result.Timestamps.TeardownDone = time.Now().UTC()
203 e.calculateDurations(&result)
204 return &result, err
205 }
206
207 for k, v := range opts.Headers {
208 req.Header.Set(k, v)
209 }
210
211 resp, err := client.Do(req)
212 if err != nil {
213 result.Verdict = common.ClassifyError(err)
214 result.ErrorMessage = err.Error()
215 result.Timestamps.TeardownDone = time.Now().UTC()
216 e.calculateDurations(&result)
217 return &result, err
218 }
219
220 respBody, _ := io.ReadAll(resp.Body)
221 _ = resp.Body.Close()
222 lastRespBody = respBody
223 result.BytesSent += int64(len(chunkFrame))
224 result.BytesRecv += int64(len(respBody))
225
226 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
227 result.Timestamps.TeardownDone = time.Now().UTC()
228 e.calculateDurations(&result)
229
230 var ack common.VerificationACK
231 if jsonErr := json.Unmarshal(respBody, &ack); jsonErr == nil && ack.Verdict != "" {
232 result.Verdict = ack.Verdict
233 result.ErrorMessage = ack.ErrorMessage
234 } else {
235 result.Verdict = common.VerdictBlockedL7
236 result.ErrorMessage = fmt.Sprintf("server returned status %d: %s", resp.StatusCode, string(respBody))
237 }
238 return &result, fmt.Errorf("%s", result.ErrorMessage)
239 }
240 }
241
242 result.Timestamps.AckReceived = time.Now().UTC()
243 result.Timestamps.TeardownDone = time.Now().UTC()
244
245 // Parse and strictly verify cryptographic verification ACK
246 var ack common.VerificationACK
247 if err := json.Unmarshal(lastRespBody, &ack); err != nil {
248 result.Verdict = common.VerdictBlockedL7
249 result.ErrorMessage = fmt.Sprintf("failed to parse verification ACK: %v", err)
250 e.calculateDurations(&result)
251 return &result, fmt.Errorf("invalid verification ACK: %w", err)
252 }
253
254 verdict, verifyErr := ack.Verify(opts.ExpectedHash, totalChunks)
255 result.Verdict = verdict
256 if verifyErr != nil {
257 result.ErrorMessage = verifyErr.Error()
258 e.calculateDurations(&result)
259 return &result, verifyErr
260 }
261
262 result.RawEvidence = common.FormatBoundedRawEvidenceStream(headEvidence, tailEvidence, totalPayloadBytes)
263 e.calculateDurations(&result)
264 return &result, nil
265}
266
267func (e *HTTP1Exfiltrator) createBinaryRequest(ctx context.Context, url string, opts exfil.ExfilOptions, seq, total int, chunkFrame []byte) (*http.Request, error) {
268 req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(chunkFrame))
269 if err != nil {
270 return nil, err
271 }
272 req.Header.Set("Content-Type", "application/octet-stream")
273 req.Header.Set(common.HeaderSessionID, opts.SessionToken.String())
274 req.Header.Set(common.HeaderSequence, fmt.Sprintf("%d", seq))
275 req.Header.Set(common.HeaderTotalChunks, fmt.Sprintf("%d", total))
276 req.Header.Set(common.HeaderPayloadLength, fmt.Sprintf("%d", len(chunkFrame)-common.ChunkHeaderSize))
277 req.Header.Set(common.HeaderPayloadHash, opts.ExpectedHash)
278 for k, v := range opts.Headers {
279 req.Header.Set(k, v)
280 }
281 return req, nil
282}
283
284func (e *HTTP1Exfiltrator) createMultipartRequest(ctx context.Context, url string, opts exfil.ExfilOptions, seq, total int, chunkFrame []byte) (*http.Request, error) {
285 body := &bytes.Buffer{}
286 writer := multipart.NewWriter(body)
287
288 _ = writer.WriteField("session_id", opts.SessionToken.String())
289 _ = writer.WriteField("sequence", fmt.Sprintf("%d", seq))
290 _ = writer.WriteField("total_chunks", fmt.Sprintf("%d", total))
291 _ = writer.WriteField("payload_hash", opts.ExpectedHash)
292
293 part, err := writer.CreateFormFile("file", "chunk.bin")
294 if err != nil {
295 return nil, err
296 }
297
298 payload := chunkFrame
299 if len(chunkFrame) >= common.ChunkHeaderSize {
300 payload = chunkFrame[common.ChunkHeaderSize:]
301 }
302 if _, err := part.Write(payload); err != nil {
303 return nil, err
304 }
305
306 if err := writer.Close(); err != nil {
307 return nil, err
308 }
309
310 req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
311 if err != nil {
312 return nil, err
313 }
314 req.Header.Set("Content-Type", writer.FormDataContentType())
315 for k, v := range opts.Headers {
316 req.Header.Set(k, v)
317 }
318 return req, nil
319}
320
321func (e *HTTP1Exfiltrator) calculateDurations(r *common.ProbeResult) {
322 t := &r.Timestamps
323 if t.TeardownDone.IsZero() {
324 t.TeardownDone = time.Now().UTC()
325 }
326 t.ComputeDurations()
327 if t.TransferDuration > 0 {
328 r.ThroughputBps = float64(r.BytesSent*8) / (float64(t.TransferDuration) / float64(time.Second)) / 1000.0
329 }
330}
331
332// Ensure net import is retained
333var _ net.Conn