PHP 魔术方法讲解
PHP 魔术方法讲解
魔术方法(Magic Methods)是 PHP 中以双下划线
__开头的特殊方法,它们在特定条件下由 PHP 自动调用,无需手动触发。掌握它们的触发时机和参数传递机制,是理解 PHP 面向对象和反序列化漏洞的基础。
总览:魔术方法触发条件速查
| 魔术方法 | 触发条件 | 参数 |
|---|---|---|
__construct() | new 实例化对象时 | 构造参数 |
__destruct() | 对象被销毁时(脚本结束或无引用) | 无 |
__sleep() | serialize() 序列化时 | 无,需返回数组 |
__wakeup() | unserialize() 反序列化时 | 无 |
__toString() | 对象被当作字符串使用时(echo/print 等) | 无,需返回字符串 |
__invoke() | 对象被当作函数调用时 | 调用参数 |
__clone() | clone 克隆对象时 | 无 |
__call() | 调用不存在的方法时 | ($方法名, $参数数组) |
__callStatic() | 静态调用不存在的方法时 | ($方法名, $参数数组) |
__get() | 访问不存在的属性时 | ($属性名) |
__set() | 设置不存在的属性时 | ($属性名, $属性值) |
__isset() | 对不存在的属性使用 isset()/empty() 时 | ($属性名) |
__unset() | 对不存在的属性使用 unset() 时 | ($属性名) |
__construct() — 构造函数
触发时机: 使用 new 实例化对象时自动调用,且仅在实例化时触发一次。
1 |
|
说明: 第 8 行 new User("benben") 触发 __construct()。注意,unserialize() 创建对象时不会触发 __construct(),因为反序列化是直接从字符串恢复对象状态,而非走正常的实例化流程。
__destruct() — 析构函数
触发时机: 对象不再被使用(无引用)或脚本执行结束时,PHP 垃圾回收机制销毁对象并自动调用。
1 |
|
两次触发的详细分析:
第一次触发: 第 8 行创建的
$test对象。当脚本运行到?>结束时,所有变量被销毁,$test指向的对象触发__destruct()。第二次触发: 第 10 行
unserialize($ser)在内存中重新创建了一个新对象。但因为没有将结果赋值给任何变量(没有写$a = unserialize($ser)),这个临时对象在当行执行完后就没有任何引用了,PHP 认为它已无用,立即销毁,触发第二次__destruct()。
安全视角: 这正是反序列化漏洞利用的关键——攻击者可以通过控制序列化数据,在
__destruct()中执行危险操作。即使没有变量接收反序列化结果,对象也会被创建并最终触发析构。
__sleep() — 序列化前置钩子
触发时机: 对对象调用 serialize() 时,PHP 会先检查类中是否存在 __sleep()。
- 如果存在: 先执行
__sleep(),然后根据其返回值(一个属性名数组)决定序列化哪些属性。 - 如果不存在: 默认序列化对象中所有属性(包括私有属性)。
1 |
|
输出:
1 | O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";} |
说明: __sleep() 返回了 array('username', 'nickname'),所以序列化结果中只包含这两个属性,$password 被忽略了。__sleep() 的作用就是精准控制哪些属性参与序列化——常用于过滤敏感数据(如密码、密钥)或执行序列化前的清理工作(如关闭数据库连接、保存文件句柄状态)。
__wakeup() — 反序列化前置钩子
触发时机: 对字符串调用 unserialize() 时,PHP 会先检查类中是否存在 __wakeup(),如果存在则先执行它,再完成反序列化。
1 |
|
输出:
1 | object(User)#1 (4) { |
说明: __wakeup() 在反序列化时被触发,将 password 属性的值设为 username 的值(即 "a")。这里序列化字符串只传了 2 个属性(username 和 nickname),但 __wakeup() 可以用来初始化或修正未被序列化的属性。
与
__destruct()的联动:unserialize()的完整流程是:创建对象 →__wakeup()→ 对象可用 → 对象销毁时__destruct()。所以反序列化可能同时触发这两个方法。
__wakeup() 绕过 — CVE-2016-7124
影响版本: PHP5 < 5.6.25,PHP7 < 7.0.10
原理: 当序列化字符串中声明的属性个数大于实际属性个数时,__wakeup() 会被跳过,但反序列化仍然正常执行。
1 | 正常情况(属性个数 = 2,会触发 __wakeup) |
只需要将对象标识符后的属性个数从 2 改为 3(或其他比实际值大的数),即可绕过 __wakeup() 的执行。这在 CTF 中是经典的考点。
__toString() — 对象转字符串
触发时机: 当对象被当作字符串使用时自动调用,例如 echo、print、字符串拼接等操作。
1 |
|
说明: print_r() 和 var_dump() 不会触发 __toString(),因为它们的作用是调试输出对象结构。而 echo 要求参数是字符串,当传入对象时就会触发 __toString()。方法必须返回一个字符串,否则会报错。
__invoke() — 对象当函数调用
触发时机: 当对象被当作函数调用时(即 $obj() 的形式)自动触发。
1 |
|
说明: $test->benben 是正常的属性访问,而 $test() 是将整个对象当作函数来调用,PHP 会自动拦截并执行 __invoke()。
错误调用的拦截方法
__call() — 调用不存在的方法
触发时机: 调用一个对象中不存在的方法时自动触发。
参数: __call($方法名, $参数数组) — 第一个参数是不存在的方法名,第二个参数是调用时传入的参数数组。
1 |
|
说明: $test->callxxx('a') 中 callxxx 不存在,__call() 接收到 $arg1 = "callxxx"、$arg2 = ["a"]。
__callStatic() — 静态调用不存在的方法
触发时机: 使用 :: 静态调用符号调用一个不存在的方法时触发。
参数: 同 __call(),为 __callStatic($方法名, $参数数组)。
1 |
|
说明: :: 是 PHP 的静态调用符号(也用于调用类常量和静态属性)。此处 callxxx 不是静态方法,触发 __callStatic()。
__get() — 访问不存在的属性
触发时机: 访问一个对象中不存在的属性时触发。
参数: __get($属性名) — 传入被访问的属性名。
1 |
|
说明: $var1 是存在的属性,访问它不会触发 __get()。而 $var2 不存在,触发 __get() 并将属性名 "var2" 传入。在 CTF 中,__get() 常被用于构造 POP 链——通过访问一个不存在的属性来跳转到另一个对象的某个方法。
__set() — 设置不存在的属性
触发时机: 给一个不存在的属性赋值时触发。
参数: __set($属性名, $属性值) — 传入属性名和要赋的值。
逻辑与
__get()对称:__get()是读不存在的属性,__set()是写不存在的属性。
__isset() — 对不存在的属性使用 isset()/empty()
触发时机: 对不存在的属性使用 isset() 或 empty() 时触发。
参数: __isset($属性名) — 传入被检测的属性名。
__unset() — 对不存在的属性使用 unset()
触发时机: 对不存在的属性使用 unset() 时触发。
参数: __unset($属性名) — 传入被销毁的属性名。
1 |
|
说明: 此处 $var 虽然在类中定义了,但它是 private 的。从类外部访问时,PHP 将其视为不可访问/不存在,因此 unset() 操作触发了 __unset()。
__clone() — 克隆对象时触发
触发时机: 使用 clone 关键字克隆对象时,在新对象上自动调用。
1 |
|
说明: clone 创建的是对象的浅拷贝。__clone() 在副本对象上执行,可以用来修正深拷贝需要处理的引用类型属性(如重新初始化内部对象引用,避免两个对象共享同一个子对象)。
附录:反序列化 POP 链常见触发链路
在 CTF 和安全审计中,理解魔术方法的触发顺序是构造 POP(Property-Oriented Programming)链的前提:
1 | unserialize() → __wakeup() → 对象可用 → __destruct() |
典型利用思路:
- 找到可利用的
__destruct()或__wakeup()作为入口 - 在其中寻找对其他对象属性的操作,触发
__get()/__call()/__toString()等跳板 - 最终跳到能执行危险操作的方法(如
eval()、system()等)作为终点
