the_open_door
café
ESEN
← Volver al índice

Python eval() Injection

Arsenal completo de comandos en la cheatsheet

eval() en Python toma una cadena y la evalúa como una expresión Python, devolviendo su resultado. Es cómodo para "calcular" algo dinámico, pero si la cadena contiene datos controlados por un usuario, eval() ejecuta lo que el usuario quiera: es ejecución de código arbitrario.

El error de diseño. El programador suele pensar que eval("109+50") solo hace aritmética. Pero la cadena puede ser cualquier expresión válida, incluida una que importe módulos y lance comandos del sistema:

eval("__import__('os').system('id')")

__import__('os') carga el módulo os dentro de la propia expresión (no hace falta un import previo), y .system(...) ejecuta un comando del shell. No hay límite real: lo que se evalúe corre con los privilegios del proceso.

Bypass de sandbox. Si filtran import, os o eval como palabras, se llega a los mismos módulos por la jerarquía de clases o por builtins alternativos:

# cadena clasica: subir a object y recorrer subclases hasta un Popen/os
().__class__.__bases__[0].__subclasses__()
# ejecutar sin escribir "import" (via builtins)
[].__class__.__mro__[1].__subclasses__()[<idx>]('id',shell=True,stdout=-1).communicate()
().__class__.__base__.__subclasses__()[<idx>].__init__.__globals__['system']('id')
# builtins ofuscados
getattr(__builtins__,'__imp'+'ort__')('os').system('id')

Por qué es escalada de privilegios. El patrón peligroso es un script que corre como root (vía sudo, un cron, un setuid) y que en algún punto pasa entrada del usuario por eval(). En BountyHunter, un validador de tickets ejecutable por sudo evaluaba con eval() la línea del "código del ticket". Bastó con un ticket que cumpliera las comprobaciones de formato y colara la carga:

**109+__import__('os').system('chmod u+s /bin/bash')**

El 109 superaba el filtro aritmético del script (% 7 == 4), y el resto de la expresión ponía el bit SUID a /bin/bash. Después, bash -p conserva el EUID de root y da shell de superusuario. Es primo del abuso de SUID: una función legítima (validar) convertida en ejecución.

No solo escalada: también punto de entrada. El mismo patrón aparece en aplicaciones web. Si un endpoint evalúa un parámetro del usuario (por ejemplo, una API REST que «valida» un código con eval()), la inyección da RCE directo como el usuario del servicio, sin necesidad de credenciales. En Hetemit, la ruta /verify de una API en Flask evaluaba el parámetro code: 7*7 devolvía 49 (mientras que {{7*7}} fallaba, lo que descartaba una plantilla Jinja) y os.system('...') ejecutaba comandos. Esa diferencia es la pista clave: confirma un eval() puro y no una inyección de plantillas, que se exploraría con otra carga.

Funciones igual de peligrosas. exec() (ejecuta sentencias, no solo expresiones), eval/exec en plantillas, pickle.loads sobre datos no confiables, yaml.load sin SafeLoader, y os.system/subprocess con shell=True e interpolación de entrada.

Cómo detectarlo. En una web, la firma en el log de acceso son parámetros con expresiones Python delatoras: __import__, os.system, subprocess, ().__class__, o el sondeo 7*7/{{7*7}}; un WAF que inspeccione esos patrones lo marca. La confirmación es el proceso Python (Flask/gunicorn, o el script bajo sudo) lanzando sh/bash/id como hijo (auditd/Sysmon for Linux sobre execve con ppid del intérprete), algo que evaluar una expresión aritmética nunca provoca. En el caso de escalada, un sudo al script validador en /var/log/auth.log seguido de un bash con bit SUID cierra la cadena.

Defensa. Nunca pasar datos externos a eval()/exec(). Para evaluar literales o aritmética acotada, usar ast.literal_eval (solo literales) o un parser aritmético explícito que rechace nombres y llamadas. Y, en sudo, no conceder NOPASSWD sobre scripts que procesen entrada del usuario.

Máquinas donde se aplica