序列化和反序列化(一)
因为通过这段时间的学习,发现,序列化和反序列化的考点和漏洞在平时遇到的还是比较多的,而且自己也没有特别去学习过这个知识点,所以在这里写一篇关于这里序列化和反序列话的博客,废话就停止在这里了。
在介绍具体的序列化和反序列化基础之前,因为没有系统性地整理过php的知识点,所以还是选择整理一下php的一些基础知识点
php面向对象基本概念、类与对象
一.面向对象
(一)基本介绍
面向对象(Object-Oriented,简称 OO)是一种编程思想和方法,它将程序中的数据和操作数据的方法封装在一起,形成"对象",并通过对象之间的交互和消息传递来完成程序的功能。
在面向对象的程序设计(英语:Object-oriented programming,缩写:OOP)中,对象是一个由信息及对信息进行处理的描述所组成的整体,是对现实世界的抽象。举个例子,在现实世界里我们所面对的事情都是对象,如计算机、电视机、自行车等。
PHP 面向对象 | 菜鸟教程
面向对象编程强调数据的封装、继承、多态和动态绑定等特性,使得程序具有更好的可扩展性、可维护性和可重用性。
(二)面向对象编程三大特征:封装、继承、多态
1、封装:将对象的属性和行为封装起来,将对象的属性和行为封装起来的载体就是类
2、继承:继承性是子类自动共享父类数据结构和方法的机制,是类之间的一种关系;当处理一个问题时,可以将一些有用的类保留下来,这些类通常有相同的属性甚至相同的方法,当遇到同样的问题时可以拿来复用。
父类:一个类被其它类继承,可将该类成为父类;
子类:一个类继承其他类称为子类(派生类);
3、多态:将父类对象应用于子类的特征就是多态。子类继承了父类的特点,同时子类又有自己的特点即是多态,多态指的是一个类的实例(对象)可以表现出多种形态。
二.面向过程
面向过程是一种以“整体事件”为中心的编程思想,编程的时候把解决问题的步骤分析处理,然后用函数把这些步骤实现,再一步步的具体步骤中再按顺序调用函数。
三.类
(一)定义(类和对象的关系)
类定义了一件事的特点,将数据的形式以及这些数据上的操作封装在一起;对象是具有类类型的变量,是对类的实例;类是想法,把类实例化,调用具体值后就变成对象。
举个例子
class Zerotwo
{
//类体:由成员函数(方法)和成员变量(属性)组成
//成员变量声明
//成员函数声明
}
在这个例子里面:
class为定义类的关键字,Zerotwo为类的名字,{}中为类的主体
类体中的内容为类的成员;
类中的变量称为成员变量(属性):定义再类内部的变量,该变量的值对外是不可见的,但是可以通过成员函数访问,在类被实例化为对象后,该变量即可成为对象的属性;
类中的函数称为成员函数(方法):定义在类的内部,可用于访问对象的数据。
(二)类的内容
1.预定义
class Zerotwo
{
var $name; //声明成员变量,var是一种修饰符
var $sexl;
function state($var1) //声明成员函数(方法)
{
echo $this->name; //使用预定义$this调用成员变量
echo $var1; //成员函数传参$var1可直接调用
}
}
运行结果可见,实际上这个定义了以后,因为只是定义好了类和类体,语言类型等等关键信息都没有加上,所以会被编译器自动识别,然后直接输出。那么接下来我们就要将这个类实例化和赋值
2、实例化和赋值
<?php
class Zerotwo
{
var $Darling; //声明成员变量,var是一种修饰符
var $Franxx;
function state($var1) //声明成员函数(方法)
{
echo $this->Darling; //使用预定义$this调用成员变量
echo $var1; //成员函数传参$var1可直接调用
}
}
$zzc = new Zerotwo(); //实例化类Zerotwo()为对象zzc
$zzc->Darling = '02,02,02'; //参数赋值
$zzc->Franxx = 'guojiadui';
print_r($zzc); //打印对象
?>
那么这里的运行结果就不一样很多了,从运行结果可以看见这里的类Zerotwo被实例化了,并且被对象zzc赋值了
再演示一个不指向的
<?php
class Zerotwo
{
var $Darling; //声明成员变量,var是一种修饰符
var $Franxx;
function state($var1) //声明成员函数(方法)
{
echo $this->Darling; //使用预定义$this调用成员变量
echo '关于'.$var1; //成员函数传参$var1可直接调用
}
}
$zzc = new Zerotwo(); //实例化类Zerotwo()为对象lin
$zzc->Darling = '02'; //参数赋值
$zzc->state('国家队');//调用函数
?>
运行结果
3、类的访问权限修饰符
PHP 支持三种访问修饰符:public
,protected
以及 private
,这些修饰符所提供的访问级别具体如下。
public:公共的,在类的内部、子类中 或者类的外部都可以使用,不受限制;
protected:受保护的,在类的内部、子类中可以使用,但不能在类的外部使用;
private:私有的,只能在类的内部使用,在类的外部或者子类中无法使用。
上图:
还是用刚刚的代码的例子
外部
<?php
class Zerotwo
{
public $Franxx = '国家队'; //声明成员变量,var是一种修饰符
private $Darling = '02';
protected $zerotwo = '比翼鸟';
function state($var1) //声明成员函数(方法)
{
echo $this->Darling; //使用预定义$this调用成员变量
echo $var1; //成员函数传参$var1可直接调用
}
}
$zzc = new Zerotwo(); //实例化类Zerotwo()为对象zzc
echo $zzc->Franxx; //外部可用
echo $zzc->Darling; //外部不可用
echo $zzc->zerotwo; //外部不可用
?>
运行结果如下
这里的代码是会报错的,因为违背了封装原则,这里尝试通过 $zzc->Darling 和 $zzc->zerotwo 在类的外部访问私有和受保护属性,就会导致错误,但是这个错误在这里无伤大雅,会在子类那边出错。
子类
再次修改代码进行访问
<?php
class Zerotwo //父类
{
public $Franxx = '国家队'; //声明成员变量,var是一种修饰符
private $Darling = '02';
protected $zerotwo = '比翼鸟';
function state($var1) //声明成员函数(方法)
{
echo $this->Darling; //使用预定义$this调用成员变量
echo $var1; //成员函数传参$var1可直接调用
}
}
class Zerotwo2 extends Zerotwo //子类
{
function test()
{
echo $this->Darling."<br />"; //private子类不可用
echo $this->zerotwo."<br />"; //protected子类可用
echo $this->Franxx."<br/>"; //public子类可以用
}
}
$zzc = new Zerotwo(); //实例化类Zerotwo()为对象zzc
$zzc2 = new Zerotwo2();
echo $zzc->Franxx."<br />"; //外部可用
echo $zzc->Darling; //外部不可用
echo $zzc2->test();
?>
分析
(这里的"<br />"是html语言的换行,php编译器是无法识别的,只会将它作为字符串输出,php语言的换行可见下面的代码)
这个时候会发现,出错了 ,这个结果并没有像编译的代码一样输出“$zzc2->test();”的内容,这里的原因是因为在 PHP 中,访问私有属性时,如果尝试从类的外部或从子类访问一个私有属性,会导致一个致命错误(Fatal Error)。这种错误会立即停止脚本的执行,因此任何后续的代码,包括 $zzc2->test();
,都不会被执行。这里也是为了演示一下,如果强行去访问私有属性会带来的后果。
修改以后的代码
<?php
class Zerotwo //父类
{
public $Franxx = '国家队'; // 公有成员变量
private $Darling = '02'; // 私有成员变量
protected $zerotwo = '比翼鸟'; // 受保护成员变量
function state($var1) // 声明成员函数
{
echo $this->Darling; // 使用 $this 调用成员变量
echo $var1; // 成员函数传参 $var1 可直接调用
}
}
class Zerotwo2 extends Zerotwo //子类
{
function test()
{
// echo $this->Darling."<br />"; // 不能访问私有属性,需移除或通过方法访问
echo $this->zerotwo . PHP_EOL; // 受保护成员变量,子类可用
echo $this->Franxx . PHP_EOL; // 公有成员变量,子类可用
}
}
$zzc = new Zerotwo(); // 实例化类 Zerotwo() 为对象 $zzc
$zzc2 = new Zerotwo2();
echo $zzc->Franxx . PHP_EOL; // 外部可用
// echo $zzc->Darling; // 外部不可用,需移除或通过方法访问
$zzc2->test();
?>
(这里的PHP_EOL就是php语言中的换行)
这里下面的两个输出就是由对象zzc2控制的。
那问题又来了,要怎么去访问私有属性呢?还是在原代码的基础上进行修改。
<?php
class Zerotwo //父类
{
public $Franxx = '国家队';
private $Darling = '02'; //定义了私有成员
protected $zerotwo = '比翼鸟';
function state($var1) // 声明成员函数
{
echo $this->Darling; // 使用 $this 调用成员变量
echo $var1; // 成员函数传参 $var1 可直接调用
}
private function test2() //定义私有成员的方法
{
echo $this->Darling; //在类的内部访问私有成员变量
}
function test3() //在类的内部访问私有成员的方法
{
$this->test2();
}
}
class Zerotwo2 extends Zerotwo //子类
{
function test()
{
// echo $this->Darling; // 私有属性,子类不可用
echo $this->zerotwo . PHP_EOL; // 受保护成员变量,子类可用
echo $this->Franxx . PHP_EOL; // 公有成员变量,子类可用
}
}
$zzc = new Zerotwo(); // 实例化类 Zerotwo() 为对象 $zzc
$zzc2 = new Zerotwo2();
echo $zzc->Franxx . PHP_EOL; // 外部可用
//echo $zzc->Darling; // 外部不可用,需移除或通过方法访问
$zzc2->test();
$zzc ->test3(); //通过在类的外部访问Zerotwo中的公有成员,间接访问Zerotwo中的私有成员
//私有成员变量无法直接从定义它的类的外部及该类的子类中直接访问
?>
通过这种间接访问的方法,就可以访问到这个私有成员了
这里就不再过多进行实验了,给出关于这个访问修饰符的文章
闪耀的钥匙:PHP 与访问修饰符_php访问修饰符-CSDN博客
序列化基础知识
一.PHP序列化:serialize()
序列化:是将变量或对象转换成字符串的过程( 序列化只作用于对象,不序列化方法),用于存储或传递 PHP 的值的过程中,同时不丢失其类型和结构。
简单而言
序列化:将对象的状态信息(属性)转换为可以存储或传输的形式的过程
对象/数组字符串
在php中使用函数serialize()将对象或者数组进行序列化,并返回一个包含字节流的字符串来表示。
php序列化的字母标识:
a - 数组 (Array): 一种数据结构,可以存储多个相同类型的元素。
b - 布尔型 (Boolean): 一种数据类型,只有两个可能的值:true 或 false。
d - 双精度浮点数 (Double): 一种数据类型,用于存储双精度浮点数值。
i - 整型 (Integer): 一种数据类型,用于存储整数值。
o - 普通对象 (Common Object): 一个通用的对象类型,它可以是任何类的实例。
r - 引用 (Reference): 指向对象的引用,而不是对象本身。
s - 字符串 (String): 一种数据类型,用于存储文本数据。
C - 自定义对象 (Custom Object): 指由开发者定义的特定类的实例。
O - 类 (Class): 在面向对象编程中,类是一种蓝图或模板,用于创建对象。
N - 空 (Null): 在许多编程语言中,null 表示一个不指向任何对象的特殊值。
R - 指针引用 (Pointer Reference): 一个指针变量,其值为另一个变量的地址。
U - 统一码字符串 (Unicode String): 一种数据类型,用于存储包含各种字符编码的文本数据。
各类型值的serialize序列化:
空字符 null -> N;
整型 123 -> i:123;
浮点型 1.5 -> d:1.5;
boolean型 true -> b:1;
boolean型 fal -> b:0;
字符串 “haha” -> s:4:"haha";
测试一下看看
(一)对象序列化
<?php
class Zerotwo //定义了一个test类
{
public $Franxx = 11;
private $Darling = aa;
protected $Zerotwo = true;
public function zerotwo()
{
echo $this->Franxx;
}
}
$zzc = new Zerotwo(); //将test这个类实例化
echo serialize($zzc); //序列化对象zzc
在这里的输出结果中:(11是公有成员输出的值)
O
: 这是一个标识符,表示接下来的数据是一个对象(O
)-
7:"Zerotwo"
: 这表示对象的类名是Zerotwo
,并且类名包含 7 个字符(实际上应该是 8 个字符,包括引号内的内容)。 -
:3:
: 这表示对象有 3 个属性。 -
{...}
: 花括号内包含了对象的属性和它们的值。-
s:6:"Franxx";i:11;
:s:6:"Franxx"
: 表示一个字符串属性名Franxx
,该字符串有 6 个字符。i:11
: 表示属性值是一个整数11
。
-
s:16:" Zerotwo Darling";s:2:"aa";
:s:16:" Zerotwo Darling"
: 表示一个字符串属性名Zerotwo Darling
,该字符串有 16 个字符(注意前面的空格)。s:2:"aa"
: 表示属性值是一个字符串aa
,该字符串有 2 个字符。
-
s:10:" * Zerotwo";b:1;
:s:10:" * Zerotwo"
: 表示一个字符串属性名* Zerotwo
,该字符串有 10 个字符(注意前面的空格和星号)。b:1
: 表示属性值是一个布尔值true
(在序列化中,布尔值true
被表示为1
,false
被表示为0
)。
-
同时,需要注意的是,protected 和private输出时有不可打印字符,如下图
所以在构造payload的时候,会使用urlencode函数,来实现构造
这里还有补充的一个小点是序列化的格式
序列化格式
- 私有属性(private): 私有属性在序列化时,属性名会被格式化为
"\0类名\0属性名"
,以确保在反序列化时仍然能识别出这是一个私有属性,并且属于哪个类。 - 受保护属性(protected): 受保护属性在序列化时,属性名会被格式化为
"\0*\0属性名"
,其中*
表示受保护属性。 - 公有属性(public): 公有属性在序列化时,属性名不会有特殊前缀,直接使用属性名。
这种序列化格式确保了在序列化和反序列化过程中,属性的可见性和所属类信息不会丢失。
(二)成员属性调用对象
<?php
class Zerotwo
{
var $zzc='zhu';
function test()
{
echo $this->zzc;
}
}
class Zerotwo2
{
var $abc;
function __construct() //使用了__construct魔术方法构造函数
{
$this->abc=new Zerotwo();
}
}
$a = new Zerotwo2();
echo serialize($a);
在这个例子中,实例化的对象$a的成员变量‘abc’调用实例化后的对象Zerotwo()
(三)pop链序列化
面向属性编程POP( Property Oriented Programming)
POP链就是利用魔法方法在里面进行多次跳转然后获取敏感数据的一种payload
<?php
class Zerotwo
{
public $a="11";
public $b=true;
public $c=0202;
}
class Zerotwo2
{
public $d;
public $e="zzczzc";
}
$m=new Zerotwo();
$n=new Zerotwo2();
$n->f=$m;
echo serialize($n);
代码中,$n->f = $m;
这一行是将$m
对象赋值给$n
对象的新属性$f
。这里,$f
是动态创建的,并不是在Zerotwo2
类的定义中显式声明的。这里m的值被嵌套在了n里面
(四)数组序列化
<?php
$zzc=array("zerotwo",0202,false);
echo serialize($zzc);
这里的
a表示这是一个数组的序列化(上面的内容有提到),成员属性名为数组的下标,格式 {i:数组下标;类型:长度:“值”; 以此类推}
二.PHP反序列化(unserialize)
(一)反序列化的特性
1.反序列化之后的内容为一个对象;
2.反序列化生成的对象里的值,由反序列化里的值提供;与原有类预定义的值无关;
3.反序列化不触发类的成员方法;需要调用方法后才能触发
(二)反序列化的作用
反序列化:将序列化后的参数还原成实例化的对象。
字符串对象
(三)例子演示
<?php
class Zerotwo
{
public $Franxx = 'zzc';
private $Darling = 0202;
protected $zerotwo = true;
public function displayVar()
{
echo $this->Franxx;
}
}
$d=new Zerotwo();
$b=serialize($d);
print_r(unserialize($b));
$a='O:7:"Zerotwo":3:{s:6:"Franxx";i:123123123;s:16:" Zerotwo Darling";s:2:"aa";s:10:" * Zerotwo";b:1;}';//反序列化生成的对象的成员属性值由被反序列化的字符串决定,与原来类预定义的值无关
$c=unserialize($a); //$c把字符串$b反序列化为对象
var_dump($c); //var_dump()用于打印变量的相关信息,包括变量的类型,值,长度等
在这个例子中,可以发现的是,反序列化生成的对象里的值,由反序列化里的值提供;与原有类预定义的值无关。为了方便观察,我将一开始预定义里面的值也打印出来了。
<?php
class Zerotwo
{
public $Franxx = 'zzc';
protected $Darling = 0202;
private $zerotwo = true;
public function displayVar()
{
echo $this->Franxx; //这里没有被调用
}
}
$d = 'O:7:"Zerotwo":3:{s:6:"Franxx";i:123123123;s:16:" Zerotwo Darling";s:2:"aa";s:10:" * Zerotwo";b:1;}';
$a = urldecode($d);
$c = unserialize($a);
$c->displayVar();
在这个例子中,反序列化不触发类的成员方法;需要调用方法后才能触发。
$d = 'O:7:"Zerotwo":3:{s:6:"Franxx";i:123123123;s:16:" Zerotwo Darling";s:2:"aa";s:10:" * Zerotwo";b:1;}';
$a = urldecode($d);
$c = unserialize($a);
这段代码将字符串反序列化为一个 Zerotwo
类的对象 $c
。但是,反序列化过程并不会调用 Zerotwo
类中的任何方法。只有当显式调用 $c->displayVar()
方法时,displayVar
方法中的代码才会被执行。
$c->displayVar();
在这个方法中,echo $this->Franxx;
会输出 Franxx
属性的值。在反序列化后的对象中,Franxx
的值被设置为 123123123
,因此调用 displayVar
方法将输出 123123123
。
(四)反序列化漏洞(简单介绍扩展一下)
1.PHP反序列化漏洞成因
通过上面的代码,可知在反序列化过程中,unserialize()接收的值(字符串)可控;通过改变这个值(字符串),得到所需要的代码,即生成的对象的属性值。
通过调用方法,触发代码执行。简单来讲,大概就是一种允许攻击者通过将恶意数据反序列化为PHP对象来执行任意代码的漏洞,我现在的理解是这样的。
2. PHP反序列化工作原理
PHP反序列化是一种将存储在字符串中的PHP对象转换为PHP变量的过程。这可以通过使用unserialize()函数来实现。
unserialize()函数:将字符串中的数据解析为一个PHP对象,并将其分配给一个变量。
3.PHP 反序列化漏洞的常见方法
(1)反弹 Shell。 这是一种在远程计算机上执行命令的方法。可以通过使用 exec() 或 system() 等函数来完成。
(2)文件上传。 这是一种将文件上传到远程计算机的方法。可以通过使用 move_uploaded_file() 函数来完成。
(3)本地文件包含。 这是一个在远程计算机上包含本地文件的方法。可以通过使用 include() 或 require() 等函数来完成。
(4)数据库访问。 这是一个访问远程计算机上的数据库的方法。可以通过使用 mysql_connect() 或 mysqli_connect() 等函数来完成。
4.防御PHP反序列化漏洞
(1)使用最新版本的 PHP。 最新版本的 PHP 通常包含针对已知漏洞的修复程序。
(2)禁用反序列化。 可以通过在 php.ini 文件中设置 unserialize_callback_func 选项来禁用反序列化。
(3)验证用户输入。 在对用户输入进行反序列化之前,请务必对其进行验证。
(4)使用白名单。 仅允许反序列化来自受信任来源的字符串。
(5)使用签名。 在序列化字符串之前,请使用签名对其进行签名。这将有助于防止恶意字符串被反序列化。
(五)魔术方法
魔术方法是一个预定好的、在特定情况下自动触发的行为方法。(触发前提:魔术方法所在的类被调用)
__construct 当一个对象创建时被调用,
__destruct 当一个对象销毁时被调用,
__toString 当一个对象被当作一个字符串被调用。
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__destruct() 对象被销毁时触发
__call() 对不存在的方法或者不可访问的方法进行调用就自动调用
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发,返回值需要为字符串
__invoke() 当脚本尝试将对象调用为函数时触发
__clone() 使用clone关键字拷贝完一个对象后触发
......
还是来一波实操
1.对象被创建时触发__construct()方法,对象使用完被销毁时触发__destruct()方法
<?php
class Zerotwo
{
public $Darling = "02";
public function __construct()
{
echo "创建成功----";
}
public function __destruct()
{
echo "销毁成功";
}
}
$Darling = new Zerotwo();
实例化test类为对象赋值给a时就被触发。
2.对象被序列化时触发__sleep(),字符串被反序列化时触发__wakeup()
<?php
class Zerotwo
{
public $Darling = "02";
public function __sleep()
{
echo "使用了serialize()----";
return array("Darling");
}
public function __wakeup()
{
echo "使用unserialize";
}
}
$Darling = new Zerotwo();
$Franxx=serialize($Darling);
$zerotwo=unserialize($Franxx);
3.echo $a 把对象当成字符串输出触发__toString(),$a() 把对象当成函数执行触发__invoke()
<?php
class Zerotwo
{
public $Darling = "02";
public function __toString()
{
return "字符串----";
}
public function __invoke()
{
echo "函数";
}
}
$Darling = new Zerotwo();
echo $Darling;
$Darling ();
4.$a->callxxx() 调用了不存在的方法 触发__call()方法
<?php
class Zerotwo
{
public function __call($Franxx,$Darling)
{
echo "鸡鸣寺的樱花开了";
}
}
$Franxx = new Zerotwo();
$Franxx->callxxx();
$a::callxxx() 静态调用 或 调用成员常量时使用不存在的方法,触发 __callStatic()方法
<?php
class Zerotwo
{
public function __callStatic($Franxx,$Darling)
{
echo "$Franxx";
}
}
$Franxx = new Zerotwo();
$Franxx::callxxx();
5.调用成员属性不存在时触发__get()方法
<?php
class Zerotwo
{
public $Darling;
public function __get($Darling)
{
echo $Darling;
}
}
$zerotwo= new Zerotwo();
$zerotwo->Franxx;
6.给不存在的成员属性赋值时触发__set()方法
<?php
class Zerotwo
{
public $Darling;
public function __set($Darling,$Franxx)
{
echo "$Franxx,$Darling";
}
}
$zerotwo= new Zerotwo();
$zerotwo->zzc=0202;
传参$Franxx,$Darling,返回不存在的成员属性名称及其值
这里特意换了一下位置看看
7.对不可调用的属性使用isset()或empty()时,isset()会被调用
对不可调用的属性使用unset()时,unset()会被调用
传参$Darling,返回不存在的成员属性名称
<?php
class Zerotwo
{
protected $Darling;
public function __isset($Darling)
{
echo "$Darling";
}
}
$zerotwo= new Zerotwo();
isset($zerotwo->Darling);
<?php
class Zerotwo
{
protected $Darling;
public function __unset($Darling)
{
echo "$Darling";
}
}
$zerotwo= new Zerotwo();
unset($zerotwo->DarlingDarling);
8.使用clone拷贝完成一个对象后,新对象会自动调用__clone()方法
<?php
class Zerotwo
{
public $Darling='0202';
public function __clone()
{
echo "调用了__clone()";
}
}
$zerotwo=new Zerotwo();
$Franxx=clone($zerotwo);
__wakeup()函数漏洞原理:(反序列化漏洞CVE-2016-7124)
当序列化字符串表示对象属性个数的值 大于 真实个数的属性时就会跳过__wakeup的执行
正则匹配大写字母O后不能跟数字时,可在数字前加+号绕过