● 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 / smb / smb_quic.go
← Back to directory
218 lines · 6.0 KB
Raw
1package smb
2
3import (
4 "context"
5 "crypto/tls"
6 "fmt"
7 "io"
8 "net"
9 "strconv"
10 "time"
11
12 "github.com/quic-go/quic-go"
13 "github.com/shehzade/contraband/pkg/client/evasion"
14 "github.com/shehzade/contraband/pkg/client/exfil"
15 "github.com/shehzade/contraband/pkg/common"
16)
17
18func init() {
19 exfil.RegisterExfiltrator(common.ProtoSMBQUIC, NewSMBQUICExfiltrator())
20}
21
22// SMBQUICExfiltrator implements MS-SMB2 over QUIC (RFC 9000 + MS-SMB2) on UDP 443 with ALPN "smb".
23type SMBQUICExfiltrator struct{}
24
25func NewSMBQUICExfiltrator() *SMBQUICExfiltrator {
26 return &SMBQUICExfiltrator{}
27}
28
29func (e *SMBQUICExfiltrator) Protocol() string {
30 return common.ProtoSMBQUIC
31}
32
33func (e *SMBQUICExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
34 if err := opts.Validate(); err != nil {
35 return nil, err
36 }
37
38 start := time.Now().UTC()
39 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
40 if err != nil {
41 return nil, err
42 }
43 result.Timestamps.StartTCP = start
44 result.PayloadType = opts.PayloadType
45 result.CarrierType = opts.CarrierType
46
47 if opts.SessionToken.IsZero() {
48 tok, tokErr := common.NewSessionToken()
49 if tokErr != nil {
50 return nil, tokErr
51 }
52 opts.SessionToken = tok
53 }
54
55 var chunker *evasion.StreamChunker
56 if reader != nil {
57 c, cErr := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
58 if cErr == nil {
59 chunker = c
60 defer chunker.Close()
61 }
62 }
63
64 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
65 tlsConf := &tls.Config{
66 ServerName: opts.TargetHost,
67 NextProtos: []string{"smb"},
68 InsecureSkipVerify: opts.InsecureTLS,
69 }
70
71 quicConf := &quic.Config{
72 HandshakeIdleTimeout: opts.Timeout,
73 MaxIdleTimeout: opts.Timeout,
74 }
75
76 // Dial QUIC session with ALPN "smb"
77 qConn, dialErr := quic.DialAddr(ctx, targetAddr, tlsConf, quicConf)
78 if dialErr != nil {
79 result.Verdict = ClassifySMBError(dialErr)
80 result.ErrorMessage = dialErr.Error()
81 calculateDurations(&result)
82 return &result, dialErr
83 }
84 defer qConn.CloseWithError(0, "exfil done")
85
86 result.Timestamps.ConnectedTCP = time.Now().UTC()
87 result.Timestamps.HandshakeTLS = time.Now().UTC()
88 result.Timestamps.FirstByteSent = time.Now().UTC()
89
90 stream, streamErr := qConn.OpenStreamSync(ctx)
91 if streamErr != nil {
92 result.Verdict = ClassifySMBError(streamErr)
93 result.ErrorMessage = streamErr.Error()
94 calculateDurations(&result)
95 return &result, streamErr
96 }
97 defer stream.Close()
98
99 // Transmit raw SMB2 Negotiate Protocol packet on QUIC stream (no NetBIOS header)
100 negPkt := BuildSMB2NegotiateRequest(0)
101 if _, err := stream.Write(negPkt); err != nil {
102 result.Verdict = ClassifySMBError(err)
103 result.ErrorMessage = fmt.Sprintf("failed to write SMB2 Negotiate to QUIC stream: %v", err)
104 calculateDurations(&result)
105 return &result, err
106 }
107 result.BytesSent += int64(len(negPkt))
108
109 // Read SMB2 Negotiate Response
110 respBuf := make([]byte, 1024)
111 n, readErr := stream.Read(respBuf)
112 if readErr != nil && readErr != io.EOF {
113 result.Verdict = ClassifySMBError(readErr)
114 result.ErrorMessage = fmt.Sprintf("failed to read SMB2 Negotiate from QUIC stream: %v", readErr)
115 calculateDurations(&result)
116 return &result, readErr
117 }
118 result.BytesRecv += int64(n)
119
120 if n < 64 {
121 err := fmt.Errorf("SMB2 Negotiate response truncated over QUIC: %d bytes", n)
122 result.Verdict = common.VerdictBlockedL7
123 result.ErrorMessage = err.Error()
124 calculateDurations(&result)
125 return &result, err
126 }
127
128 _, err = DecodeSMB2Header(respBuf[:n])
129 if err != nil {
130 result.Verdict = common.VerdictBlockedL7
131 result.ErrorMessage = fmt.Sprintf("invalid SMB2 header over QUIC: %v", err)
132 calculateDurations(&result)
133 return &result, err
134 }
135
136 // If chunker exists, also stream chunks across the QUIC stream
137 var headEvidence []byte
138 if chunker != nil {
139 for {
140 chunkFrame, _, err := chunker.Next()
141 if err == io.EOF {
142 break
143 }
144 if err != nil {
145 result.Verdict = ClassifySMBError(err)
146 result.ErrorMessage = err.Error()
147 calculateDurations(&result)
148 return &result, err
149 }
150
151 if len(headEvidence) < 1024 {
152 take := 1024 - len(headEvidence)
153 if take > len(chunkFrame) {
154 take = len(chunkFrame)
155 }
156 headEvidence = append(headEvidence, chunkFrame[:take]...)
157 }
158
159 if opts.Shaper != nil {
160 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
161 result.Verdict = ClassifySMBError(err)
162 result.ErrorMessage = err.Error()
163 calculateDurations(&result)
164 return &result, err
165 }
166 }
167 if opts.Cadence != nil {
168 delay := opts.Cadence.NextDelay()
169 if delay > 0 {
170 select {
171 case <-time.After(delay):
172 case <-ctx.Done():
173 result.Verdict = ClassifySMBError(ctx.Err())
174 result.ErrorMessage = ctx.Err().Error()
175 calculateDurations(&result)
176 return &result, ctx.Err()
177 }
178 }
179 }
180
181 // In SMB over QUIC, write chunk frame directly to stream
182 if _, err := stream.Write(chunkFrame); err != nil {
183 result.Verdict = ClassifySMBError(err)
184 result.ErrorMessage = err.Error()
185 calculateDurations(&result)
186 return &result, err
187 }
188 result.BytesSent += int64(len(chunkFrame))
189
190 ackBuf := make([]byte, 1)
191 _ = stream.SetReadDeadline(time.Now().Add(opts.Timeout))
192 if _, err := io.ReadFull(stream, ackBuf); err != nil && err != io.EOF {
193 result.Verdict = ClassifySMBError(err)
194 result.ErrorMessage = fmt.Sprintf("failed to receive chunk ACK over QUIC: %v", err)
195 calculateDurations(&result)
196 return &result, err
197 }
198 }
199 }
200
201 result.Timestamps.AckReceived = time.Now().UTC()
202 result.Timestamps.TeardownDone = time.Now().UTC()
203 result.Verdict = common.VerdictSuccess
204 if chunker != nil {
205 if opts.ExpectedHash != "" {
206 result.PayloadHash = opts.ExpectedHash
207 } else {
208 result.PayloadHash = chunker.PayloadHashHex()
209 }
210 result.TargetVolume = result.BytesSent
211 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
212 } else {
213 result.RawEvidence = common.FormatBoundedRawEvidence(respBuf[:n])
214 }
215 calculateDurations(&result)
216
217 return &result, nil
218}