原先学过python的模板注入,python的模板比较简单,做题的时候遇到了php的模板注入,看不太懂代码审计确实挺难的

看了一篇文章写的挺好的,现在重新学习一下ssti模板注入。

Tryhackme之模板注入

Introduction

服务器端模板注入(SSTI)是当用户输入被注入到应用程序的模板引擎时发生的漏洞。这可能会导致一系列安全问题,包括代码执行、数据泄露、权限升级和拒绝服务 (DoS)。 SSTI 漏洞经常出现在使用模板引擎生成动态内容的 Web 应用程序中,如果不加以解决,可能会造成严重后果。

SSTI Overview

服务器端模板注入 (SSTI) 是当用户输入不安全地合并到服务器端模板时发生的漏洞,允许攻击者在服务器上注入并执行任意代码。模板引擎通常用于 Web 应用程序中,通过将固定模板与动态数据相结合来生成动态 HTML。当这些引擎在没有适当清理的情况下处理用户输入时,它们很容易受到 SSTI 攻击。

Core Concepts of SSTI

  • 动态内容生成:模板引擎用实际数据替换占位符,允许应用程序生成动态 HTML 页面。如果用户输入没有得到适当的净化,这个过程就可能被利用。

  • 用户输入作为模板代码:当用户输入被视为模板代码的一部分时,它们可能会将有害逻辑引入到渲染的输出中,从而导致 SSTI。

SSTI 的核心在于服务器端模板中对用户输入的不当处理。模板引擎解释并执行嵌入的表达式以生成动态内容。如果攻击者可以将恶意负载注入这些表达式中,他们就可以操纵服务器端逻辑并可能执行任意代码。

Flow of an SSTI attack

当用户输入未经适当验证或转义而直接嵌入模板中时,攻击者可以制作改变模板行为的有效负载。这可能会导致各种意外的服务器端操作,包括:

  • 读取或修改服务器端文件。

  • 执行系统命令。

  • 访问敏感信息(例如环境变量、数据库凭据)。

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复现还是有很多不明白的地方,但是也学到了很多,大多数步骤就是,先找漏洞函数,最后在找到参数传递的函数,这个参数一定是可控的。