前言
环境创建:
composer create-project topthink/think=5.0.10 thinkphp5.0.10
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.10"
},
composer update
影响版本:3.2.3-5.0.10
index控制器里写个cache()函数:
public function cache()
{
Cache::set("name",input("get.username"));
return 'Cache success';
}
在复现的利用过程中我这里不能直接访问cache目录,因此写入了恶意代码但是访问不到php。但是在真实环境中,服务器如果使用了缓存而且cache目录可以直接访问的话,就可以写入恶意代码,然后直接访问利用。
分析
payload:
?username=%0d%0aphpinfo();//
跟进一下set()方法:
先跟进init()方法,看看初始化。
cache
的配置在config.php中,cache.type这里是File,$connect
相当于获得了config.php中关于缓存的配置,然后返回。
init()之后,跟进self::init()->set()
方法:
/**
* 写入缓存
* @access public
* @param string $name 缓存变量名
* @param mixed $value 存储数据
* @param int $expire 有效时间 0为永久
* @return boolean
*/
public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$filename = $this->getCacheKey($name);
if ($this->tag && !is_file($filename)) {
$first = true;
}
$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;
} else {
return false;
}
}
文件名是由getCacheKey()方法得到的,跟进看一下:
/**
* 取得变量的存储文件名
* @access protected
* @param string $name 缓存变量名
* @return string
*/
protected function getCacheKey($name)
{
$name = md5($name);
if ($this->options['cache_subdir']) {
// 使用子目录
$name = substr($name, 0, 2) . DS . substr($name, 2);
}
if ($this->options['prefix']) {
$name = $this->options['prefix'] . DS . $name;
}
$filename = $this->options['path'] . $name . '.php';
$dir = dirname($filename);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
return $filename;
}
先对name进行md5,默认cache_subdir
是true,所以会使用子目录。这里$name
是"name"
,md5后在substr进行拼接,得到这个:
b0\68931cc450442b63f5b3d276ea4297
之后再加上路径,最终缓存文件路径是这样:
D:\phpstudy_pro\WWW\thinkphp5.0.10\runtime\cache\b0\68931cc450442b63f5b3d276ea4297.php
然后对我们get传入的cache值进行序列化:$data = serialize($value);
,接着就是最关键的两行代码:
$data = "<?php\n//" . sprintf('%012d', $expire) . $data . "\n?>";
$result = file_put_contents($filename, $data);
将序列化后的值拼接进字符串中,然后写入缓存文件。正常的写入字符串是这样:
前面被//
注释了,可以利用换行来绕过:
?username=%0d%0aphpinfo();//
成功写入恶意代码。在真实的环境中如果存在文件包含或者cache目录可以直接访问,再猜测一下传入的name,就可以实现RCE了。
修复
在拼接$data
前使用了exit(),使得写入恶意代码也无法执行。