原先学过python的模板注入,python的模板比较简单,做题的时候遇到了php的模板注入,看不太懂代码审计确实挺难的
看了一篇文章写的挺好的,现在重新学习一下ssti模板注入。
Tryhackme之模板注入
Introduction
服务器端模板注入(SSTI)是当用户输入被注入到应用程序的模板引擎时发生的漏洞。这可能会导致一系列安全问题,包括代码执行、数据泄露、权限升级和拒绝服务 (DoS)。 SSTI 漏洞经常出现在使用模板引擎生成动态内容的 Web 应用程序中,如果不加以解决,可能会造成严重后果。
SSTI Overview
服务器端模板注入 (SSTI) 是当用户输入不安全地合并到服务器端模板时发生的漏洞,允许攻击者在服务器上注入并执行任意代码。模板引擎通常用于 Web 应用程序中,通过将固定模板与动态数据相结合来生成动态 HTML。当这些引擎在没有适当清理的情况下处理用户输入时,它们很容易受到 SSTI 攻击。
Core Concepts of SSTI
动态内容生成:模板引擎用实际数据替换占位符,允许应用程序生成动态 HTML 页面。如果用户输入没有得到适当的净化,这个过程就可能被利用。
用户输入作为模板代码:当用户输入被视为模板代码的一部分时,它们可能会将有害逻辑引入到渲染的输出中,从而导致 SSTI。
SSTI 的核心在于服务器端模板中对用户输入的不当处理。模板引擎解释并执行嵌入的表达式以生成动态内容。如果攻击者可以将恶意负载注入这些表达式中,他们就可以操纵服务器端逻辑并可能执行任意代码。
当用户输入未经适当验证或转义而直接嵌入模板中时,攻击者可以制作改变模板行为的有效负载。这可能会导致各种意外的服务器端操作,包括:
读取或修改服务器端文件。
执行系统命令。
访问敏感信息(例如环境变量、数据库凭据)。
Template Engin
Template Engin
模板引擎就像一台帮助动态构建网页的机器。简单来说它的工作原理如下:
想象一下我们正在为朋友制作一张生日贺卡。我们想要包含他们的姓名、年龄和个性化消息。我们无需从头开始编写新卡片,而是使用带有姓名、年龄和消息占位符的模板。
模板引擎的工作原理类似:
模板:引擎使用预先设计的模板,其中包含动态内容的占位符(例如 {{ name }})。
用户输入:引擎接收用户输入(如姓名、年龄或消息)并将其存储在变量中。
组合:引擎将模板与用户输入组合,用实际数据替换占位符。
输出:引擎生成最终的动态网页,并将用户的输入插入到模板中。
Common Template Engines
模板引擎是现代 Web 开发不可或缺的一部分,允许开发人员通过将模板与数据相结合来生成动态 HTML 内容。以下是一些最常用的模板引擎:
Jinja2:在Python应用程序中非常流行,以其表现力和强大的渲染能力而闻名。
Twig:Twig 是 PHP 中 Symfony 的默认模板引擎,它提供了具有安全默认设置的强大环境。
Pug/Jade:Pug/Jade 以其简洁、简洁的 HTML 模板语法而闻名,在 Node.js 开发人员中很受欢迎。
How Template Engines Parse and Process Inputs
模板引擎通过解析模板文件来工作,其中包含与动态内容的特殊语法混合的静态内容。渲染模板时,引擎会用运行时提供的实际数据替换动态部分。例如:
from jinja2 import Template
hello_template = Template("Hello, {{ name }}!")
output = hello_template.render(name="World")
print(output)
在此示例中, {{ name }}
是一个占位符,在渲染期间将替换为值 "World"
。
Determining the Template Engine
不同的模板引擎具有不同的语法和功能,使得它们以各种方式容易受到 SSTI 的影响。以下是易受攻击的模板语法的一些示例:
Jinja2 和 Twig 在语法和行为上相似,这使得仅通过有效负载响应来区分它们有些困难。但是,我们可以通过测试它们的表达式处理能力来检测它们的存在。例如,使用易受攻击的虚拟机,如果我们在 Twig 中使用负载 {{7*'7'}},则输出将为 49。
但是,如果我们在使用 Jinja2 的应用程序中使用相同的有效负载,则输出将为 7777777
Pug,以前称为 Jade,使用不同的语法来处理表达式,可以利用它来识别其用法。 Pug/Jade 计算 #{}
内的 JavaScript 表达式。例如,使用有效负载 #{7*7} 将返回 49。
PHP-Smarty
Smarty 是一个强大的 PHP 模板引擎,使开发人员能够将表示与业务逻辑分离,从而提高应用程序的可维护性和可扩展性。然而,如果没有安全配置,它在模板中执行 PHP 函数的能力可能会使应用程序遭受服务器端模板注入攻击。
Smarty 的灵活性允许在其模板内动态执行 PHP 函数,这可能会成为重大的安全风险。应仔细控制通过模板变量或修饰符执行 PHP 代码的能力,以防止未经授权的命令执行。
NodeJS-Pug
Pug(以前称为 Jade)是一个高性能模板引擎,因其简洁的 HTML 渲染和条件、迭代和模板继承等高级功能而在 Node.js 社区中广泛使用。虽然 Pug 为开发人员提供了强大的工具,但其直接在模板中执行 JavaScript 代码的能力可能会带来重大的安全风险。
Pug 的安全漏洞主要源于其在模板变量中插入 JavaScript 代码的能力。此功能专为动态内容生成而设计,如果用户输入未经适当处理就嵌入到模板中,则可能会被恶意利用。
主要漏洞点:
JavaScript 插值:Pug 允许使用插值大括号
#{}
将 JavaScript 直接嵌入到模板中。如果在没有适当清理的情况下对用户输入进行插值,则可能会导致任意代码执行。默认转义:Pug 确实为某些输入提供自动转义,将
<
、>
和&
等字符转换为其等效的 HTML 实体,以防止 XSS 攻击。但是,这种默认行为并不能涵盖所有潜在的安全问题,特别是在处理未转义插值!{}
或复杂输入场景时。
注入基本的 Pug 语法来测试模板处理,例如 #{7*7}
。如果应用程序输出 49,则确认 Pug 正在处理模板。
由于 Pug 允许 JavaScript 插值,因此我们可以使用有效负载 #{root.process.mainModule.require('child_process').spawnSync('ls').stdout}
上述有效负载使用 Node.js 的核心模块来执行系统命令。
root.process
从 Pug 模板内的 Node.js 访问全局process
对象。mainModule.require('child_process')
动态地需要child_process
模块,绕过可能阻止其常规包含的潜在限制。spawnSync('ls')
:同步执行ls
命令。.stdout
:捕获命令的标准输出,其中包括目录列表。
正确使用spawnSync
const { spawnSync } = require('child_process');
const result = spawnSync('ls', ['-lah']);
console.log(result.stdout.toString());
后面的就不写了
这里补充一下大佬的博客
SSTI(模板注入)漏洞(cms实例篇)
对于这个cms框架我不是太熟悉所以审代码的时候会有很多地方不熟悉
对于一些框架的审计,看着复现还是有很多不会的东西
苹果CMS模板注入导致代码执行
进去之后全局搜索看看有没有什么命令执行函数
点进去看一下
这里有一个参数,我们看一下是怎么来的
通过这个this->H传入到下面正在匹配到iar数组,最后通过这个二维数组传给strif
我们看一下这个ifex方法的调用
我们在index.php里面看到了这个方法的调用,我们看一下上面的逻辑
上面有一个be方法
我们跟进去看一下
这个m这个参数是由get传参进去的,也就是说这个参数我们是可控的
后面就是以-符号作为分隔符,传入par数组
将这个ac赋值为par[0]
把par[2]赋值为method
用一个include来包含我们传入的php文件。我们传入的文件要在这个acs的数组里面
根据payload,这个传入的函数就是vod.php
然后把这个函数里面加一个键值对,键名为module,,值为这个ac的名字
最后包含这个php文件
我们看一下这个vod.php
里面全是一些方法之类的
这个在index.php里面我们也出现过这个method这个属性
也是我们可控的
根据payload我们传入的就是search
我们看一下这个search
这里面也有个be方法,同样可以传值,这个里面写的是all,也就是说我们可以POST传参也可以GET传参
然后把这wd参数进行检查,通过chkSql这个方法,我们进去看看看它会检查什么
里面还包裹着一个参数
这个就是对字符的一些转译
这里由H属性,导入了一个html页面,然后将colarr数组里面的值替换为vallarr里面的值,当然也有我们传入的wd
那我们这个H属性有了,我们接下来看看下面的,继续回到index.php
后来就是调用这个函数,跟进一下
这个H就是刚刚的那个H,因为这个tql就是我们new的一个新对象
最后就到这个函数的执行了
$labelRule = buildregx('{if-([\s\S]*?):([\s\S]+?)}([\s\S]*?){endif-\1}',"is");
这个东西就是差不多是 {if-A:"{page:typepid}"="0"}字段,一次一次的通过for循环来写入最后eval执行
所以类似这样就可以
{if-dddd:phpinfo()}{endif-dddd}
我们看一下经过处理后的这个数组
我们利用的是最后一个eval
本地环境没有搭好,回头看看吧
OFCMS模板注入导致任意命令执行(JAVA未复现)
PbootCms-2.07模板注入导致Getshell
下载到源码进入到源码,进入到View.php
这里有个include,可以产生文件包含,我们看一下这变量是怎么来的
最后通过这个tpl_c_file来传出去
看一下中间的对file的处理
先正则匹配过滤掉相对路径
如果我们传入 ../ 就把这个替换为空
这里我们的if有两层判断,判断这个是否以/开头,第二层看这个是不是包含@
最后的else就是把模板路径加上/之后加上我们的file
组成我们的tql_file
后来就是判断我们的文件是否存在看tql_c_file的创建时间是否小于tql_file的创建时间,最后就是判断我们的配置文件是不是没有读取成功
这三个判断全是false,下面的判断也是一样的
这里我们直接进入文件包含
在此前我们的tql_c_flie
是被编译过的,看大佬的wp说的是这个漏洞产生的根本原因是编译文件造成的任意文件读取,这点不太懂
最后我们就是找一下在哪传参
这四个地方调用这个函数
这个是display,加载模板就会调用这个方法,感觉利用不大
这个是 解析模板
下面的这个两个调用了这个函数
我们都看一下这两个函数
这里我们可以利用这个传参在这个页面
这里的url请求直接是构造器的这个
前面的参数
上面的绕过就用双写绕过就行
因为本地环境搭不起来,给个payload
GET 127.0.0.1/index.php/Search
POST searchtql=..././..././robot.txt
同样构造利用的还有 TagController.php 文件
和上面的一样
74cms模板注入导致Getshell
这个是个文件包含,我们进去先找include函数
在View.class.php这个文件中,找到了include
我们来分析一下这个是怎么包含的
看看parseTemplate是干什么的
判断是不是文件,如果是直接返回
后面说用php原生模板,看一下开发手册原生模板是什么
原生模板尽量使用<php></php>php标签这种形式,然后文件包含
我们看一下这个fetch函数从哪里调用
这个display调用了这个函数,display应该就是展示模板
最后追溯的这个display,这个在MController.class.php 文件中就可以看到 display() 函数的调用
着里面就是传入的类型
这个tpl就是我们传入的type,最后调用了这个display。
这个type为get.type,这个是前端获取的值,这个值就是我们可控的
我们试一下
http://127.0.0.1/74cms_v4.1.5/upload/index.php?m=&c=M&a=index&page_seo=1&type=../favicon.ico
成功包含
最后我们想要getshell,就找到上传路径,上传一个恶意文件,最后包含这个恶意文件就行了
ZZZ_CMS
这个题很早就看了,但是没有看的太明白,复现完上面的之后,觉得也没有那么难了
首先我们全局搜索危险函数
这里面有个eval函数,这个和第一个苹果CMS的很像
我们看一下这个参数是怎么传递的
首先就是通过zcontent传入经过正则匹配到matches数组
这个正则匹配的格式要满足
{if:}{end if}
接着就将这个matches[1][i],传入ifstr,然后我们要进入到这个if判断中,将数组中的字符串赋值个arr1,arr2,arr0
所以我们必须要进入这个if判断,想要进入这个if判断我们就要有那个arr数组里面的东西,我们往上看,这里有一个对ifstr的处理
传入=替换成==,这样下面的array里面也有这个==,所以我们传入=就可以了,当然传入其他的字符也行
接着进入到if判断之后,下面有个字符串分隔的操作
我们跟进这个函数
判断一下这个==是否在传入的字符串中,如果在,就以他为分隔服进行分隔
后面就是过滤一下我们传入的这个字符串
最后拼接进行命令执行
上面的分析完之后,就看一下我们怎么控制参数
我们跟进一下这个函数
我们发现只有经过上面的这些函数的处理才进入到这个函数,我们全局搜索一下这个参数,看看有没有什么可以传参的地方
我们在这里找到了这个替换函数将keywords替换为这个参数
我们追溯一下这个keywords
我们发现这个可以通过cookie传入,参数是keys,这样我们就控制参数值了,而且下面还没有什么过滤
我们看一下,这函数怎么传递
这里调用了这个函数
是在这里,调用了这个G函数
跟进去看一下
这个获取全局变量的意思
传入的参数就是location
这里我们就传入的是search
所以payload就是
GET ?location=search
Cookie keys={if:=`calc`}{end if}
传递的时候过滤了这些
function danger_key($s,$type=0) {
if($type==1){
$s= htmlspecialchars($s);
$s =preg_replace('/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]/','',$s);
$s =preg_replace("/&(?!(#[0-9]+|[a-z]+);)/si",'&',$s);
$s =str_replace( array("php","\0", "%00", "\r","<", ">","'", '"', "{","}", "%3"), '', $s);
}
$str= $s;
$danger=array('preg','server','chr','decode','md5','post','get','request','file','cookie','session','sql','mkdir','copy','fwrite','del','encrypt','$','system','exec','shell','open','ini_','chroot','eval','passthru','include','require','assert','union','create','func','symlink','sleep','ascii','print','echo','base_','replace','_map','_dump','_array','regexp','select','dbpre','zzz_','{if','curl','certutil');
foreach ($danger as $val){
if(strpos($str,$val) !==false){
error('很抱歉,执行出错,系统限制使用【'.$val.'】,请点击返回重新操作,如此问题为误报,请联系管理员');
}
}
return $s;
}
总结
看着大佬的wp复现还是有很多不明白的地方,但是也学到了很多,大多数步骤就是,先找漏洞函数,最后在找到参数传递的函数,这个参数一定是可控的。