123 lines
4.6 KiB
Go
123 lines
4.6 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// NewSMBHandler logs incoming SMB negotiate/session setup attempts and returns a minimal response.
|
|
// It is NOT a full SMB implementation; it's enough to keep scanners interacting and capture tokens.
|
|
func NewSMBHandler(log LoggerFunc) Handler {
|
|
return func(conn net.Conn) {
|
|
defer conn.Close()
|
|
remote := conn.RemoteAddr().String()
|
|
conn.SetDeadline(time.Now().Add(15 * time.Second))
|
|
buf := make([]byte, 2048)
|
|
n, _ := conn.Read(buf)
|
|
if n <= 0 {
|
|
return
|
|
}
|
|
first := buf[:n]
|
|
det := map[string]string{"event":"smb_probe","bytes_received":strconv.Itoa(n)}
|
|
// rough fingerprint if NTLMSSP is present
|
|
if bytes.Contains(first, []byte("NTLMSSP")) {
|
|
det["ntlmssp"] = "present"
|
|
if u, d, w, lmLen, ntLen := parseNTLMSSP(first); u != "" || d != "" || w != "" {
|
|
if u != "" { det["user"] = u }
|
|
if d != "" { det["domain"] = d }
|
|
if w != "" { det["workstation"] = w }
|
|
if lmLen > 0 { det["lm_resp_len"] = strconv.Itoa(lmLen) }
|
|
if ntLen > 0 { det["nt_resp_len"] = strconv.Itoa(ntLen) }
|
|
}
|
|
}
|
|
// Try to guess dialect by sniffing strings
|
|
fstr := strings.ToUpper(string(first))
|
|
if strings.Contains(fstr, "SMB 2.1") || strings.Contains(fstr, "SMB2") {
|
|
det["dialect"] = "smb2"
|
|
} else if strings.Contains(fstr, "SMB 3") {
|
|
det["dialect"] = "smb3"
|
|
} else {
|
|
det["dialect"] = "unknown"
|
|
}
|
|
log(Record{Timestamp: Now(), RemoteAddr: remoteIP(remote), RemotePort: remotePort(remote), Service: "smb", Details: det})
|
|
|
|
// Send a minimal SMB2 NEGOTIATE failure-like response (not a valid implementation, just a banner)
|
|
resp := []byte{
|
|
0x00, 0x00, 0x00, 0x3F, // NetBIOS length header (len following)
|
|
0xFE, 0x53, 0x4D, 0x42, // SMB2 magic
|
|
0x40, 0x00, 0x00, 0x00, // header fields
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x24, 0x00, // StructureSize
|
|
0x00, 0x00, // SecurityMode
|
|
0x01, 0x00, // DialectRevision (SMB 2.1)
|
|
0x00, 0x00, // NegotiateContextCount
|
|
0x00, 0x00, 0x00, 0x00, // ServerGuid part (fake)
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, // Capabilities & MaxTrans/Read/WriteSize (fake zeros)
|
|
}
|
|
_, _ = conn.Write(resp)
|
|
}
|
|
}
|
|
|
|
// parseNTLMSSP tries to extract fields from an NTLMSSP message embedded in SMB payloads.
|
|
// It supports Type 1, 2, and 3 and focuses on Type 3 credential info.
|
|
func parseNTLMSSP(p []byte) (username, domain, workstation string, lmLen, ntLen int) {
|
|
sig := []byte("NTLMSSP\x00")
|
|
i := bytes.Index(p, sig)
|
|
if i < 0 || i+12 >= len(p) { return "","","",0,0 }
|
|
// Message type at offset i+8 (LE uint32)
|
|
if i+12 > len(p) { return }
|
|
mtype := binary.LittleEndian.Uint32(p[i+8 : i+12])
|
|
switch mtype {
|
|
case 1: // Negotiate - no creds
|
|
return "","","",0,0
|
|
case 2: // Challenge - no creds to extract
|
|
return "","","",0,0
|
|
case 3: // Authenticate - contains domain, user, workstation
|
|
// Offsets are relative to start of NTLMSSP message
|
|
base := i
|
|
// Helper to read fields: len(2), maxLen(2), offset(4)
|
|
readField := func(off int) (l, o int) {
|
|
if base+off+8 > len(p) { return 0, 0 }
|
|
l = int(binary.LittleEndian.Uint16(p[base+off : base+off+2]))
|
|
o = int(binary.LittleEndian.Uint32(p[base+off+4 : base+off+8]))
|
|
return
|
|
}
|
|
// Per spec Type 3 layout:
|
|
// 0x14 LMResp, 0x1C NTResp, 0x24 DomainName, 0x2C UserName, 0x34 Workstation, 0x3C EncryptedRandomSessionKey
|
|
lmLen, _ = readField(0x14)
|
|
ntLen, _ = readField(0x1C)
|
|
domLen, domOff := readField(0x24)
|
|
usrLen, usrOff := readField(0x2C)
|
|
wsLen, wsOff := readField(0x34)
|
|
// Extract UTF-16LE strings safely
|
|
username = readUTF16LE(p, usrOff, usrLen)
|
|
domain = readUTF16LE(p, domOff, domLen)
|
|
workstation = readUTF16LE(p, wsOff, wsLen)
|
|
return
|
|
default:
|
|
return "","","",0,0
|
|
}
|
|
}
|
|
|
|
func readUTF16LE(p []byte, off, length int) string {
|
|
if off <= 0 || length <= 1 || off+length > len(p) { return "" }
|
|
// best-effort: convert UTF-16LE to UTF-8 by dropping high bytes if ASCII
|
|
// for simplicity; full decoding not necessary for logging
|
|
b := make([]byte, 0, length/2)
|
|
for j := 0; j+1 < length && off+j+1 < len(p); j += 2 {
|
|
b = append(b, p[off+j])
|
|
}
|
|
s := strings.TrimSpace(string(b))
|
|
return s
|
|
}
|