#![deny(unsafe_code)]
use dcerpc::ndr::{NdrDecoder, NdrEncoder};
use dcerpc::transport::SmbPipe;
use dcerpc::{Result, Syntax};
use ms_lsat::{access, opnum as lsat_opnum, PolicyHandle};
use smb2_client::SmbClient;
use windows_sddl::sid::Sid;
pub fn lsad_syntax() -> Syntax {
Syntax::new("12345778-1234-abcd-ef00-0123456789ab", 0, 0)
}
pub mod opnum {
pub const CLOSE: u16 = 0;
pub const ENUMERATE_TRUSTED_DOMAINS: u16 = 13;
pub const OPEN_POLICY2: u16 = 44;
pub const QUERY_TRUSTED_DOMAIN_INFO_BY_NAME: u16 = 48;
pub const ENUMERATE_TRUSTED_DOMAINS_EX: u16 = 50;
}
fn decode_sid(d: &mut NdrDecoder) -> Result<Sid> {
let _max = d.u32()?;
let revision = d.u8()?;
let count = d.u8()? as usize;
let auth = d.read_bytes(6)?;
let identifier_authority = auth.iter().fold(0u64, |acc, &b| (acc << 8) | b as u64);
if count
.checked_mul(4)
.map_or(true, |need| need > d.remaining())
{
return Err(dcerpc::RpcError::Protocol(format!(
"RPC_SID: SubAuthorityCount={count} exceeds remaining stub"
)));
}
let mut sub_authorities = Vec::with_capacity(count);
for _ in 0..count {
sub_authorities.push(d.u32()?);
}
Ok(Sid {
revision,
identifier_authority,
sub_authorities,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct TrustedDomainInfo {
pub name: String,
pub sid: Sid,
}
pub fn encode_enumerate_trusted_domains(
handle: &PolicyHandle,
enumeration_context: u32,
prefered_maximum_length: u32,
) -> Vec<u8> {
let mut e = NdrEncoder::new();
handle.encode(&mut e);
e.u32(enumeration_context);
e.u32(prefered_maximum_length);
e.into_bytes()
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct EnumerateTrustsPage {
pub trusts: Vec<TrustedDomainInfo>,
pub next_context: u32,
}
pub fn decode_enumerate_trusted_domains(stub: &[u8]) -> Result<EnumerateTrustsPage> {
let mut d = NdrDecoder::new(stub);
let next_context = d.u32()?;
let entries = d.u32()? as usize;
let info_ptr = d.u32()?;
if info_ptr == 0 {
return Ok(EnumerateTrustsPage {
trusts: Vec::new(),
next_context,
});
}
let _max_count = d.u32()?;
if entries
.checked_mul(12)
.map_or(true, |need| need > d.remaining())
{
return Err(dcerpc::RpcError::Protocol(format!(
"LsarEnumerateTrustedDomains: EntriesRead={entries} exceeds remaining stub"
)));
}
let mut headers: Vec<(u16, u32, u32)> = Vec::with_capacity(entries);
for _ in 0..entries {
let name_len = d.u16()?;
let _name_max = d.u16()?;
let name_ptr = d.u32()?;
let sid_ptr = d.u32()?;
headers.push((name_len, name_ptr, sid_ptr));
}
let mut trusts = Vec::with_capacity(entries);
for (_len, name_ptr, sid_ptr) in headers {
let name = if name_ptr != 0 {
d.conformant_varying_wstr()?
} else {
String::new()
};
let sid = if sid_ptr != 0 {
decode_sid(&mut d)?
} else {
Sid {
revision: 1,
identifier_authority: 0,
sub_authorities: vec![],
}
};
trusts.push(TrustedDomainInfo { name, sid });
}
Ok(EnumerateTrustsPage {
trusts,
next_context,
})
}
pub struct LsadClient<'a> {
pipe: SmbPipe<'a>,
handle: Option<PolicyHandle>,
}
impl<'a> LsadClient<'a> {
pub async fn bind(client: &'a mut SmbClient, file_id: [u8; 16]) -> Result<Self> {
let mut pipe = SmbPipe::new(client, file_id);
pipe.bind(lsad_syntax()).await?;
Ok(LsadClient { pipe, handle: None })
}
async fn ensure_handle(&mut self, system_name: &str) -> Result<PolicyHandle> {
if let Some(h) = self.handle {
return Ok(h);
}
let stub = ms_lsat::encode_open_policy2(system_name, access::MAXIMUM_ALLOWED);
let resp = self.pipe.call(lsat_opnum::OPEN_POLICY2, &stub).await?;
let mut d = NdrDecoder::new(&resp);
let h = PolicyHandle::decode(&mut d)?;
self.handle = Some(h);
Ok(h)
}
pub async fn enumerate_trusts(&mut self, system_name: &str) -> Result<Vec<TrustedDomainInfo>> {
let h = self.ensure_handle(system_name).await?;
let mut out: Vec<TrustedDomainInfo> = Vec::new();
let mut ctx: u32 = 0;
loop {
let stub = encode_enumerate_trusted_domains(&h, ctx, 8192);
let resp = self
.pipe
.call(opnum::ENUMERATE_TRUSTED_DOMAINS, &stub)
.await?;
let page = decode_enumerate_trusted_domains(&resp)?;
let got = page.trusts.len();
out.extend(page.trusts);
if page.next_context == 0 || got == 0 {
break;
}
ctx = page.next_context;
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use dcerpc::ndr::NdrEncoder;
fn encode_sid(e: &mut NdrEncoder, sid: &Sid) {
e.u32(sid.sub_authorities.len() as u32);
e.u8(sid.revision);
e.u8(sid.sub_authorities.len() as u8);
let a = sid.identifier_authority;
e.bytes(&[
(a >> 40) as u8,
(a >> 32) as u8,
(a >> 24) as u8,
(a >> 16) as u8,
(a >> 8) as u8,
a as u8,
]);
for s in &sid.sub_authorities {
e.u32(*s);
}
}
#[test]
fn enumerate_request_stub_is_fixed_28_bytes() {
let stub = encode_enumerate_trusted_domains(&PolicyHandle([0u8; 20]), 0, 8192);
assert_eq!(stub.len(), 28);
assert_eq!(u32::from_le_bytes(stub[20..24].try_into().unwrap()), 0);
assert_eq!(u32::from_le_bytes(stub[24..28].try_into().unwrap()), 8192);
}
#[test]
fn enumerate_context_is_returned_intact_on_empty_page() {
let mut e = NdrEncoder::new();
e.u32(42); e.u32(0); e.u32(0); let stub = e.into_bytes();
let page = decode_enumerate_trusted_domains(&stub).unwrap();
assert_eq!(page.next_context, 42);
assert!(page.trusts.is_empty());
}
#[test]
fn enumerate_roundtrip_single_trust() {
let dom_sid = Sid::parse("S-1-5-21-1111-2222-3333").unwrap();
let dn: Vec<u16> = "TRUSTED-CORP".encode_utf16().collect();
let dl = (dn.len() * 2) as u16;
let mut e = NdrEncoder::new();
e.u32(0); e.u32(1); e.referent(); e.u32(1); e.u16(dl);
e.u16(dl);
e.referent(); e.referent(); e.u32(dn.len() as u32);
e.u32(0);
e.u32(dn.len() as u32);
for u in &dn {
e.u16(*u);
}
encode_sid(&mut e, &dom_sid);
e.u32(0);
let stub = e.into_bytes();
let page = decode_enumerate_trusted_domains(&stub).unwrap();
assert_eq!(page.next_context, 0);
assert_eq!(page.trusts.len(), 1);
assert_eq!(page.trusts[0].name, "TRUSTED-CORP");
assert_eq!(page.trusts[0].sid.to_string(), "S-1-5-21-1111-2222-3333");
}
#[test]
fn enumerate_multiple_trusts_deferred_order() {
let sids = [
Sid::parse("S-1-5-21-10-20-30").unwrap(),
Sid::parse("S-1-5-21-40-50-60").unwrap(),
];
let names: Vec<Vec<u16>> = ["FOREST-A", "FOREST-B"]
.iter()
.map(|s| s.encode_utf16().collect())
.collect();
let mut e = NdrEncoder::new();
e.u32(0); e.u32(2); e.referent(); e.u32(2);
for name in &names {
let l = (name.len() * 2) as u16;
e.u16(l);
e.u16(l);
e.referent(); e.referent(); }
for (name, sid) in names.iter().zip(sids.iter()) {
e.u32(name.len() as u32);
e.u32(0);
e.u32(name.len() as u32);
for u in name {
e.u16(*u);
}
encode_sid(&mut e, sid);
}
e.u32(0);
let stub = e.into_bytes();
let page = decode_enumerate_trusted_domains(&stub).unwrap();
assert_eq!(page.trusts.len(), 2);
assert_eq!(page.trusts[0].name, "FOREST-A");
assert_eq!(page.trusts[0].sid.to_string(), "S-1-5-21-10-20-30");
assert_eq!(page.trusts[1].name, "FOREST-B");
assert_eq!(page.trusts[1].sid.to_string(), "S-1-5-21-40-50-60");
}
#[test]
fn hostile_entries_read_is_bounded_against_stub() {
let mut e = NdrEncoder::new();
e.u32(0); e.u32(u32::MAX); e.referent(); e.u32(u32::MAX); let stub = e.into_bytes();
let err = decode_enumerate_trusted_domains(&stub).unwrap_err();
let _ = err;
}
}