php反序列化
php反序列化
魔术方法
__call()
类中没有此方法的时候调用此函数
<?php
class people{
public $age=18;
public $name="xiaomei";
public function __call($m,$b){
echo '不存在此方法'.$a;
echo '<br>';
echo implode(';',$b);
}
}
$a=new people();
$a->fack("hello","asdfcg");
?>输出
不存在此方法
hello;asdfcg
有此函数,但函数是private无法调用,也会触发__call();
serialize()
序列化
<?php
class people{
public $age=18;
public $name="xiaomei";
public function __call($m,$b){
echo '不存在此方法'.$a;
echo '<br>';
echo implode(';',$b);
}
}
$a=new people();
$b=serialize($a);
echo $b;
?>输出:
O:6:"people":2:{s:3:"age";i:18;s:4:"name";s:7:"xiaomei";}
unserialize()
反序列化
<?php
$a='i:1;';
echo unserialize($a);
?>
//输出1<?php
$a='s:5:"hello";';
$b=unserialize($a);
echo $b;
?>
//输出hello<?php
$a='s:5:"hello";';
class people{
public $name='zhang';
public $age=18;
public function speak(){
echo 'hello,my name is zhang';
}
}
$a=new people();
$b= serialize($a);
echo $b;
echo '<br>';
$c=unserialize($b);
var_dump($c);
?>
//反序列化不能使用echo输出
输出:
O:6:"people":2:{s:4:"name";s:5:"zhang";s:3:"age";i:18;}
C:\phpstudy_pro\WWW\phptest\array.php:16:
object(people)[2]
public 'name' => string 'zhang' (length=5)
public 'age' => int 18__get()
此函数用于属性不存在或者属性为不可访问时候出发
<?php
class people{
public $name='zhang';
public $age=18;
private $height=165;
public function __get($a){
echo $a.'属性不存在或者不能访问';
}
}
$a=new people();
echo($a->height);
echo '<br>';
echo($a->weight);
?>
输出:
height属性不存在或者不能访问
weight属性不存在或者不能访问__isset()
<?php
class people{
public $name = 'zhang';
public $age = 18;
private $height = 165;
public function __isset($property){
if (isset($this->$property)) {
echo "<br/>" . $property . " 存在\n";
return true;
} else {
echo "<br/>" . $property . " 不存在\n";
return false;
}
}
}
$a = new people();
var_dump(isset($a->name)); // 输出: bool(true)
var_dump($a->__isset('height')); // 输出: height 存在 bool(true)
isset($a->weight); // 输出: weight 不存在
?>__isset() 是 PHP 中的一个魔术方法(Magic Method),它在以下情况下被触发:
当对一个对象的 不可访问属性(inaccessible property)调用 isset() 或 empty() 时,__isset() 方法会被自动调用。
- 不可访问属性 包括:
- 私有属性(private):在当前类外部不可访问。
- 受保护属性(protected):在当前类及其子类外部不可访问。
- 未定义的属性:对象中不存在的属性。
__unset()
__unset() 是 PHP 中的一个魔术方法(Magic Method),用于在尝试 销毁(unset)一个对象的不可访问属性 时动态处理该操作。它的行为和 __isset() 类似,但用于处理属性的销毁。
触发条件:
当对一个对象的 不可访问属性(inaccessible property)调用 unset() 时,__unset() 方法会被自动调用。
不可访问属性 包括:
私有属性(private):在当前类外部不可访问。
受保护属性(protected):在当前类及其子类外部不可访问。
未定义的属性:对象中不存在的属性。
<?php
class Person {
private $age = 25;
public function __unset($name) {
echo "正在尝试销毁属性 $name\n";
// 可以在这里添加自定义逻辑
unset($this->$name);
}
}
$person = new Person();
// 触发 __unset(),因为 $age 是私有属性
unset($person->age); // 输出: 正在尝试销毁属性 age
// 触发 __unset(),因为 weight 属性不存在
unset($person->weight); // 输出: 正在尝试销毁属性 weight
?>'; } } $a = new people(); unset($a->age); ?>输出:无法删除属性后者不存在age

__sleep()
__sleep() 是 PHP 中的一个魔术方法,用于在对象序列化(serialize())时自动调用。它的主要作用是允许你在序列化对象之前执行一些清理操作,或者指定哪些属性应该被序列化。
用法
__sleep() 方法应该返回一个包含对象属性名称的数组。只有这些属性会被序列化。如果 __sleep() 没有定义或者返回一个空数组,那么所有属性都会被序列化。
<?php
class User {
private $name;
private $age;
private $password;
public function __construct($name, $age, $password) {
$this->name = $name;
$this->age = $age;
$this->password = $password;
}
// __sleep()魔术方法
public function __sleep() {
// 返回需要序列化的属性名数组
// 这里我们选择不序列化password属性
return array('name', 'age');
}
}
// 创建对象
$user = new User('John', 30, 'secret123');
// 序列化对象
$serialized = serialize($user);
echo "序列化后的字符串:\n";
echo $serialized . "\n\n";
echo "原始对象:\n";
print_r($user);
?>输出:
序列化后的字符串: O:4:"User":2:{s:10:"
原始对象: User Object ( [name:User:private] => John [age:User:private] => 30 [password:User:private] => secret123 )
注意事项
__sleep()必须返回一个数组:数组中的元素应该是你想要序列化的属性名称。__sleep()通常用于清理资源:例如关闭数据库连接、文件句柄等,这些资源不应该被序列化。__sleep()和__wakeup()是一对:__wakeup()在反序列化时自动调用,用于重新初始化对象。
__wakeup()
__wakeup() 是 PHP 中的一个魔术方法,用于在反序列化对象时自动调用。当使用 unserialize() 函数将一个序列化的字符串转换回对象时,如果该对象的类中定义了 __wakeup() 方法,PHP 会自动调用这个方法。
__wakeup() 通常用于重新初始化对象的状态,或者在反序列化后执行一些必要的操作。
下面是一个简单的示例,演示如何使用 __wakeup() 方法:
<?php
class MyClass {
public $data;
public function __construct($data) {
$this->data = $data;
echo "Constructor called. Data set to: " . $this->data . "\n";
}
public function __wakeup() {
echo "__wakeup() called. Reinitializing object.\n";
// 可以在反序列化后重新初始化对象的状态
$this->data = "Reinitialized Data";
}
public function getData() {
return $this->data;
}
}
// 创建一个对象并序列化它
$obj = new MyClass("Initial Data");
$serialized = serialize($obj);
echo "Serialized object: " . $serialized . "\n";
// 反序列化对象
$unserialized = unserialize($serialized);
echo "Unserialized object data: " . $unserialized->getData() . "\n";
?>输出结果:
Constructor called. Data set to: Initial Data
Serialized object: O:7:"MyClass":1:{s:4:"data";s:12:"Initial Data";}
__wakeup() called. Reinitializing object.
Unserialized object data: Reinitialized Data解释:
- 构造函数 (
__construct): 当创建MyClass的实例时,构造函数被调用,并初始化$data属性。 - 序列化 (
serialize): 将对象序列化为字符串。 - 反序列化 (
unserialize): 将序列化字符串转换回对象。在这个过程中,__wakeup()方法被自动调用,并且$data属性被重新初始化为"Reinitialized Data"。 - 获取数据 (
getData): 最后,我们通过getData()方法获取反序列化后的对象数据,并输出它。
总结:
__wakeup() 方法在反序列化对象时非常有用,特别是当你需要在反序列化后重新初始化对象的状态或执行一些必要的操作时。
__toString()
__toString() 是 PHP 中的一个魔术方法,用于定义当一个对象被当作字符串处理时的行为。例如,当你尝试直接 echo 或 print 一个对象,或者将对象与字符串连接时,PHP 会自动调用 __toString() 方法。
如果类中没有定义 __toString() 方法,直接尝试将对象当作字符串使用会导致致命错误(Fatal Error)。
示例代码
<?php
class User {
private $name;
private $email;
public function __construct($name, $email) {
$this->name = $name;
$this->email = $email;
}
// 定义 __toString() 方法
public function __toString() {
return "User: {$this->name} ({$this->email})";
}
}
// 创建一个 User 对象
$user = new User("John Doe", "john.doe@example.com");
// 直接输出对象
echo $user; // 这里会自动调用 __toString() 方法
?>输出结果:
User: John Doe (john.doe@example.com)解释:
__toString()方法:- 在这个例子中,
__toString()方法返回一个格式化的字符串,包含用户的姓名和邮箱。 - 当对象被当作字符串使用时(例如
echo $user),PHP 会自动调用__toString()方法。
- 在这个例子中,
- 直接输出对象:
- 如果没有定义
__toString()方法,直接echo $user会导致致命错误。 - 定义了
__toString()后,对象可以像字符串一样被输出。
- 如果没有定义
另一个示例:结合其他操作
<?php
class Product {
private $name;
private $price;
public function __construct($name, $price) {
$this->name = $name;
$this->price = $price;
}
// 定义 __toString() 方法
public function __toString() {
return "Product: {$this->name}, Price: {$this->price} USD";
}
}
// 创建一个 Product 对象
$product = new Product("Laptop", 1200);
// 直接输出对象
echo $product . "\n"; // 调用 __toString()
// 将对象与字符串连接
$description = "This is a " . $product;
echo $description . "\n";
?>输出结果:
Product: Laptop, Price: 1200 USD
This is a Product: Laptop, Price: 1200 USD注意事项:
- 返回值必须是字符串:
__toString()方法必须返回一个字符串。如果返回其他类型的值(如数组、对象等),会导致致命错误。
- 不能抛出异常:
- 在
__toString()方法中不能抛出异常,否则会导致致命错误。
- 在
- 用途:
__toString()通常用于调试或日志记录,方便将对象的状态以字符串形式输出。
总结:
__toString() 是一个非常实用的魔术方法,允许你将对象以字符串的形式表示。它在需要将对象直接输出或与字符串拼接时非常有用,但需要注意返回值必须是字符串,且不能抛出异常。
__invoke()
对象被当作函数调用时触发
定义与用途
- 定义:在 PHP 中,通过在类中定义
public function __invoke()方法,可以使该类的对象能够像函数一样被调用。 - 用途
- 允许将对象直接作为函数执行。
- 适用于创建可以在浏览器中显示内容的无状态组件(如 React 中的组件)。
<?php
class CallableObject {
private $message;
// 构造函数
public function __construct($message) {
$this->message = $message;
}
// 魔术方法 __invoke()
public function __invoke($name = null) {
if ($name) {
echo "Hello, $name! " . $this->message . "\n";
} else {
echo $this->message . "\n";
}
}
}
// 创建 CallableObject 的实例
$obj = new CallableObject("This is the __invoke() method in action!");
// 像调用函数一样调用对象
$obj(); // 输出: This is the __invoke() method in action!
// 带参数调用
$obj("John"); // 输出: Hello, John! This is the __invoke() method in action!
?>输出:
This is the __invoke() method in action! Hello, John! This is the __invoke() method in action!
使用场景
__invoke() 方法通常用于以下场景:
- 将对象作为回调函数:
- 例如,将对象传递给
array_map()、usort()等需要回调函数的函数。
- 例如,将对象传递给
- 实现可调用对象:
- 当你希望对象的行为类似于函数时,可以使用
__invoke()。
- 当你希望对象的行为类似于函数时,可以使用
- 简化代码:
- 在某些情况下,使用
__invoke()可以让代码更简洁和直观。
- 在某些情况下,使用
__clone()
__clone() 是 PHP 中的一个魔术方法,用于在对象被克隆时自动调用。当你使用 clone 关键字复制一个对象时,__clone() 方法会被触发,允许你在克隆过程中执行一些自定义操作。
使用场景
- 深拷贝:默认情况下,PHP 的
clone是浅拷贝。如果对象包含其他对象的引用,你可能需要在__clone()方法中手动处理这些引用的复制,以实现深拷贝。 - 资源重置:如果对象包含一些资源(如数据库连接、文件句柄等),你可能需要在克隆时重新初始化这些资源。
示例代码
class MyClass {
public $data;
public $resource;
public function __construct($data) {
$this->data = $data;
$this->resource = fopen('somefile.txt', 'r'); // 假设这是一个资源
}
// 魔术方法 __clone
public function __clone() {
// 深拷贝:克隆对象中的对象
$this->data = clone $this->data;
// 重新初始化资源
$this->resource = fopen('somefile.txt', 'r');
}
}
$obj1 = new MyClass(new stdClass());
$obj2 = clone $obj1; // 触发 __clone() 方法
var_dump($obj1);
var_dump($obj2);关键点
- 自动调用:
__clone()方法在对象被克隆时自动调用,无需手动调用。 - 深拷贝:如果你需要深拷贝对象中的引用类型属性(如其他对象),可以在
__clone()方法中手动处理。 - 资源管理:如果对象包含资源(如文件句柄、数据库连接等),通常需要在
__clone()方法中重新初始化这些资源。
注意事项
__clone()方法不能有参数。__clone()方法通常用于处理对象内部的复杂状态或资源,确保克隆后的对象是独立的。
通过合理使用 __clone() 方法,你可以更好地控制对象的克隆行为,确保克隆后的对象状态符合预期。
__autoload()
__autoload() 是 PHP 5 中引入的一个魔术方法,用于自动加载类文件。当代码中尝试使用一个尚未定义的类时,PHP 会自动调用 __autoload() 方法,并将类名作为参数传递给它。开发者可以在 __autoload() 方法中实现逻辑,动态加载所需的类文件。
使用场景
- 自动加载类文件:避免手动包含大量类文件,减少代码冗余。
- 按需加载:只有在类被使用时才加载对应的文件,提高性能。
示例代码
function __autoload($className) {
$filePath = __DIR__ . '/classes/' . $className . '.php';
if (file_exists($filePath)) {
require_once $filePath;
} else {
throw new Exception("Class $className could not be loaded.");
}
}
// 使用未定义的类时,__autoload() 会被调用
$obj = new MyClass(); // 自动加载 MyClass.php 文件关键点
- 自动触发:当代码中尝试使用一个未定义的类时,
__autoload()方法会被自动调用。 - 类名作为参数:
__autoload()方法接收一个参数,即需要加载的类名。 - 文件路径映射:通常会在
__autoload()方法中根据类名构造文件路径,并加载对应的文件。
注意:__autoload() 已被弃用
从 PHP 7.2.0 开始,__autoload() 方法被弃用,并建议使用 spl_autoload_register() 函数来注册自定义的自动加载函数。spl_autoload_register() 更加灵活,允许注册多个自动加载函数。
使用 spl_autoload_register() 的示例
spl_autoload_register(function ($className) {
$filePath = __DIR__ . '/classes/' . $className . '.php';
if (file_exists($filePath)) {
require_once $filePath;
} else {
throw new Exception("Class $className could not be loaded.");
}
});
// 使用未定义的类时,自动加载函数会被调用
$obj = new MyClass(); // 自动加载 MyClass.php 文件spl_autoload_register() 的优势
- 支持多个加载器:可以注册多个自动加载函数,按顺序尝试加载类。
- 更灵活:可以通过匿名函数、类方法等多种方式注册自动加载逻辑。
- 兼容性:是 PHP 推荐的自动加载方式,兼容性更好。
总结
__autoload()是 PHP 5 中用于自动加载类的魔术方法,但已被弃用。- 推荐使用
spl_autoload_register()来实现自动加载,功能更强大且灵活。 - 自动加载机制可以显著减少手动包含文件的工作量,并提高代码的可维护性。
__debugInfo()
__debugInfo() 是 PHP 5.6 引入的一个魔术方法,用于自定义对象在使用 var_dump() 或 print_r() 等调试函数时的输出内容。默认情况下,var_dump() 会显示对象的所有属性,但通过实现 __debugInfo() 方法,你可以控制哪些属性或信息被输出,从而提供更简洁或更有意义的调试信息。
使用场景
- 隐藏敏感信息:例如,避免在调试输出中暴露密码、密钥等敏感数据。
- 简化调试信息:只显示与调试相关的关键属性,减少冗余信息。
- 自定义输出格式:提供更友好的调试信息格式。
示例代码
class User {
private $name;
private $email;
private $password; // 敏感信息
public function __construct($name, $email, $password) {
$this->name = $name;
$this->email = $email;
$this->password = $password;
}
// 自定义调试信息
public function __debugInfo() {
return [
'name' => $this->name,
'email' => $this->email,
'password' => '***REDACTED***', // 隐藏敏感信息
];
}
}
$user = new User('John Doe', 'john@example.com', 'secret123');
// 使用 var_dump 输出调试信息
var_dump($user);输出结果
plaintext
object(User)#1 (3) {
["name"]=> string(8) "John Doe"
["email"]=> string(15) "john@example.com"
["password"]=> string(12) "***REDACTED***"
}关键点
- 返回值:
__debugInfo()方法必须返回一个数组,数组的内容将作为调试信息输出。 - 覆盖默认行为:实现
__debugInfo()后,var_dump()和print_r()将不再显示对象的所有属性,而是显示__debugInfo()返回的内容。 - 调试友好:通过自定义调试信息,可以使调试输出更清晰、更安全。
注意事项
- 如果没有实现
__debugInfo()方法,var_dump()和print_r()会默认显示对象的所有属性(包括私有和受保护的属性)。 __debugInfo()不会影响对象的序列化行为(如serialize()和json_encode()),它仅用于调试输出。
总结
__debugInfo()是一个非常有用的魔术方法,特别适合在调试时隐藏敏感信息或简化输出。- 通过实现
__debugInfo(),你可以完全控制对象在调试时的表现,使其更符合实际需求。 - 它是 PHP 5.6 及以上版本的功能,建议在需要调试自定义对象时使用。
魔术方法总览
PHP中的魔术方法包括:
- __construct()
- 类的构造函数,用于初始化对象。
- 当使用
new关键字实例化对象时,会自动调用。
- __destruct()
- 析构函数,用于释放资源。
- 当对象被销毁时,会自动调用。
- __call()
- 当调用类中不存在的非静态方法时,会自动执行此方法。
- 提供两个参数:方法名和方法的参数列表。
- __callStatic()
- 当使用静态方式调用类中不存在的方法时,会自动执行此方法。
- 提供两个参数:方法名和方法的参数列表。
- __get()
- 访问类中未定义的属性时,会自动调用此方法。
- 提供一个参数:属性名。
- __set()
- 设置类中未定义的属性值时,会自动调用此方法。
- 提供两个参数:属性名和属性值。
- __isset()
- 当对未定义的属性使用
isset()函数时,会自动调用此方法。 - 提供一个参数:属性名。
- __unset()
- 当对未定义的属性使用
unset()函数时,会自动调用此方法。 - 提供一个参数:属性名。
- __sleep()
- 当对象被序列化时,会先调用此方法。
- 返回一个包含所有应被序列化的属性名的数组。
__wakeup()
- 当对象被反序列化时,会先调用此方法。
- 用于重新初始化对象状态。
__toString()
- 当对象被当作字符串处理时,会自动调用此方法。
- 需要返回一个字符串。
__invoke()
- 当对象被当作函数调用时,会自动调用此方法。
- 提供一个参数:函数的参数列表。
__set_state()
- 当使用
var_export()函数导出类时,此静态方法会被调用。 - 提供一个参数:包含类属性名和值的数组。
- 当使用
__clone()
- 当对象被克隆时,会自动调用此方法。
- 通常用于在新对象中执行必要的初始化操作。
__autoload()
- 当尝试加载未定义的类时,会自动调用此方法。
- 用于实现自动加载类文件的功能。
__debugInfo()
- 当使用
var_dump()或print_r()函数转储对象时,会自动调用此方法。 - 用于返回调试时所需的信息。
- 当使用
这些魔术方法让开发者能够更好地控制对象的行为,特别是在处理不常见的操作或者需要自动化处理某些任务时非常有用。
实践
__wakeup()
当反序列化字符串中类的个数多余实际类中的个数时,__wakeup()魔术方法会被绕过

__destruct()

PHP 在反序列化时,语法是以
;作为字段的分隔,以}作为结尾并且是根据长度判断内容且其中字符串必须以双引号包裹,不能不写或以单引号包裹; 在结束符}之后的任何内容不会影响反序列化的后的结果
注意点,很容易以为序列化后的字符串是;},但对象序列化是直接}结尾,php反序列化字符逃逸,就是通过这个结尾符实现的当长度不对应的时候会出现报错
字符串逃逸
在 PHP 反序列化中,字符串逃逸(String Escape)是一种利用序列化字符串的格式特性,通过字符替换或过滤操作改变序列化数据的结构,从而注入恶意属性或篡改原有数据的攻击技术。其核心在于利用序列化字符串的严格语法格式(如长度标记 s:长度:"值";)与过滤逻辑的不一致性,构造精心设计的字符串,绕过校验并触发非预期行为。
原理剖析
PHP 的序列化字符串格式对每个属性值的长度和内容有严格规定,例如:
// 示例类
class User {
public $username = "admin";
public $isAdmin = false;
}
// 序列化后的字符串
O:4:"User":2:{s:8:"username";s:5:"admin";s:7:"isAdmin";b:0;}- 结构解析:
s:8:"username"表示属性名是一个长度为 8 的字符串"username"。 - 关键点:如果对序列化字符串中的某些字符进行过滤或替换(如删除敏感词、转义字符),可能导致字符串的实际长度与声明的长度不一致,从而破坏序列化结构。
字符串逃逸的两种类型
1. 字符增多逃逸(正向逃逸)
场景:过滤函数将某个字符替换为更长的字符串(如 "<" → "<")。
攻击思路:
- 构造足够多的被过滤字符,使替换后的字符串长度超过原长度声明。
- 后续字符被解析为新的属性或值,实现注入。
示例:
假设代码过滤 "a" 并替换为 "aa",原始序列化字符串为:
s:5:"value";s:10:"aaaaa";}s:3:"cmd";s:2:"ls";过滤后变为:
s:5:"value";s:10:"aaaaaaaaaa";}s:3:"cmd";s:2:"ls";由于实际字符串长度变为 10,但长度声明仍为 5,解析器会继续解析后续字符 ";}s:3:"cmd"...,导致 cmd 属性被注入。
2. 字符减少逃逸(反向逃逸)
场景:过滤函数删除某些字符(如删除 "x")。
攻击思路:
- 构造被删除的字符,使替换后的字符串长度短于原长度声明。
- 后续字符被“吞并”为当前属性值的一部分,破坏原有结构。
示例:
假设代码删除 "x",原始序列化字符串为:
s:5:"value";s:12:"xxx";}s:3:"cmd";s:2:"ls";}过滤后变为:
s:5:"value";s:12:"";}s:3:"cmd";s:2:"ls";}解析器会认为 value 属性的值为 ";}s:3:"cmd"... 的前 12 个字符,导致后续 cmd 属性被解析。
完整攻击示例
场景描述
假设一个类在反序列化时会过滤 "danger" 字符串:
class Filter {
public $data;
public function __wakeup() {
$this->data = str_replace("danger", "", $this->data); // 删除 "danger"
}
}
// 攻击者构造的序列化数据
$payload = 'O:6:"Filter":1:{s:4:"data";s:20:"dangerdanger";s:10:"injected";s:5:"evil";}";}';
unserialize($payload);攻击步骤
原始序列化字符串:
O:6:"Filter":1:{s:4:"data";s:20:"dangerdanger";s:10:"injected";s:5:"evil";}";}过滤后字符串:
"danger"被删除,"dangerdanger"→ 空字符串。- 实际
data属性的值变为"",但原长度声明为s:20。 - 解析器会从当前位置向后读取 20 个字符(包括后续部分)。
结构破坏与注入:
s:4:"data";s:20:"";s:10:"injected";s:5:"evil";}";- 解析器读取
""(2字符),剩余需要读取 18 字符。 - 后续的
;s:10:"injected"...被当作data属性值的一部分,直到满足 20 字符长度。 - 最终
injected属性被成功注入。
- 解析器读取
防御措施
- 避免处理序列化数据:尽量不使用
unserialize()处理用户输入。 - 严格校验输入格式:使用正则表达式验证序列化字符串的合法性。
- 使用安全的反序列化函数:
// 限制允许反序列化的类 unserialize($data, ['allowed_classes' => ['SafeClass']]); - 禁止字符替换操作:避免在反序列化前对数据进行替换或过滤。
总结
字符串逃逸攻击的本质是利用序列化字符串的格式漏洞,通过字符替换或删除改变数据结构,注入恶意属性。防御的核心在于严格校验输入数据的完整性和合法性,避免对序列化字符串进行破坏结构的操作。
