LLM 生成的『数学表达式』直接进 sympy.sympify,一句 introspection 就开 shell
- 主页:https://nvd.nist.gov/vuln/detail/CVE-2024-46946
- 从哪读起:先看 12end 的 PoC gist(gist.github.com/12end/68c0c58d…),一行载荷就是整个洞;再回头看 NVD 的 CVSS 9.8 三个 H 从哪来。
从『解方程』到 os.system("id"):一条表达式的旅程
先把受害对象说清楚:没有一家具名公司被公开证实被此洞打穿。这是一个库漏洞——langchain_experimental 0.1.17 到 0.3.0(NVD 记录)里的 LLMSymbolicMathChain 组件,加上一条可复现的 PoC,报在 GitHub gist(作者 handle 12end,是否即 CVE 报告人未独立核实)。因为 LangChain 是被大量部署的 agent 框架,这条链条本身值得看,不是因为某个新闻事件。
攻击链,一步步落地。LLMSymbolicMathChain 的设计意图:用户问一道数学题(『解 x²−4=0』),Chain 用一段 prompt 让 LLM 把题目改写成一段 SymPy 能吃的『表达式』文本,然后把这段文本交给 sympy.sympify(expression, evaluate=True) 去求值。问题在最后一步——sympify 不是计算器,它内部走 Python 的 eval()。也就是说,模型吐出来的那段字符串会被当成 Python 代码跑。
于是攻击者不问数学题,问的是让模型生成这段(PoC 原文逐字引,未改写):
sympy.sympify("this.__class__.__mro__[8].__subclasses__()[154].__init__.__globals__['__builtins__']['exec']('import os;os.system(\"id\")')")
拆开看每个 __ 在干什么:this.__class__.__mro__[8] 从当前对象爬到 object(方法解析顺序里的基类);.__subclasses__() 列出 object 的所有子类——运行时里成百上千个类;[154] 挑中其中某个能摸到 __globals__ 的类;顺着 .__init__.__globals__['__builtins__']['exec'] 就拿到了 exec,最后 exec('import os;os.system("id")') 落地成命令执行。注意 [154] 这个索引是环境相关的——不同 Python 版本、不同已导入模块下,子类列表顺序会变,154 未必指向同一个类;它是一个具体环境下的示例,不是通用魔数。
三道你以为存在的门,一道都没关
第一道『它只是个数学库』。 直觉上 SymPy 是符号计算,处理的是 sin(x),能出什么事?但 sympify 的职责是把任意字符串解析成 SymPy 对象,为此它调 eval。它从设计上就没有『只接受数学』这条边界——喂给它 __import__ 风格的代码,它照跑。这对应 CVSS 向量里的 Attack Vector: Network、无需认证、无需交互。
第二道『输出会被校验』。 Chain 默认信任 LLM 返回的是一段合法数学表达式,不做语法检查、不做符号白名单。模型被 prompt 里的题目牵着走,你把恶意题目塞进去,它就把恶意表达式吐出来,Chain 原样转交 sink。中间没有任何一层问一句『这串里怎么有 __subclasses__』。
第三道『沙箱』。 默认在宿主进程里直接执行,没有隔离、没有降权。os.system("id") 拿到的就是跑 LangChain 那个进程的权限。这就是 CVSS 9.8 里 C/I/A 三个 High 的来源:机密性、完整性、可用性一次全丢。本地语料 6710 篇里只有 1 篇提到这条 CVE——学术侧关注度很低,但复现门槛几乎为零。
厂商补在出口:一个默认关掉的危险开关 + 32 个符号的白名单
修复改的是执行 sink 这一层。后续版本给 LLMSymbolicMathChain 加了 allow_dangerous_requests,而且构造时不显式传就直接报错:
if "allow_dangerous_requests" not in kwargs:
raise ValueError("LLMSymbolicMathChain requires allow_dangerous_requests to be set...")
默认走安全分支:不再用 sympify,改用 sympy.parse_expr() 配一个 local_dict 白名单——只放约 32 个符号(sin/cos/tan、sinh/cosh、exp/log/ln、pi/e/I/oo、factorial/gcd/sqrt 等)。表达式里出现白名单外的名字就解析不通。危险分支(str(sympy.sympify(expression, evaluate=True)))只在你手动传 allow_dangerous_requests=True 时才走,源码里配了一句逐字的警告:
You should absolutely NOT be deploying this in a production environment with
allow_dangerous_requests=True. As this would allow a malicious actor to execute
arbitrary code on your system.
注意 GitHub Advisory(GHSA-p2qj-r53j-h3xj)一度显示 no patched version——修复是以行为开关的形式落进后续版本的,不是一个干净的『升到 x.y.z 就好』。具体 fix 版本号与 advisory 的对应关系我没有逐一核对,这里只说修复形态。
入口没修,也修不了
这是整张卡最该记住的一句:白名单挡住的是代码落地,不是模型被说服。
allow_dangerous_requests 和 local_dict 都长在 sink 上——它们限制模型的输出能触发什么,不阻止模型生成那段 __subclasses__ 载荷。『用一道数学题诱导 LLM 吐出 introspection payload』这件事,从头到尾不在这个 CVE 的修复范围里,也不该指望一个库补丁解决——那是模型侧的可说服性问题。把开关拨到 allow_dangerous_requests=True,就完全退回打穿前的状态,一行都没变。
同类 sink 是一个模式,不是孤例:PALChain(CVE-2023-44467,同样 langchain_experimental 任意代码执行)让 LLM 生成 Python 程序再 exec,LLMMathChain 早期把表达式喂 eval。凡是『LLM 输出 → 代码执行』的链条,能设防的点只有 sink:给执行口上白名单、上沙箱、默认关掉。指望在入口拦住『模型会不会被那段文字说服』,目前没有一个库补丁做得到——出口治理和入口治理是两件事,这个 CVE 只碰了前一件。
已核实来源
- https://nvd.nist.gov/vuln/detail/CVE-2024-46946
- https://github.com/advisories/GHSA-p2qj-r53j-h3xj
- https://gist.github.com/12end/68c0c58d2564ef4141bccd4651480820
- https://github.com/langchain-ai/langchain-experimental/blob/main/libs/experimental/langchain_experimental/llm_symbolic_math/base.py
- https://vulert.com/vuln-db/CVE-2024-46946
- https://vulert.com/vuln-db/pypi-langchain-experimental-91723
本文由自动化管道生成(采集 → 逐字核验 → 模型撰写),未经人工改写。