深入浅出反序列化漏洞

2022-04-28 15:46:07 浏览数 (2)

一、前置知识

阅读本文前应该先去了解,什么是类,什么是对象,推荐搜索关键词,php对象和类,java对象和类

二、反序列化

用大白话来讲,序列化就是把(类的实例化对象)对象序列化成字符串,反序列化就是把字符串又转化回对象。打个比方,序列化就是把你洗菜,做菜,炒菜最后做出一盘红烧排骨的一系列动作,写成菜谱。而反序列化就是你拿着菜谱,按照菜谱的步骤又做出一盘红烧排骨。

1.php反序列化

serialize():序列化函数 unserialize():反序列化函数

下面我先用php代码举个栗子。

代码语言:javascript复制
//序列化
<?php
    //类meat
	class meat
	{
		var $say = "tastes delicious!";
		var $cost = 198;
		function welcome()
		{
			echo 'nice to meet you.';
		}
	}
	$pork = new meat();
	echo serialize($pork);
?>

访问这个php文件,就得出了序列化之后的字符串

代码语言:javascript复制
O:4:"meat":2:{s:3:"say";s:17:"tastes delicious!";s:4:"cost";i:198;}

//说明如下
O:4:"meat":2:     
O:object 说明我们序列化的是一个对象,后面的4代表类名占四位字符,类的名字为meat,2代表类有两个变量
s:3:"say";s:17:"tastes delicious!";
第一个变量名为字符串,占三个字符,为say,第一个变量值为字符串,占17个字符为tastes delicious!
s:4:"cost";i:198;
第二个变量名为字符串占四个字符,为cost,第二个变量值为整形,为198

从序列化的字符串中我们可以看出来,序列化的字符串中只含有类名还有类中变量的信息,没有类中函数的信息,之所以这么做的原因是同一类对象中每个对象都具备相同的函数,但是每个对象的变量值却不一定相同,所以我们序列化时只要保存对象的变量就可以了

思考一下,是不是我们如果能控制序列化后的字符串,我们就能控制反序列化后的对象。下面将刚刚序列化后的字符串反序列化回去。

代码语言:javascript复制
<?php
    //类meat
	class meat
	{
		var $say = "tastes delicious!";
		var $cost = 198;
		function welcome()
		{
			echo 'nice to meet you.';
		}
	}	
	//注意反序列化时当前类的定义一定要在当前文件中
	$str = 'O:4:"meat":2:{s:3:"say";s:17:"tastes delicious!";s:4:"cost";i:198;}';
	$pork = unserialize($str);
	$pork->welcome();
	echo '<br/>';
	var_dump($pork);
?>

代码运行结果如下,这就是一整个序列化和反序列化的过程

2.php魔术方法

从上文看到,我们能控制的只有序列化后的字符串,序列化的字符串只能控制对象中的变量,反序列化后,能不能调用对象中的函数,不是我们能控制的,源码中对象的成员函数有没有调用,是在源码中写死的,所以此时就引出了魔术方法。

魔术方法,之所以被冠名为魔术方法,就因为它们很神奇,神奇的地方就是,它们不需要手动调用,只要满足了触发条件,就能自动被调用。

代码语言:javascript复制
__construct():当一个对象创建时被调用
__destruct():当一个对象销毁时被调用
__toString():当对象被当作字符串时被调用
__sleep():当对象被序列化时被调用
__wakeup():当对象被反序列化时被调用
__get():当调用一个未定义的属性时被调用
__set():给一个未定义的属性赋值时调用
__invoke():以调用函数的方式调用一个对象时被调用。

如果我们控制的序列化字符串中的变量,在这些魔术方法中的话,魔术方法被触发,我们的恶意参数就会生效,这就是反序列化漏洞。

下面我们简单演示一下魔术方法是如何被触发的。

代码语言:javascript复制
<?php
    //类meat
	class meat
	{
		var $say = "tastes delicious!";
		var $cost = 198;
		function welcome()
		{
			echo 'nice to meet you.';
		}
		//当对象被反序列化时被调用
		function __wakeup()
		{
			echo $this->cost;
			echo '<br/>';
		}
	}
	$str = 'O:4:"meat":2:{s:3:"say";s:17:"tastes delicious!";s:4:"cost";i:99999;}';
	$pork = unserialize($str); 、
	$pork->welcome();
	echo '<br/>';
	var_dump($pork);
?>    

此时我们就是修改了序列化字符串中变量的值,将它从198修改成了99999,刚好该参数在魔术方法__wakeup中,在反序列化时此魔术方法被触发,这样子我们的恶意参数就生效了,原本程序想要输出198,结果被我们篡改成了99999。

3.java反序列化

java反序列化和php反序列化根本原理都是一样的,这不过其中的函数有不同。

java规定如果一个对象想要进行序列化操作,那么这个对象对应的类必须实现序列化接口,也就是Serializable

接口。一个可以被序列化的对象的类定义如下:

代码语言:javascript复制
import java.io.Serializable;
public class Person implements Serializable {
    private String name;
}

java的序列化使用的是ObjectOutputStream(对象输出流)类的writeObject()方法。

java的反序列化使用的是ObjectInputStream(对象输入流)类的readObject()方法。

3.1序列化过程

代码语言:javascript复制
public class Person implements Serializable 
{
    private String name;
    private int age;
 
    Person(String name, int age) 
    {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public String toString() 
    {
        return "姓名:" name "年龄:" age;
    }
}

public static void main(String[] args) throws IOException 
{
        //实例化Person对象
        Person jack = new Person("jack", 12);
        //生成一个文件对象,文件不存在将自动创建文件
        File f = new File("F:"   File.separator   "serTest.txt");
        //构造一个对象输出流oos
        ObjectOutputStream oos = null;
        //构造一个文件输出流
        FileOutputStream fileOutputStream = new FileOutputStream(f);
        //构造对象输出流
        oos = new ObjectOutputStream(fileOutputStream);
        //序列化一个对象到文件变成二进制内容,二进制字节流文件,直接打开是乱码
        oos.writeObject(jack);
        oos.close();
}

3.2反序列化过程

代码语言:javascript复制
public class Person implements Serializable 
{
    private String name;
    private int age;
 
    Person(String name, int age) 
    {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public String toString() 
    {
        return "姓名:" name "年龄:" age;
    }
}

public static void main(String[] args) throws IOException, ClassNotFoundException
{
        //生成一个文件对象
        File f = new File("F:"   File.separator   "serTest.txt");
        //构建对象输入流对象
        ObjectInputStream oos = null;
        //构建文件输入流对象
        FileInputStream fileOutputStream = new FileInputStream(f);
        oos = new ObjectInputStream(fileOutputStream);
        //读取序列化
        Person jack=(Person)oos.readObject();
        System.out.println(jack);
   
}

3.3反序列化漏洞demo

如果类是这么写的,在反序列化的时候就会弹出计算器了。

代码语言:javascript复制
public class Person implements Serializable 
{
    private String name;
    private int age;
 
    Person(String name, int age) 
    {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public String toString() 
    {
        return "姓名:" name "年龄:" age;
    }
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        
        // 执行默认的 readObject() 方法
        in.defaultReadObject();
        
        // 执行打开计算器程序命令
        Runtime.getRuntime().exec("calc");
    }
}
    

0 人点赞