前文留了个坑,8u231之后还出现了限制与绕过,填个坑
本节的思路:
- 回顾JEP290 bypass的原理
- 8u231的限制,老的JRMP绕过方式为何不行了
- An Trinh的绕过方式
- 8u241的限制
JEP290 JRMP绕过原理
回顾前文讲过,绕过JEP290的原理如下:
- 利用JEP290白名单内的类,建立JRMP连接,覆盖掉registerRefs
- Registry 处理bind请求,最后会触发DGC逻辑,DGC向registerRefs里保存的Server发送dirty请求
- DGC底层使用的是StreamRemoteCall.executeCall进行io的传输,executeCall发送完之后,会根据对方的反馈进行不同的操作,当对方返回一定条件时(初步判断是对方RMI Exception),会直接序列化InputStream,造成反序列化漏洞
但是8u231中已经对此做了限制
8u231限制
详情可以参考【1】,讲的很详细,不赘述
这里直接给出重点结论
- sun.rmi.registry.RegistryImpl_Skel#dispatch报错情况消除ref
- sun.rmi.transport.DGCImpl_Stub#dirty提前了黑名单
An Trinh的RMI注册端的Bypass方法
复现
直接参考【1】ysomap中的poc
Start Evil Registry
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 8888 CommonsCollections5 "open -a calculator.app"
Start Vuln Registry
启动helloRegistry
启动 ysomap/core/exploit/rmi/NamingTest.java
分析
同样在cc链上下断点
1 | readObject:150, LazyMap (org.apache.commons.collections.map) |
先来看看两次payload的区别
1 | public void test_bypass_jep290_plus() throws Exception{ |
跟踪调试下来发现参考【1】讲解的很详细了,需要仔细阅读,没什么多的好讲的,原文涉及到的就不赘述,啰嗦下自己的理解,重要的记录下
1 | 原JEP290实际上 序列化代码反序列化过程中 并未建立JRMP连接,只是覆盖了incomingRefTable,从而影响了DGC流程触发漏洞;而An Trinh的攻击思路是 直接在反序列化的时候,建立JRMP连接,触发UnicastRef底层StreamRemoteCall的处理异常逻辑(之后还是会走原JEP290覆盖incomingRefTable的流程)。 |
这个调用栈很关键
跟踪下调用栈
- UnicastRemoteObject.readObject
1 | private void readObject(java.io.ObjectInputStream in) |
此时的ssf为 serverSocketFactory,值为Proxy[RMIServerSocketFactory,RemoteObjectInvocationHandler[UnicastRef [liveRef: [endpoint:[127.0.0.1:10999](remote),objID:[0:0:0, 534121287]]]]]
1 | public static Remote exportObject(Remote obj, int port, |
注意到此时obj属性ref被值为sref,sref=UnicastServerRef(new LiveRef(port=0, csf=null, ssf=serverSocketFactory))。
是不是很类似原poc中构造UnicastRef的部分
1 | ObjID id = new ObjID(new Random().nextInt()); // RMI registry |
- UnicastServerRef/LiveRef/TCPEndpoint/TCPTransport
这个过程比较简单
UnicastServerRef$exportObject
1 | Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3); |
其中this.ref为上面的LiveRef,var5为Proxy[Remote,RemoteObjectInvocationHandler[UnicastRef2 [liveRef: [endpoint:[localhost:0,Proxy[RMIServerSocketFactory,RemoteObjectInvocationHandler[UnicastRef [liveRef: [endpoint:[127.0.0.1:10999](remote),objID:[0:0:0, 534121287]]]]]](local),objID:[-e331dff:173958b39d9:-7ffe, -3106094860174826018]]]]]
LiveRef$exportObject
接下来没什么重要的,直到建立TCP连接并监听
- TCPEndpoint$newServerSocket
1 | ServerSocket var2 = ((RMIServerSocketFactory)var1).createServerSocket(this.listenPort); |
这里需要注意的是,var1为前文的代理,值为上文的serverSocketFactory,具体值为Proxy[RMIServerSocketFactory,RemoteObjectInvocationHandler[UnicastRef [liveRef: [endpoint:[127.0.0.1:10999](remote),objID:[0:0:0, 534121287]]]]]
serverSocketFactory.所有方法(当让包括createServerSocket)动态代理到RemoteObjectInvoationhandler的invoke,对应POC处的代码
1 | RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance( |
所以下一步是会是
RemoteObjectInvocationHandler$invoke
RemoteObjectInvocationHandler$invokeRemoteMethod
- RemoteObjectInvocationHandler$invokeRemoteMethod
1 | private Object invokeRemoteMethod(Object proxy, |
UnicastRef$invoke
1 | var7.executeCall(); |
这里和原JEP290的就不一样的,虽然都有用到这个类,但是原来的调用链用到的是UnicastRef$readExternal,这里就不一样了,executeCall会进入到StreamRemoteCall.executeCall流程,也就是前文的经常提及的触发点。
如何发送序列化Payload
前文的test_bypass_jep290_plus 中构造了可利用的remoteObject,没有提及如何发送给Vuln Registry,为了简化直接使用的
1 | // 修改之后的lookup |
那么为什么不用以下的
1 | registry.lookup(remoteObject); |
这两个是因为不同的原因,依次单独来讲
实现lookup(object)
lookup只接受String类型的参数,直接传object肯定不行,参考【1】中提及了直接改包,比较直接暴力这里不深入跟进
还有一种是ysomap中重写lookup函数的方式,来看看具体实现
ysomap.core.exploit.rmi.component.Naming#lookup
1 | public static Remote lookup(Registry registry, Object obj) |
对比下sun.rmi.registry.RegistryImpl_Stub$lookup
1 | public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException { |
可以发现:
- ysomap Naming$lookup其实只实现到发送ref.invoke
- 段1通过反射,获取RemoteCall
- 注意段2有给out增加属性enableReplace=false;发送的是Object类型的obj,而不是原来的String
其中enableReplace=false有深意
enableReplace=false
这里很重要
强烈建议阅读参考【1】原文关于enableReplace 这一段,这里就直接引用相关重要结论
我们默认理解为序列化过程是对于我们的恶意object进行writeobject,RMIConnectionImpl_Stub.writeobject()、UnicastRemoteObject.writeobject()那么当然是序列化的。(实际上也可以,在github的Bypass290代码中尝试序列化写入了文件中进行查看,结果也是把正确的ref值写入了,就不贴图了)
但是实际上客户端序列化的过程为:ObjectOutput.writeobject(我们的恶意object)
java.io.ObjectOutputStream#writeObject0
1 | private void writeObject0(Object obj, boolean unshared) |
replaceobject替换的方法具体在sun.rmi.server.MarshalOutputStream#replaceObject中
1 | //var1就是我们想要序列化的类 |
通过bind攻击
理解以上的原理,lookup适用的bind同样适用,ysomap Naming增加同样的bind函数即可。
小结
本节只能算是参考【1】中的jep290 An Trinh方式的复现笔记,其间几个重点类的构造巧妙,只能通过正向调试分析为什么可以,至于逆向分析为什么会想到这个思路,顶礼膜拜,不能望其项背。
还有8u241等知识点未复现,之后有空再作记录。