攻击JavaRMI概述

2024-03-12 13:40
文章标签 java 概述 攻击 rmi

本文主要是介绍攻击JavaRMI概述,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

编者注:

1.本文作者为默安科技逐日实验室,如需转载或讨论,请联系【默安逐日实验室】公众号。

2.本文仅供学习研究,严禁从事非法活动,任何后果由使用者本人负责。

可能师傅们也遇到过这种情况:因为问题A跟进某个类方法,调试期间遇到一个更纠结的问题B,在费尽周折终于搞清楚后,蓦然回首却忘了自己在干什么(问题A)......

JavaRMI与相关攻击面错综复杂,因此本文仅以时间线梳理各种绕过方法与缓解措施的主要逻辑,而不会详述具体调用栈(但会指出关键类方法)。大家可以自行跟进加深印象,也可以参考其它师傅的文章云调试。

RMI全称是Remote Method Invocation(远程方法调用),可以理解为远程过程调用(RPC)的Java实现版本。它使得应用程序员可以像调用本地方法一样(stub.func(arg)),调用远程主机上提供的方法,方法实际最终在远程主机上执行(serv.func(arg)),原因是本地类封装了与远程类基于序列化的网络通信(socket)。

我们将上述提供业务方法的远程主机称为服务端,将发起方法调用的本地主机称为客户端。服务端会监听在一个随机端口上,将自己的通信地址等信息封装后(stub)由某种途径交给客户端。这种途径就是默认监听在1099端口上的注册端。

RMI全称是Remote Method Invocation(远程方法调用),可以理解为远程过程调用(RPC)的Java实现版本。它使得应用程序员可以像调用本地方法一样(stub.func(arg)),调用远程主机上提供的方法,方法实际最终在远程主机上执行(serv.func(arg)),原因是本地类封装了与远程类基于序列化的网络通信(socket)。

我们将上述提供业务方法的远程主机称为服务端,将发起方法调用的本地主机称为客户端。服务端会监听在一个随机端口上,将自己的通信地址等信息封装后(stub)由某种途径交给客户端。这种途径就是默认监听在1099端口上的注册端。

攻击业务危险方法

如果远程方法实现中存在输入流可控的危险方法:

clazz {func(arg) {exec(arg)}
}

理论上就跟利用Web应用漏洞一样,RMI此时就是一个毫无存在感的中间层。实际情况是基本没有能直接调用到的危险方法,而且也不知道危险方法的调用接口(clazz.func(arg))。

  • java.rmi.server.useCodebaseOnly在7u21|6u45时由默认false改为true,下文均需要被攻击端存在gadgets。另外为了行文流畅,8u241对String类型反序列化作的变动放在文末统一说明。

攻击远程方法参数

当服务端并不存在危险业务方法,但我们可以拿到远程方法接口类(比如开源应用)。发现它接收一个Object类型参数(func(Object arg, ...)),在此处传入恶意payload对象,服务端从网络接收数据并试图反序列化还原对象时,就会触发gadgets。

相比Object类型,远程方法入参更有可能是int、boolean等基本数据类型,或是String以及其它封装类。客户端会根据方法名及参数类型生成哈希,服务端收到这个哈希就能知道调用的是哪一个方法(sun.rmi.server.UnicastServerRef#hashToMethod_Map)。

那么把恶意对象强行塞给非Object参数能否触发反序列化执行呢?当远程方法参数为非基本数据类型时,sun.rmi.server.UnicastRef#unmarshalValue就会进入readObject所在分支。通过修改网络数据、修改字节码、修改内存对象、修改RMI客户端实现 任意一种方法,将方法哈希修改为服务端存在且符合类型要求的远程方法哈希,即可触发强行塞入的恶意对象。

攻击RMI注册端

抛开具体业务远程方法,Registry也提供了基本方法:

  • bind(String, Remote)
  • list()
  • lookup(String)
  • rebind(String, Remote)
  • unbind(String)

除了list方法不会向注册端传递参数,其余四个方法的任意参数都会被注册端接收并readObject(sun.rmi.registry.RegistryImpl_Skel#dispatch),要解决的问题是如何让恶意对象符合类型要求。

BaRMIe采用代理注册端,通过handleData方法替换字节流数据,最终形成bind(payload, null)。

ysoserial.exploit.RMIRegistryExploit采用动态代理,通过createMemoitizedProxy方法封装为Remote,最终形成bind("pwnedXXX", payload)。

我们也可以自己创建一个类并实现Remote, Serializable,将payload对象赋值放入任意类属性。虽然注册端递归反序列化后最终会找不到自定义类报错,但在此之前会先触发gadgets。

在JEP290(8u121,7u131,6u141)加入的sun.rmi.registry.RegistryImpl#registryFilter会限制递归深度、数组长度,并基于白名单递归检查类型,会拦截payload的最终执行。

此外在8u141将dispatch中原来的先readObject再RegistryImpl.checkAccess(默认仅允许注册端与服务端同地址),修正为了先作检查。会使攻击者伪装成服务端向注册端发起bind/rebind/unbind的攻击失效,但不会影响攻击者作为客户端发起lookup。

攻击DGC服务

DGC全称是Distributed Garbage Collection,顾名思义其实就是GC(垃圾回收)的分布式方案。与Registry类似,DGC服务也提供了基本方法dirty()和clean()。客户端需要用到注册端服务端的远程对象时,会通过dirty申请。相应的,当客户端不再需要时则会通过clean注销。

而同样与Registry类似,注册端服务端上的sun.rmi.transport.DGCImpl_Skel#dispatch,会接收DGC客户端经由dirty或clean传过来的部分数据并readObject,要解决的问题是sun.rmi.transport.DGCImpl_Stub#dirty并没有一个参数接口用于传递对象。

对此,ysoserial.exploit.JRMPClient按照DGC通信的固定格式直接走socket通信在相应位置写入了payload,最终形成[0x4a524d49, 2, 0x4c, 0x50| 2, 0, 0, 0, 1, -669196253586618813L, payload]的数据流。

在JEP290加入的sun.rmi.transport.DGCImpl#checkInput逻辑也与之前相同,会拦截payload的最终执行。

攻击JRMP客户端

RMI和DGC服务都是基于JRMP协议通信,就像HTTP应用与TCP之间会有Web服务器处理HTTP协议一样,JRMP也有相应的处理模块。我们将主动发起JRMP请求的一方称为JRMP客户端,将监听JRMP请求的一方成为JRMP服务端。

JEP290主要通过白名单限制了RMI服务端与DGC服务(注册端或服务端)readObject时能使用的类,却没有限制JRMP客户端处理JRMP服务端返回的异常信息readObject(sun.rmi.transport.StreamRemoteCall#executeCall)。

同上文所述,JRMP服务端的sun.rmi.transport.Transport#serviceCall等位置都是直接writeObject(Exception),所以需要实现恶意JRMP服务端在JRMP客户端发起连接时,将payload走异常接口给抛回去。

ysoserial.exploit.JRMPListener就是这种实现,构造为[81, 2, payload]的数据返回给JRMP客户端,使其进入异常处理触发payload。

寻找Gadgets

  • DGCClient implements the client side of the RMI distributed garbage collection system. The external interface to DGCClient is the registerRefs() method. When a LiveRef to a remote object enters the JVM, it must be registered with the DGCClient to participate in distributed garbage collection. When the first LiveRef to a particular remote object is registered, a dirty() call is made to the server-side DGC for the remote object.

可以知道当LiveRef加载进JVM后,会通过registerRefs注册并发起dirty请求。ysoserial.payloads.JRMPClient以sun.rmi.server.UnicastRef#readExternal充当反序列化入口,在其中装填了sun.rmi.transport.LiveRef,借由DGC机制便可主动发起JRMP请求。

如果有地方能够触发这个反序列化入口,就可以让它成为JRMP客户端向恶意JRMP服务端发起请求,进而走没被限制的异常readObject触发最终payload。刚好UnicastRef是RegistryImpl#registryFilter的白名单类,原汤化原食了。

触发反序列化

ysoserial.exploit.RMIRegistryExploit利用的registry.bind(name, remote)在8u141后失效了,ysomap.exploits.rmi.RMIRegistryExploit使用的Naming.lookup(registry, remote)则依然有效。

同样要解决让第一步的payload符合类型要求的问题。要么依托原RMI客户端发起lookup,找一个实现了Remote且能存放LiveRef的类(比如RemoteObjectInvocationHandler),或者利用递归反序列化特性自己构造类;要么重新实现RMI客户端强行发送数据。

  • 前者找类并测试会遇到一个与类型相关很有意思的问题

8u231捕获了在加载Ref(第一步的payload)后会触发的异常,在发起JRMP请求(releaseInputStream)之前清除了Ref。

并且将dirty和clean中的setObjectInputFilter(DGCImpl_Stub::leaseFilter)过滤器提到了JRMP请求发起之后、恶意JRMP服务端最终gadgets触发之前(ref.invoke(call))的位置。

细看可以发现缓解措施针对的都是加载Ref(第一步的payload)之后的过程,当有一个gadgets能在加载自身就触发JRMP请求,就能绕过这些过滤,An Trinhs找到了一条这样的链:

1. UnicastRemoteObject#readObject

2.RMIServerSocketFactory#createServerSocket

3.RemoteObjectInvocationHandler#invoke

利用动态代理机制封装了类,同时gank了本来的方法调用,将其引入invoke。

从OracleJDK-8u241/OpenJDK-8u242开始将多处readObject再转型String的地方修改为了SharedSecrets.getJavaObjectInputStreamReadString().readString,修复了lookup时的反序列化入口,并且影响上文攻击远程方法参数攻击RMI注册端中涉及String类型参数的地方。

反制攻击方

最后稍微聊一下喜闻乐见的反制问题,ysoserial在477ecb8更新了一个无限制的registry.list(),哪怕在8u261中依然是写着赤果果的readObject:

而在那个commit之前,也许作者是想借由原生的registry.bind(name, remote)得到注册端回显,方便攻击时判断gadgets环境,为此其戴了ExecCheckingSecurityManage作为安全措施(/doge),但还是可以通过CC链读写文件等方式间接溯源或者RCE。

参考内容

Attacking Java RMI services after JEP 290

针对RMI服务的九重攻击 - 下

JEP 290: Filter Incoming Serialization Data

Understanding distributed garbage collection

30行代码透彻解析RPC

这篇关于攻击JavaRMI概述的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/801442

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2