什么是RMI?
在说RMI 之前,需要理解两个名词:对象序列化
、RPC
1. 序列化:
一个类的对象要想序列化成功,必须满足两个条件:
1.该类必须实现 java.io.Serializable 对象。
2.该类的所有属性必须是可序列化的。如果有一个属性不是可序列化的,则该属性必须注明是短暂的。被声明为transient的属性不会被序列化。
如果你想知道一个 Java 标准类是否是可序列化的,请查看该类的文档。检验一个类的实例是否能序列化十分简单, 只需要查看该类有没有实现java.io.Serializable接口。
2. RPC:
RPC即Remote Procedure Call,可以理解成远程过程调用。RPC只是一个概念,不要纠结于他的实现形式,甚至JSON都可以实现一个RPC协议。
RMI:
二者结合,就成了RMI->Remote Method Invocation,即远程方法调用,它可以被看作是RPC的Java版本。它使客户机上运行的程序可以调用远程服务器上的对象。看代码比较好懂。
RMI实现(非Spring方式):
- 实现一个可序列化类。
- 定义一个接口,这个接口需要继承Remote接口,这个接口中的方法必须声明RemoteException异常。
1
2
3
4
5
6import java.rmi.*;
import java.util.List;
public interface SerializeService extends Remote{
List<Serialize> getList() throws RemoteException;
} - 创建一个类实现2中的接口,还需要继承UnicastRemoteObject类,并显示声明无参构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import com.sun.xml.internal.bind.v2.schemagen.xmlschema.List;
public class SerializeServiceImpl extends UnicastRemoteObject implements SerializeService{
public SerializeServiceImpl() throws RemoteException{};
public List<Serialize> getList() throws RemoteException{
List<Serialize> array = new ArrayList<Serialize>();
Serialize ser1 = new Serialize();
ser1.setAge(10);
ser1.setName("lfy");
array.add(ser1);
return array;
}
} - 创建并启动RMI服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class StartService{
public StartService(){};
public static void main(String[] args){
try {
SerializeServiceImpl serializeService = new SerializeServiceImpl();
LocateRegistry.createRegistry(5005);
Naming.rebind("rmi://127.0.0.1:5005/SerializeService", serializeService);
System.out.println("Server Start!");
} catch (Exception e) {
e.printStackTrace();
}
}
} - 创建客户进行RMI调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import java.rmi.Naming;
import java.util.List;
public class Client{
public Client(){};
public static void main(String[] args){
try {
SerializeService serializeService = (SerializeService) Naming.lookup("rmi://127.0.0.1:5005/SerializeService");
List<Serialize> serList= serializeService.getList();
for(Serialize s: serList){
System.out.println("Name: "+s.getName()+" Age: "+s.getAge());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
我的理解:
其实RMI从远程拿到对象,就好像Spring 从Bean里拿到对象一个道理,就是从别的地方拿到需要的数据。RMI通信传输的数据是经过序列化的数据,
具体可看我抓到的包。
图1
JAVA RMI 反序列化远程命令执行漏洞分析 ?
需要知道的背景知识
通过 JNDI 的接口就可以存取 RMI Registry/LDAP/DNS/NIS 等所谓 Naming Service 或 DirectoryService 的内容
JNDI的使用目的,最根本的就是java应用通过一个名字获取其他JVM中的数据。而在提供JNDI服务的服务端应用中,建立了一个类似键值对的形式,存储JNDI的名字和数据的绑定。这就类似于数据库的连接池,不必每次去连接数据库都重新建立一个连接,而是直接从连接池中获取已有连接拿来使用即可,节省了内存同时也优化了效率。
引用网上的一个很形象的解释:1
2jndi就象是url,记录着你要访问的地址
而rmi就象是http或是tcp,是一种连接方式因此,当lookup可控时,我们可以通过JNDI注入的方式实现RCE。
JNDI API 中涉及到的常见的方法与接口的作用,如:Context.lookup
一个简单的JNDI RMI Server demo1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class RmiJndiSever {
public static void main(String[] args) {
try {
//注册RMI服务器端口
LocateRegistry.createRegistry(8080);
//建立RMI服务端接口实现对象
RmiSimple server = new RmiSimpleImpl();
//设置JNDI属性
Properties properties = new Properties();
//RMI的JNDI工厂类
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY , "com.sun.jndi.rmi.registry.RegistryContextFactory");
//RMI服务端的访问地址
properties.setProperty(Context.PROVIDER_URL, "rmi://localhost:8080");
//根据JNDI属性,创建上下文
InitialContext ctx = new InitialContext(properties);
//将服务端接口实现对象与JNDI命名绑定,这个地方写的并不是很规范
//如果在J2EE开发中,规范的写法是,绑定的名字要以java:comp/env/开头
ctx.bind("RmiSimple", server);
System.out.println("RMI与JNDI集成服务启动.等待客户端调用...");
} catch (RemoteException e) {
e.printStackTrace();
} catch (NamingException e) {
e.printStackTrace();
}
}
}
1. RMI反序列化漏洞利用条件
主要有两个条件:
1.存在反序列化传输。比如CVE-2017-3241
2.存在有缺陷的第三方库如commons-collections,或者利用java本身漏洞,
注意rmi可以打服务端registry也可以客户端lookup,两种都会反序列化https://paper.seebug.org/1091/
2. JNDI注入利用条件
- lookup参数可控。
- InitialContext类及他的子类的lookup方法允许动态协议转换
- lookup查找的对象是Reference类型及其子类
- 当远程调用类的时候默认会在rmi服务器中的classpath中查找,如果不存在就会去url地址去加载类。如果都加载不到就会失败。
存在接口可以进行对象反序列化
访问对象可以出网,因为要进行远程类下载(内网中另作讨论)
目标对象中的CLASSPATH中存在Spring-tx-xx.jar有缺陷类的jar包
那Reference为何会找远程的class下载呢?
在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。
至于RMI的反序列化过程,参考这篇https://xz.aliyun.com/t/2223
。同时还可以看一下rmi的原理,和java的代理。
绕过高版本JDK的限制进行JNDI注入利用
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
1 | 攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly 系统属性的限制,相对来说更加通用。 |
通过这篇文章,我们发现,上边我们rmitest1 demo使用的就是通过Reference返回一个对exp对象的引用,然后客户端利用时通过factory将其转为对象实例。
同样通过这篇文章,如果我们把exp类从本地文件中删除,就不能rce了,这是因为我本地8u191版本会限制。用tomcat el即可绕过。还有个问题就是,demo中的代码不会去请求下载class文件,哪怕我把限制的两个属性设置true也不行,这个不懂。。
RMI各种反序列化
这里补充下,我也就不写了,有人写的非常全了。
https://blog.0kami.cn/2020/02/06/rmi-registry-security-problem/
https://xz.aliyun.com/t/7930