考点:SSTI(服务端模板注入),Flask SSTI 过滤绕过,Python 函数的 __globals__ 属性
打开靶机。

有点乱,不方便看,右键查看网页源代码。

因为代码里有flask 和jinja,所以很容易的就联想到了SSTI漏洞。
代码剖析:
划重点 :flag 就存在 app.config['FLAG'] 里,我们的目标就是拿到这个值。
python
app.config['FLAG'] = os.environ.pop('FLAG')
把输入字符串里的所有左括号 ( 和右括号 ) 都删掉,过滤括号,让我们不能调用函数。
python
s = s.replace('(', '').replace(')', '')
比如:
-
输入
{``{ config.__class__() }}→ 变成{``{ config.__class__ }}(括号没了,调用不了) -
输入
{``{ ''.__class__.__mro__[1].__subclasses__() }}→ 括号全没了,废了
设置黑名单,把 config 和 self 置为 None。
python
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
-
blacklist = ['config', 'self']------ 黑名单列表,包含config和self -
'{% set {}=None%}'.format(c)------ 对每个黑名单项,生成{% set config=None %}这样的语句 -
''.join([...])------ 把所有语句拼起来 -
+ s------ 拼到我们输入的内容前面
比如,在我们的模板内容前面,会自动加上:
python
{% set config=None %}
{% set self=None %}
这两行的意思是:把 config 变量和 self 变量设置为 None(空值)。
作用:
-
我们不能直接用
{``{ config }}拿配置了,因为config被设成了None -
我们不能用
{``{ self.__dict__ }}之类的,因为self也被设成了None
把经过 safe_jinja 处理后的字符串,当作 Jinja2 模板来渲染。
python
return flask.render_template_string(safe_jinja(shrine))
-
flask.render_template_string()------ Flask 提供的函数,把字符串当作模板渲染 -
safe_jinja(shrine)------ 先把我们的输入过滤一遍 -
然后把过滤后的内容渲染成模板
这就是漏洞点 :我们输入的 shrine 参数,最终被当作模板渲染了,虽然有过滤,但过滤不严格,可以绕过。
先搞懂所有前置概念
什么是模板引擎?
模板引擎就是一个"填空游戏"的工具。你写一个带"空位"的HTML模板,模板引擎把数据填进去,生成最终的网页。
举个例子:
html
<!-- 模板文件 -->
<h1>你好,{{ name }}!</h1>
<p>你的年龄是:{{ age }}</p>
python
# Python代码
render_template("模板.html", name="小明", age=18)
最终生成的网页:
html
<h1>你好,小明!</h1>
<p>你的年龄是:18</p>
{``{ name }} 和 {``{ age }} 就是"空位",模板引擎会把它们替换成真实的数据。
什么是 Jinja2?
Jinja2 是 Python 里最流行的模板引擎,Flask 框架默认用的就是它。
Jinja2 的语法特点:
-
{``{ 变量名 }}------ 输出变量的值 -
{% 语句 %}------ 执行控制语句(比如循环、判断) -
{# 注释 #}------ 模板里的注释
重点 :{``{ }} 里面不仅可以放变量名,还可以放表达式,甚至可以调用函数、访问属性。
比如:
html
{{ 1 + 1 }} <!-- 输出 2 -->
{{ "hello".upper() }} <!-- 输出 HELLO -->
{{ [1,2,3][0] }} <!-- 输出 1 -->
这就是 SSTI 漏洞的基础------如果用户能控制模板内容,就能在 {``{ }} 里写任意代码。
什么是 SSTI?
SSTI = Server-Side Template Injection = 服务端模板注入
原理 :用户输入的内容被当作模板代码执行了,而不是被当作普通字符串显示。
对比一下:
| 正常情况 | 有SSTI漏洞 |
|---|---|
用户输入 {``{7*7}} |
用户输入 {``{7*7}} |
页面显示:{``{7*7}}(原样显示) |
页面显示:49(被执行了) |
怎么判断有没有SSTI?
在输入框或URL参数里输 {``{7*7}},如果页面显示 49,说明被执行了,存在SSTI。
Flask 是什么?
Flask 是 Python 的一个轻量级 Web 框架,用来写网站的。
这道题就是用 Flask 写的。
Flask 和 Jinja2 的关系:
-
Flask 负责处理 HTTP 请求、路由等
-
Jinja2 负责渲染 HTML 模板
-
它们是黄金搭档,Flask 默认集成了 Jinja2
Python 的 __globals__ 是什么?
Python 里每个函数都有一个 __globals__ 属性,它是一个字典,存着这个函数所在模块的所有全局变量。
python
# 这是一个模块文件:test.py
x = 100
y = "hello"
def my_func():
pass
print(my_func.__globals__)
运行结果:
python
{
'x': 100,
'y': 'hello',
'my_func': <function my_func 0x...>,
'__name__': '__main__',
... 其他内置变量
}
可以看到,my_func.__globals__ 里包含了这个模块里所有的全局变量:x、y、my_func 自己,等等。
这很重要 ,因为如果我们能拿到一个函数的 __globals__,就能拿到这个函数所在模块的所有全局变量。
在这道题里,url_for 是 Flask 里的函数,它的 __globals__ 里就有 Flask 应用的各种全局变量,包括 current_app。
Flask 的内置全局变量/函数
在 Jinja2 模板里,Flask 默认提供了一些可以直接用的全局变量和函数:
| 名字 | 类型 | 作用 |
|---|---|---|
config |
变量 | Flask应用的配置对象,app.config |
request |
变量 | 当前请求对象 |
session |
变量 | 当前会话对象 |
url_for() |
函数 | 根据路由函数名生成URL |
get_flashed_messages() |
函数 | 获取闪现消息 |
注意:
-
config、request、session是变量(对象) -
url_for、get_flashed_messages是函数
这道题过滤了 config 和 self,但没过滤 url_for 和 get_flashed_messages。
步骤分析:
我们的目标是拿到 app.config['FLAG']。
也就是要找到一个方法,在模板里访问到 Flask 应用对象 app,然后访问它的 config 属性。
因为config和self被设为空,括号不给用,所以我们需要找一个模板里能用的、不需要括号的东西 ,通过它间接拿到 app。
在Jinja2 模板里,url_for是一个函数,函数有什么特殊属性?------ __globals__,前面讲过,每个函数都有 __globals__ 属性,里面是函数所在模块的所有全局变量。
url_for 是 Flask 里的函数,它定义在 Flask 的模块里,那它的 __globals__ 里应该有 Flask 的各种全局变量,包括 current_app(当前应用对象)。
思路:
url_for(模板里能用的函数)
↓
.__globals__(函数的全局变量字典)
↓
['current_app'](当前Flask应用对象)
↓
.config(应用配置,里面有FLAG)
这条链路上只需要属性访问和字典访问,都没被过滤。
验证思路:
先用一个7+7测试一下。
/shrine/{{7+7}}

/shrine/{{ url_for.__globals__ }}

确认是应用对象:
/shrine/{{url_for.__globals__['current_app']}}

/shrine/{{url_for.__globals__['current_app'].config}}

最后再分析一下这个payload。
{{ url_for.__globals__ ['current_app'] .config }}
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
从 __globals__ 字典里取出键为 current_app 的值。
第一层:{``{ ... }}
{{ url_for.__globals__['current_app'].config }}
↑↑ ↑↑
{``{ }} 是 Jinja2 的输出语法,意思是"把里面表达式的结果输出到页面上"。
第二层:url_for
{{ url_for .__globals__['current_app'].config }}
↑↑↑↑↑↑↑
url_for 是 Flask 在模板里提供的全局函数,用来根据路由名生成 URL。
它本身是一个函数对象,我们这里不调用它(因为括号被过滤了,也不需要调用),只是把它当作一个普通对象来访问它的属性。
第三层:.__globals__
{{ url_for .__globals__ ['current_app'].config }}
↑↑↑↑↑↑↑↑↑↑↑
__globals__ 是 Python 函数的一个特殊属性,它是一个字典,包含了函数定义所在模块的所有全局变量。
因为 url_for 是 Flask 模块里的函数,所以 url_for.__globals__ 里有 Flask 模块的所有全局变量。
第四层:['current_app']
{{ url_for.__globals__ ['current_app'] .config }}
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
从 __globals__ 字典里取出键为 current_app 的值。
current_app 是什么?
-
它是 Flask 的一个"代理对象",代表当前正在处理请求的 Flask 应用
-
简单理解:
current_app就约等于app(我们在源码里创建的那个 Flask 应用对象)
为什么不直接用 app?
-
因为
app可能不在__globals__里,或者名字不一样 -
但
current_app是 Flask 的标准全局变量,一定在
第五层:.config
{{ url_for.__globals__['current_app'] .config }}
↑↑↑↑↑↑↑
访问 current_app 的 config 属性。
config 是 Flask 应用的配置对象,本质上是一个字典。
我们的 flag 就存在 app.config['FLAG'] 里,也就是 current_app.config['FLAG'] 里。
直接访问 .config 会把整个配置字典都显示出来,我们就能在里面找到 FLAG 的值。
===END===