September 9, 2026HackTheBoxActive DirectoryWindows Server 2025 12 min read
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.
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.
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 distinguishedName — restoring it, attributes and all. bloodyAD's get writable shows alex.turner has exactly that right:
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:
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
DC01PS ...> 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:
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:
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:
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:
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:
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
Stage
Root cause
alex → mark
WRITE on CN=Deleted Objects lets a normal user reanimate a tombstoned account with its old password (CWE-284)
mark → ryan
Writable share auto-feeds "approved" .vsix extensions to an editor — writable code path = RCE (CWE-732 / CWE-94)
ryan → svc_deploy
BadSuccessor: create-child on an OU lets a dMSA claim succession to any account and inherit its keys (Server 2025 dMSA abuse)
svc_deploy → root
Domain-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)