● 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 / grpc.go
← Back to directory
255 lines · 7.3 KB
Raw
1package web
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "fmt"
9 "io"
10 "strings"
11 "time"
12
13 "github.com/shehzade/contraband/pkg/client/evasion"
14 "github.com/shehzade/contraband/pkg/client/exfil"
15 "github.com/shehzade/contraband/pkg/common"
16 "github.com/shehzade/contraband/proto"
17 "google.golang.org/grpc"
18 "google.golang.org/grpc/credentials/insecure"
19 "google.golang.org/grpc/metadata"
20)
21
22type GRPCExfiltrator struct{}
23
24func NewGRPCExfiltrator() *GRPCExfiltrator {
25 return &GRPCExfiltrator{}
26}
27
28func init() {
29 exfil.RegisterExfiltrator(common.ProtoGRPC, NewGRPCExfiltrator())
30}
31
32func (e *GRPCExfiltrator) Protocol() string {
33 return common.ProtoGRPC
34}
35
36func (e *GRPCExfiltrator) 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
51 if opts.TotalBytes <= 0 {
52 if br, ok := reader.(*bytes.Reader); ok {
53 opts.TotalBytes = int64(br.Len())
54 } else if bb, ok := reader.(*bytes.Buffer); ok {
55 opts.TotalBytes = int64(bb.Len())
56 }
57 }
58
59 if opts.ExpectedHash == "" {
60 if br, ok := reader.(*bytes.Reader); ok {
61 cur, _ := br.Seek(0, io.SeekCurrent)
62 h := sha256.New()
63 _, _ = io.Copy(h, br)
64 opts.ExpectedHash = hex.EncodeToString(h.Sum(nil))
65 _, _ = br.Seek(cur, io.SeekStart)
66 }
67 }
68 result.PayloadHash = opts.ExpectedHash
69
70 if opts.SessionToken.IsZero() {
71 tok, tokErr := common.NewSessionToken()
72 if tokErr != nil {
73 return nil, tokErr
74 }
75 opts.SessionToken = tok
76 }
77
78 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
79 if err != nil {
80 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
81 }
82 defer chunker.Close()
83
84 totalChunks := uint32(chunker.TotalChunks())
85 result.TargetVolume = opts.TotalBytes
86
87 isTLS := opts.UseTLS || (opts.TargetPort != 80 && opts.InsecureTLS) || opts.TargetPort == 443 || opts.TargetPort == 8443 || opts.TargetPort == 9443 || opts.TargetPort == 10443
88 targetAddr := fmt.Sprintf("%s:%d", opts.TargetHost, opts.TargetPort)
89
90 var dialOpts []grpc.DialOption
91 if isTLS {
92 tlsCfg := exfil.TLSConfig{
93 ServerName: opts.TargetHost,
94 InsecureSkipVerify: opts.InsecureTLS,
95 Fingerprint: opts.TLSFingerprint,
96 ALPN: []string{"h2"},
97 }
98 dialOpts = append(dialOpts, grpc.WithTransportCredentials(exfil.NewGRPCTransportCredentials(tlsCfg)))
99 } else {
100 dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
101 }
102
103 conn, err := grpc.NewClient(targetAddr, dialOpts...)
104 if err != nil {
105 result.Verdict = common.ClassifyError(err)
106 result.ErrorMessage = fmt.Sprintf("grpc dial failed: %v", err)
107 result.Timestamps.TeardownDone = time.Now().UTC()
108 e.calculateDurations(&result)
109 return &result, err
110 }
111 defer conn.Close()
112
113 client := proto.NewExfilServiceClient(conn)
114
115 result.Timestamps.ConnectedTCP = time.Now().UTC()
116 if isTLS {
117 result.Timestamps.HandshakeTLS = time.Now().UTC()
118 }
119 result.Timestamps.FirstByteSent = time.Now().UTC()
120
121 mdMap := make(map[string]string)
122 for k, v := range opts.Headers {
123 mdMap[strings.ToLower(k)] = v
124 }
125 if _, ok := mdMap[strings.ToLower(common.HeaderProtocol)]; !ok {
126 mdMap[strings.ToLower(common.HeaderProtocol)] = common.ProtoGRPC
127 }
128 ctx = metadata.NewOutgoingContext(ctx, metadata.New(mdMap))
129
130 stream, err := client.UploadStream(ctx)
131 if err != nil {
132 result.Verdict = common.ClassifyError(err)
133 result.ErrorMessage = fmt.Sprintf("failed to open grpc upload stream: %v", err)
134 result.Timestamps.TeardownDone = time.Now().UTC()
135 e.calculateDurations(&result)
136 return &result, err
137 }
138
139 var headEvidence []byte
140 for {
141 chunkFrame, seq, err := chunker.Next()
142 if err == io.EOF {
143 break
144 }
145 if err != nil {
146 result.Verdict = common.ClassifyError(err)
147 result.ErrorMessage = err.Error()
148 result.Timestamps.TeardownDone = time.Now().UTC()
149 e.calculateDurations(&result)
150 return &result, err
151 }
152
153 if len(headEvidence) < 768 && len(chunkFrame) > common.ChunkHeaderSize {
154 p := chunkFrame[common.ChunkHeaderSize:]
155 needed := 768 - len(headEvidence)
156 if needed > len(p) {
157 needed = len(p)
158 }
159 headEvidence = append(headEvidence, p[:needed]...)
160 }
161
162 if opts.Shaper != nil {
163 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
164 result.Verdict = common.ClassifyError(err)
165 result.ErrorMessage = err.Error()
166 result.Timestamps.TeardownDone = time.Now().UTC()
167 e.calculateDurations(&result)
168 return &result, err
169 }
170 }
171
172 if opts.Cadence != nil {
173 delay := opts.Cadence.NextDelay()
174 if delay > 0 {
175 select {
176 case <-time.After(delay):
177 case <-ctx.Done():
178 result.Verdict = common.ClassifyError(ctx.Err())
179 result.ErrorMessage = ctx.Err().Error()
180 result.Timestamps.TeardownDone = time.Now().UTC()
181 e.calculateDurations(&result)
182 return &result, ctx.Err()
183 }
184 }
185 }
186
187 rawChunk := chunkFrame
188 if len(chunkFrame) >= common.ChunkHeaderSize {
189 rawChunk = chunkFrame[common.ChunkHeaderSize:]
190 }
191
192 req := &proto.ChunkRequest{
193 Token: opts.SessionToken[:],
194 Sequence: seq,
195 TotalChunks: totalChunks,
196 Payload: rawChunk,
197 ExpectedHash: opts.ExpectedHash,
198 }
199
200 if err := stream.Send(req); err != nil {
201 result.Verdict = common.ClassifyError(err)
202 result.ErrorMessage = fmt.Sprintf("grpc stream send failed at seq %d: %v", seq, err)
203 result.Timestamps.TeardownDone = time.Now().UTC()
204 e.calculateDurations(&result)
205 return &result, err
206 }
207 result.BytesSent += int64(len(rawChunk))
208 }
209
210 resp, err := stream.CloseAndRecv()
211 if err != nil {
212 result.Verdict = common.ClassifyError(err)
213 result.ErrorMessage = fmt.Sprintf("grpc stream receive ack failed: %v", err)
214 result.Timestamps.TeardownDone = time.Now().UTC()
215 e.calculateDurations(&result)
216 return &result, err
217 }
218
219 result.Timestamps.AckReceived = time.Now().UTC()
220 result.Timestamps.TeardownDone = time.Now().UTC()
221
222 if !resp.Success {
223 if resp.Verdict != "" {
224 result.Verdict = common.Verdict(resp.Verdict)
225 } else {
226 result.Verdict = common.VerdictBlockedL7
227 }
228 result.ErrorMessage = resp.ErrorMessage
229 e.calculateDurations(&result)
230 return &result, fmt.Errorf("grpc server rejected exfiltration: %s", resp.ErrorMessage)
231 }
232
233 if opts.ExpectedHash != "" && !strings.EqualFold(resp.PayloadHash, opts.ExpectedHash) {
234 result.Verdict = common.VerdictBlockedDLP
235 result.ErrorMessage = fmt.Sprintf("cryptographic hash mismatch: expected %s, got %s", opts.ExpectedHash, resp.PayloadHash)
236 e.calculateDurations(&result)
237 return &result, fmt.Errorf("%s", result.ErrorMessage)
238 }
239
240 result.Verdict = common.VerdictSuccess
241 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
242 e.calculateDurations(&result)
243 return &result, nil
244}
245
246func (e *GRPCExfiltrator) calculateDurations(r *common.ProbeResult) {
247 t := &r.Timestamps
248 if t.TeardownDone.IsZero() {
249 t.TeardownDone = time.Now().UTC()
250 }
251 t.ComputeDurations()
252 if t.TransferDuration > 0 {
253 r.ThroughputBps = float64(r.BytesSent*8) / (float64(t.TransferDuration) / float64(time.Second)) / 1000.0
254 }
255}