pub const EDITF_ATTRIBUTESUBJECTALTNAME2: u32 = 0x0004_0000;
pub const IF_ENFORCEENCRYPTICERTREQUEST: u32 = 0x0000_0200;
pub const SZOID_NTDS_CA_SECURITY_EXT: &str = "1.3.6.1.4.1.311.25.2";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EscHit {
pub id: &'static str,
pub title: &'static str,
pub detail: String,
}
pub fn esc6(edit_flags: u32) -> Option<EscHit> {
(edit_flags & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0).then(|| EscHit {
id: "A-Esc6",
title: "ESC6: CA honors requester-supplied SAN (EDITF_ATTRIBUTESUBJECTALTNAME2)",
detail: format!(
"CA EditFlags = 0x{edit_flags:08x} has EDITF_ATTRIBUTESUBJECTALTNAME2 set — any \
enrollable template can be requested with an arbitrary SAN, so a low-priv user can \
obtain a cert as a Domain Admin. Remediation: clear the flag \
(`certutil -setreg policy\\EditFlags -EDITF_ATTRIBUTESUBJECTALTNAME2`) and restart the CA."
),
})
}
pub fn esc11(interface_flags: u32) -> Option<EscHit> {
(interface_flags & IF_ENFORCEENCRYPTICERTREQUEST == 0).then(|| EscHit {
id: "A-Esc11",
title: "ESC11: CA ICertRequest does not enforce RPC encryption",
detail: format!(
"CA InterfaceFlags = 0x{interface_flags:08x} lacks IF_ENFORCEENCRYPTICERTREQUEST — the \
ICPR endpoint accepts unencrypted requests, so a coerced machine's NTLM can be relayed \
to it for a cert. Remediation: set IF_ENFORCEENCRYPTICERTREQUEST and restart the CA."
),
})
}
pub fn esc16(disable_extension_list: &str) -> Option<EscHit> {
disable_extension_list
.lines()
.any(|l| l.trim() == SZOID_NTDS_CA_SECURITY_EXT)
.then(|| EscHit {
id: "A-Esc16",
title: "ESC16: CA globally disables the SID security extension",
detail: format!(
"CA DisableExtensionList contains {SZOID_NTDS_CA_SECURITY_EXT} — every issued cert \
omits the SID binding, enabling weak certificate mapping / impersonation. \
Remediation: remove the OID from DisableExtensionList and enforce strong mapping."
),
})
}
pub fn esc10(strong_binding: u32) -> Option<EscHit> {
(strong_binding < 2).then(|| EscHit {
id: "A-Esc10",
title: "ESC10: DC Kerberos certificate mapping not strongly enforced",
detail: format!(
"Kdc\\StrongCertificateBindingEnforcement = {strong_binding} ({}). A UPN/SAN cert can be \
mapped to a privileged account. Remediation: set it to 2 (Full) — KB5014754.",
match strong_binding {
0 => "Disabled",
1 => "Compatibility",
_ => "?",
}
),
})
}
pub fn esc10_absent() -> EscHit {
EscHit {
id: "A-Esc10",
title: "ESC10: DC StrongCertificateBindingEnforcement is not set (default is version-dependent)",
detail:
"Kdc\\StrongCertificateBindingEnforcement is absent. Its default is Compatibility \
(exploitable) on Server 2016–2022 prior to the Feb-2025 enforcement, and Full (safe) \
on Server 2025 / enforced builds. Verify the DC's patch level; set it explicitly to 2 \
(Full) — KB5014754."
.into(),
}
}
pub const CA_MANAGE_CA: u32 = 0x0000_0001;
pub const CA_MANAGE_CERTIFICATES: u32 = 0x0000_0002;
fn is_tier0(sid: &windows_sddl::Sid) -> bool {
let s = sid.to_string();
if s == "S-1-5-32-544" || s == "S-1-5-18" || s == "S-1-5-9" {
return true;
}
matches!(sid.rid(), Some(500 | 512 | 516 | 518 | 519))
&& sid.identifier_authority == 5
&& sid.sub_authorities.first() == Some(&21)
}
fn esc7_from_sd(sd: &windows_sddl::SecurityDescriptor) -> Vec<EscHit> {
let Some(dacl) = &sd.dacl else {
return Vec::new();
};
let mut hits = Vec::new();
for ace in &dacl.aces {
if !ace.is_allow() || is_tier0(&ace.trustee) {
continue;
}
let m = ace.mask.bits();
let ca = m & CA_MANAGE_CA != 0;
let certs = m & CA_MANAGE_CERTIFICATES != 0;
if !ca && !certs {
continue;
}
let right = match (ca, certs) {
(true, true) => "ManageCA + ManageCertificates",
(true, false) => "ManageCA",
_ => "ManageCertificates",
};
hits.push(EscHit {
id: "A-Esc7",
title: "ESC7: non-admin principal holds CA management rights",
detail: format!(
"{} is granted {right} on the CA. ManageCertificates lets it approve a pending \
request (issue a cert on any template → ESC1-style impersonation); ManageCA lets \
it set EDITF_ATTRIBUTESUBJECTALTNAME2 (→ ESC6) or add itself as an officer. \
Remediation: remove the ACE — restrict CA Administrators/Certificate Managers to Tier-0.",
ace.trustee
),
});
}
hits
}
pub fn esc7(sd_bytes: &[u8]) -> Vec<EscHit> {
match windows_sddl::parse(sd_bytes) {
Ok(sd) => esc7_from_sd(&sd),
Err(_) => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use windows_sddl::{Ace, AceType, AccessMask, Acl, SecurityDescriptor, Sid};
fn sd_with(trustee: &str, mask: u32) -> SecurityDescriptor {
SecurityDescriptor {
owner: None,
group: None,
dacl: Some(Acl {
aces: vec![Ace {
ace_type: AceType::AccessAllowed,
flags: 0,
mask: AccessMask::from_bits_truncate(mask),
trustee: Sid::parse(trustee).unwrap(),
object_type: None,
inherited_object_type: None,
}],
}),
}
}
#[test]
fn esc7_fires_for_nonadmin_manageca() {
let sd = sd_with("S-1-5-21-1-2-3-1105", CA_MANAGE_CA);
let h = esc7_from_sd(&sd);
assert_eq!(h.len(), 1);
assert_eq!(h[0].id, "A-Esc7");
assert!(h[0].detail.contains("ManageCA"));
}
#[test]
fn esc7_ignores_tier0_and_enroll_only() {
assert!(esc7_from_sd(&sd_with(
"S-1-5-21-1-2-3-512",
CA_MANAGE_CA | CA_MANAGE_CERTIFICATES
))
.is_empty());
assert!(esc7_from_sd(&sd_with("S-1-5-32-544", CA_MANAGE_CA)).is_empty());
assert!(esc7_from_sd(&sd_with("S-1-5-11", 0x200)).is_empty());
}
#[test]
fn esc6_fires_only_on_the_bit() {
assert!(esc6(0x0004_0000).is_some());
assert!(esc6(0x0014_0014).is_some()); assert!(esc6(0x0000_0000).is_none());
assert!(esc6(0x0002_0000).is_none()); }
#[test]
fn esc11_fires_when_encryption_not_enforced() {
assert!(esc11(0x0000_0000).is_some()); assert!(esc11(0x0000_0040).is_some()); assert!(esc11(0x0000_0200).is_none()); assert!(esc11(0x0000_0240).is_none());
}
#[test]
fn esc16_matches_the_security_ext_oid() {
assert!(esc16("1.3.6.1.4.1.311.25.2").is_some());
assert!(esc16("1.2.3.4\n1.3.6.1.4.1.311.25.2\n5.6.7").is_some());
assert!(esc16("1.2.3.4\n5.6.7").is_none());
assert!(esc16("").is_none());
}
#[test]
fn esc10_below_full_is_weak() {
assert!(esc10(0).is_some());
assert!(esc10(1).is_some());
assert!(esc10(2).is_none()); }
}