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