Skip to main content

netget/server/smb/
auth.rs

1//! The authentication messages SESSION_SETUP carries: SPNEGO (RFC 4178) around NTLMSSP
2//! (MS-NLMP).
3//!
4//! **This authenticates nothing.** No password is checked and no session key is derived: the
5//! server walks a client through the three NTLMSSP messages because that is the only way a real
6//! client will finish SESSION_SETUP, and it hands the *model* the user name the client
7//! presented so the model can admit or refuse it. Every session it grants is reported to the
8//! client as a guest session (or a null session, for an anonymous login), which is what tells
9//! a client not to expect signing — there is no key to sign with.
10//!
11//! The encodings are fixed shapes written out by hand rather than a general ASN.1 codec: the
12//! server emits three tokens and reads one field out of the client's, and a DER encoder for
13//! three tokens is smaller than a dependency.
14
15/// Signature at the start of every NTLMSSP message.
16pub const NTLMSSP_SIGNATURE: &[u8; 8] = b"NTLMSSP\0";
17
18pub const NTLMSSP_NEGOTIATE: u32 = 1;
19pub const NTLMSSP_CHALLENGE: u32 = 2;
20pub const NTLMSSP_AUTHENTICATE: u32 = 3;
21
22// NegotiateFlags (MS-NLMP 2.2.2.5).
23pub const NEGOTIATE_UNICODE: u32 = 0x0000_0001;
24pub const NEGOTIATE_OEM: u32 = 0x0000_0002;
25pub const REQUEST_TARGET: u32 = 0x0000_0004;
26pub const NEGOTIATE_SIGN: u32 = 0x0000_0010;
27pub const NEGOTIATE_SEAL: u32 = 0x0000_0020;
28pub const NEGOTIATE_NTLM: u32 = 0x0000_0200;
29pub const NEGOTIATE_ANONYMOUS: u32 = 0x0000_0800;
30pub const NEGOTIATE_ALWAYS_SIGN: u32 = 0x0000_8000;
31pub const TARGET_TYPE_SERVER: u32 = 0x0002_0000;
32pub const NEGOTIATE_EXTENDED_SESSIONSECURITY: u32 = 0x0008_0000;
33pub const NEGOTIATE_TARGET_INFO: u32 = 0x0080_0000;
34pub const NEGOTIATE_VERSION: u32 = 0x0200_0000;
35pub const NEGOTIATE_128: u32 = 0x2000_0000;
36pub const NEGOTIATE_KEY_EXCH: u32 = 0x4000_0000;
37pub const NEGOTIATE_56: u32 = 0x8000_0000;
38
39/// Flags the server echoes when the client asked for them.
40const ECHOED_FLAGS: u32 = NEGOTIATE_SIGN
41    | NEGOTIATE_SEAL
42    | NEGOTIATE_EXTENDED_SESSIONSECURITY
43    | NEGOTIATE_VERSION
44    | NEGOTIATE_128
45    | NEGOTIATE_KEY_EXCH
46    | NEGOTIATE_56;
47
48/// Name the server gives itself in the CHALLENGE's target fields.
49const TARGET_NETBIOS: &str = "NETGET";
50const TARGET_DNS: &str = "netget";
51
52/// DER encoding of the NTLMSSP mechanism OID, 1.3.6.1.4.1.311.2.2.10.
53const NTLMSSP_OID: &[u8] = &[
54    0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0a,
55];
56/// DER encoding of the SPNEGO OID, 1.3.6.1.5.5.2.
57const SPNEGO_OID: &[u8] = &[0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
58
59/// How the client wrapped its NTLMSSP token, which is how the reply is wrapped.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum Wrapping {
62    /// A bare NTLMSSP message in the security buffer.
63    Raw,
64    /// NTLMSSP inside SPNEGO (`negTokenInit` first, `negTokenResp` after).
65    Spnego,
66}
67
68/// Encode one DER TLV.
69fn der(tag: u8, content: &[u8]) -> Vec<u8> {
70    let mut out = vec![tag];
71    let len = content.len();
72    if len < 0x80 {
73        out.push(len as u8);
74    } else if len <= 0xFF {
75        out.extend_from_slice(&[0x81, len as u8]);
76    } else {
77        out.extend_from_slice(&[0x82, (len >> 8) as u8, len as u8]);
78    }
79    out.extend_from_slice(content);
80    out
81}
82
83/// The security buffer of the NEGOTIATE response: a GSS-API `InitialContextToken` carrying an
84/// SPNEGO `negTokenInit` that offers NTLMSSP and nothing else.
85pub fn negotiate_blob() -> Vec<u8> {
86    let mech_types = der(0x30, NTLMSSP_OID);
87    let neg_token_init = der(0x30, &der(0xa0, &mech_types));
88    let mut inner = SPNEGO_OID.to_vec();
89    inner.extend_from_slice(&der(0xa0, &neg_token_init));
90    der(0x60, &inner)
91}
92
93/// SPNEGO `negTokenResp` (RFC 4178 4.2.2).
94fn neg_token_resp(neg_state: u8, supported_mech: bool, response_token: Option<&[u8]>) -> Vec<u8> {
95    let mut seq = der(0xa0, &[0x0a, 0x01, neg_state]);
96    if supported_mech {
97        seq.extend_from_slice(&der(0xa1, NTLMSSP_OID));
98    }
99    if let Some(token) = response_token {
100        seq.extend_from_slice(&der(0xa2, &der(0x04, token)));
101    }
102    der(0xa1, &der(0x30, &seq))
103}
104
105const NEG_STATE_ACCEPT_COMPLETED: u8 = 0;
106const NEG_STATE_ACCEPT_INCOMPLETE: u8 = 1;
107
108/// Wrap a CHALLENGE the way the client wrapped its NEGOTIATE.
109pub fn wrap_challenge(challenge: &[u8], wrapping: Wrapping) -> Vec<u8> {
110    match wrapping {
111        Wrapping::Raw => challenge.to_vec(),
112        Wrapping::Spnego => neg_token_resp(NEG_STATE_ACCEPT_INCOMPLETE, true, Some(challenge)),
113    }
114}
115
116/// The final security buffer of a granted session: SPNEGO `accept-completed`, or nothing at
117/// all for a bare NTLMSSP exchange, which has no fourth message.
118pub fn accept_completed(wrapping: Wrapping) -> Vec<u8> {
119    match wrapping {
120        Wrapping::Raw => Vec::new(),
121        Wrapping::Spnego => neg_token_resp(NEG_STATE_ACCEPT_COMPLETED, false, None),
122    }
123}
124
125/// Locate the NTLMSSP message in a SESSION_SETUP security buffer.
126///
127/// SPNEGO carries it as the OCTET STRING of `mechToken` (in `negTokenInit`) or `responseToken`
128/// (in `negTokenResp`). Rather than walk the DER, this finds the NTLMSSP signature: every field
129/// of an NTLMSSP message is addressed by an offset from that signature, so whatever follows the
130/// token inside the SPNEGO wrapper (a `mechListMIC`) sits past every field and is never read.
131/// A buffer with no NTLMSSP signature at all is a mechanism this server does not speak.
132pub fn find_ntlmssp(buffer: &[u8]) -> Option<(&[u8], Wrapping)> {
133    if buffer.starts_with(NTLMSSP_SIGNATURE) {
134        return Some((buffer, Wrapping::Raw));
135    }
136    let start = buffer
137        .windows(NTLMSSP_SIGNATURE.len())
138        .position(|w| w == NTLMSSP_SIGNATURE)?;
139    Some((&buffer[start..], Wrapping::Spnego))
140}
141
142/// The `MessageType` of an NTLMSSP message.
143pub fn message_type(token: &[u8]) -> Option<u32> {
144    (token.len() >= 12 && token.starts_with(NTLMSSP_SIGNATURE)).then(|| le32(token, 8))
145}
146
147/// The `NegotiateFlags` of an NTLMSSP NEGOTIATE message (MS-NLMP 2.2.1.1).
148pub fn negotiate_flags(token: &[u8]) -> u32 {
149    if token.len() >= 16 {
150        le32(token, 12)
151    } else {
152        NEGOTIATE_UNICODE
153    }
154}
155
156/// Build an NTLMSSP CHALLENGE (MS-NLMP 2.2.1.2) answering a NEGOTIATE with `client_flags`.
157pub fn challenge(client_flags: u32, server_challenge: [u8; 8], timestamp: u64) -> Vec<u8> {
158    let flags = NEGOTIATE_UNICODE
159        | REQUEST_TARGET
160        | NEGOTIATE_NTLM
161        | NEGOTIATE_ALWAYS_SIGN
162        | TARGET_TYPE_SERVER
163        | NEGOTIATE_TARGET_INFO
164        | (client_flags & ECHOED_FLAGS);
165
166    let target_name = utf16le(TARGET_NETBIOS);
167    let mut target_info = Vec::new();
168    for (id, value) in [
169        (2u16, utf16le(TARGET_NETBIOS)),          // MsvAvNbDomainName
170        (1u16, utf16le(TARGET_NETBIOS)),          // MsvAvNbComputerName
171        (4u16, utf16le(TARGET_DNS)),              // MsvAvDnsDomainName
172        (3u16, utf16le(TARGET_DNS)),              // MsvAvDnsComputerName
173        (7u16, timestamp.to_le_bytes().to_vec()), // MsvAvTimestamp
174    ] {
175        target_info.extend_from_slice(&id.to_le_bytes());
176        target_info.extend_from_slice(&(value.len() as u16).to_le_bytes());
177        target_info.extend_from_slice(&value);
178    }
179    target_info.extend_from_slice(&[0, 0, 0, 0]); // MsvAvEOL
180
181    const FIXED: usize = 56;
182    let target_name_offset = FIXED as u32;
183    let target_info_offset = target_name_offset + target_name.len() as u32;
184
185    let mut out = Vec::with_capacity(FIXED + target_name.len() + target_info.len());
186    out.extend_from_slice(NTLMSSP_SIGNATURE);
187    out.extend_from_slice(&NTLMSSP_CHALLENGE.to_le_bytes());
188    push_fields(&mut out, target_name.len(), target_name_offset);
189    out.extend_from_slice(&flags.to_le_bytes());
190    out.extend_from_slice(&server_challenge);
191    out.extend_from_slice(&[0u8; 8]); // Reserved
192    push_fields(&mut out, target_info.len(), target_info_offset);
193    // Version: 10.0.20348, NTLMSSP_REVISION_W2K3. Sent whether or not the client asked; the
194    // eight bytes are in the fixed part either way and are ignored without the flag.
195    out.extend_from_slice(&[10, 0]);
196    out.extend_from_slice(&20348u16.to_le_bytes());
197    out.extend_from_slice(&[0, 0, 0, 0x0F]);
198    debug_assert_eq!(out.len(), FIXED);
199    out.extend_from_slice(&target_name);
200    out.extend_from_slice(&target_info);
201    out
202}
203
204fn push_fields(out: &mut Vec<u8>, len: usize, offset: u32) {
205    out.extend_from_slice(&(len as u16).to_le_bytes()); // Len
206    out.extend_from_slice(&(len as u16).to_le_bytes()); // MaxLen
207    out.extend_from_slice(&offset.to_le_bytes()); // BufferOffset
208}
209
210/// What an NTLMSSP AUTHENTICATE message says about who is logging in.
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct Authenticate {
213    pub user: String,
214    pub domain: String,
215    pub workstation: String,
216    /// MS-NLMP 3.2.5.1.2: an anonymous login carries an empty user name and an empty NT
217    /// response (and an LM response that is empty or a single zero byte).
218    pub anonymous: bool,
219}
220
221/// Parse an NTLMSSP AUTHENTICATE (MS-NLMP 2.2.1.3). Every field is bounds-checked against the
222/// token; `None` if any is out of range.
223pub fn parse_authenticate(token: &[u8]) -> Option<Authenticate> {
224    if message_type(token)? != NTLMSSP_AUTHENTICATE || token.len() < 64 {
225        return None;
226    }
227    let flags = le32(token, 60);
228    let unicode = flags & NEGOTIATE_UNICODE != 0;
229    let lm = field(token, 12)?;
230    let nt = field(token, 20)?;
231    let text = |off: usize| -> Option<String> {
232        let bytes = field(token, off)?;
233        if unicode {
234            from_utf16le(bytes)
235        } else {
236            Some(String::from_utf8_lossy(bytes).into_owned())
237        }
238    };
239    let domain = text(28)?;
240    let user = text(36)?;
241    let workstation = text(44)?;
242    let anonymous = (flags & NEGOTIATE_ANONYMOUS != 0)
243        || (user.is_empty() && nt.is_empty() && (lm.is_empty() || lm == [0]));
244    Some(Authenticate {
245        user,
246        domain,
247        workstation,
248        anonymous,
249    })
250}
251
252/// The bytes a `Len`/`MaxLen`/`BufferOffset` field triple at `at` points to.
253fn field(token: &[u8], at: usize) -> Option<&[u8]> {
254    let len = u16::from_le_bytes([*token.get(at)?, *token.get(at + 1)?]) as usize;
255    let offset = le32(token.get(at + 4..at + 8)?, 0) as usize;
256    if len == 0 {
257        return Some(&[]);
258    }
259    token.get(offset..offset.checked_add(len)?)
260}
261
262fn le32(b: &[u8], off: usize) -> u32 {
263    u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
264}
265
266fn utf16le(s: &str) -> Vec<u8> {
267    super::wire::utf16le(s)
268}
269
270fn from_utf16le(b: &[u8]) -> Option<String> {
271    super::wire::from_utf16le(b)
272}