说明
按照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
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.php
中populateCompiledFilepath()
中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public 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
3public 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 |
|
页面也显示了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整个执行编译过程非常的复杂,在加上第一次分析这种编译类型的漏洞,所以导致我整个分析过程都十分的缓慢和痛苦。但是总体来说,这个漏洞还是比较有趣的。