简介
PS:下面的客户端攻击、服务端攻击、注册中心攻击都是讨论JEP290未引入之前的情况。实战遇到,先看看java版本,大于等于下面版本的直接看绕过就行了,小于的话看具体情况选择。
Java SE Development Kit 8, Update 121 (JDK 8u121)
Java SE Development Kit 7, Update 131 (JDK 7u131)
Java SE Development Kit 6, Update 141 (JDK 6u141)我们前面认识了RMI,也大致了解了RMI中各个角色受到的威胁。
接着我们就对这些方面的威胁做一次简单的复现,以更好的理解。
这里我用的JDK8u65,之所以用这个版本是因为JDK6u141、JDK7u131、JDK 8u121加入了JEP 290限制,限制了可以反序列化的类。所以在高版本环境下复现是会出问题的。需要做一些绕过,这个我们放在最后面。
简单的说:在JEP290未引入前,我们是可以执行任意Payload的,但在着之后,我们只能通过payload/JRMP来攻击目标,这就要求对方主机到我们主机是网络畅通的。
因此如果目标机子低版本情况下(在JEP290未引入之前),我们可以执行任意的Payload。如果在着之后,直接看绕过利用就行了,其他都没用了。
我们可以根据下图,来决定采取什么攻击方式。
PS:当然,不建议随便连公网上的RMI服务,小心被反打,为什么?往下看就懂了。

参考链接
https://www.anquanke.com/post/id/204740?from=timeline#h3-8
https://zhuanlan.zhihu.com/p/96151046
http://events.jianshu.io/p/7ef5ffa38dfa(JEP290详解)
https://paper.seebug.org/1091/#serverrmi-server
[https://lalajun.github.io/2020/06/22/RMI%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E6%B7%B1%E5%85%A5-%E4%B8%8A/](https://lalajun.github.io/2020/06/22/RMI 反序列化-深入-上/) (必看,有上下两篇)
客户端发起攻击
客户端攻击Registry
PS
,使用bind绑定一个恶意对象到注册中心上。具体漏洞点我们前面也分析过了,这里不在赘述。通常情况下,客户端都是只能从注册中心获取数据的,但唯一的例外就是注册对象(服务)时能从本地绑定对象到注册中心。再结合我们前面所说的,高版本JDK下,注册中心和服务端必须在同一台机子。因此不难得出结论,在高版本JDK下,想要从客户端攻击注册中心不太可能。(一般来说,如果注册中心和服务端在同一台机子上,我们没必要非要攻击注册中心,攻击服务端不就完事了么)
我们可以看看Registry这个类的接口。可以看到就两个接口是接收对象传参的。那么是不是我们只能攻击必须要是Remote类型的Object接口呢?即实际上只有bind、rebind接口才是可以攻击的?
但事实是 RMI注册端没有任何校验,你的payload放在Remote参数位置可以攻击成功,放在String参数位置也可以攻击成功。但如果在Remote参数攻击的话,我们的Payload就需要是Remote类型的。

这里演示的是8u65环境下的攻击。
我们在本地创建注册中心,还是之前那套代码,但我给它引入了commons-collections3.2.1组件。然后客户端的就需要变一下了,这里用的是CC1链(代码就是我之前的代码)。

同时,这里让代码在远程jvm上运行。可以看到,我们最终成功弹出Calc,成功从客户端攻击注册中心。


客户端攻击服务端
场景一:服务端存在一个接收Object参数的远程方法
PS
,传一个恶意对象到服务端上。如果服务器上存在一些危险方法,可以对危险方法进行探测:https://github.com/NickstaDB/BaRMIe虽然我们前面分析了很多,明确知道了服务端是会反序列化客户端传来的数据的,但是实际上不是说随便传什么程序都会走到反序列化代码那边的。在之前是存在很多校验及排错语句的。比如说我们客户端在获取到服务端的某个对象后, 不可能说你能调用它没有的方法对吧。
这里我们本地实现了User接口,然后把setUser方法的第二个参数改成了Object类型,而服务端上setUser的参数是String类型。此时如果我们去调用,就会报错,因为服务端是先去寻找你要执行的方法然后才去反序列化数据的。而你这个方法在服务端是找不到的,所以就抛出错误。

也就是说客户端想要攻击服务端就得有个前提:服务端存在一个接收Object参数的远程方法。
这里我们再到把服务段的setUser的第二个参数改成Object类型,此时我们就能成功触发readObject,从而执行命令。

场景二:绕过Object类型参数(场景一的利用面扩充!!重要!!)
在看文章的时候很多文章提到了即使远程方法不存在Object类型的参数也可以打。但大多讲得很模糊,完全没搞懂什么意思。直到我看到这篇文章才大彻大悟。
[https://lalajun.github.io/2020/06/22/RMI%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E6%B7%B1%E5%85%A5-%E4%B8%8A/#%E5%88%A9%E7%94%A8Object%E7%B1%BB%E5%9E%8B%E5%8F%82%E6%95%B0](https://lalajun.github.io/2020/06/22/RMI 反序列化-深入-上/#利用Object类型参数)
我们前面分析的时,如果我们客户端不管服务段接收的参数类型,强行传个Object过去,就会诱发一个名为method not supported的错误。

起初我以为服务端是在接收到对象后,但后面想明白了,没序列化前就是个String,怎么可能能够计算呢,所以必是在客户端进行计算后,跟着序列化数据传过去的。相关代码在sun/rmi/server/UnicastServerRef类的dispatch方法。
这个method抛出的错误很明显,就是客户端传过来的methodhash和服务段本地的不一致。

而如果我们人为的修改这个hash使其通过一致性检查后,就会进入到unmarshalValue这个方法中,跟进看看。

到这里上面的流程主要就以下步骤:
1.根据传输过来的Method hash,判断本地提供的RMI服务类的方法是否有这个Method hash
2.根据Method hash取到Method类,遍历入参,从输入流按顺序反序列化入参
3.当服务端设定的RMI方法的入参不是基础数据类型时,执行in.readObject就会触发我们的payload
通过上述流程,我们不难发现,如果绕过了method hash检查,并且服务端这个方法的入参类型不是基础类型的话,我们就能通畅的进行反序列化恶意对象。
而有个很关键的点就是String它不是基础类型。换句话说,如果服务段上这个method方法它的入参类型是String,我们强行给它传个Object,它也是能够反序列化成功的(前提是绕过MethodHash检查)。
那么:RMI服务端需要起一个具有Object参数的RMI方法 的利用条件限制 就扩展到了 RMI服务端只需要起一个具有不属于基础数据类型参数的RMI方法(比如String啥的)
攻击原理核心在于替换原本不是Object类型的参数变为Object类型。之前我们修改String接口变为Object,是可以做到修改参数类型,但是那样还会修改method hash。所以这里只能修改底层代码,去替换原本的参数为我们想要的payload。
所以就有以下思路:
- 直接修改rmi底层源码(将java.rmi包的代码复制到新的包中,在其中进行修改)
- 在运行时,添加调试器hook客户端,在序列化对象之前替换对象
- 在客户端使用Javassist工具更改字节码
- 使用代理,来替换已经序列化的对象中的参数信息
RemoteObjectInvocationHandler的重打包
下面我们是通过javaagent hook住java.rmi.server.RemoteObjectInvocationHandler类的InvokeRemoteMethod方法的第三个参数把非Object的改为Object。
这里用如下项目:
https://github.com/Afant1/RemoteObjectInvocationHandler
PS:这里它原本的payload是 URLDNS,我自己给它加了CC1,改起来也不难,就是把自己的Gadge库给引入进去。然后按照它原本的格式输出一个恶意对象就完事了。如果不想折腾,就玩玩作者的URLDNS也是没毛病的。但我觉得我们虽然不会写工具,但起码能做到改工具把,不然真正让你去打站,总不能就只打个URLDNS就够了是把。

如果和我一样加了自己的本地lib的话,pom打包配置请参考https://blog.csdn.net/gaopu12345/article/details/78596830


攻击利用
最后打包成jar包后就可以开始我们的攻击了。这里我们的客户端只需要保证和服务段一样的接口,然后传入正常的数据过去就行。当挂上这个agent后,String类型的参数就会被我们的payload给替换掉。


场景三:远程加载对象(版本要比较低,利用苛刻,以下我没复现成功)
其实这种情况下也是想办法让服务段存在一个有Object参数的方法来打的。
具体怎么实现呢,这里先介绍下codebase
PS:大致原理就是客户端指定一个URL,当远程对象(服务端)尝试调用一个它不存在的类时,此时会通过URL来实例化对象。
具体实现就是如果远程对象存在接收一个对象参数,再执行这个对象的某个方法(如下图,远程对象存在这样也一个方法)。然后客户端传一个服务端上不存在的对象给远程对象。再加上codebase就能实现攻击了。


这里客户端传个了EvilClass对象给Server端,Server找不到这么一个类,就会去Client指定的codebase地址去查找这个类,并执行其中的内容。也就是说这种情况下,根本用不到什么构造反序列化链,只要让这个类是恶意的就完事了。
Java RMI的动态加载类codebase
java.rmi.server.codebase:java.rmi.server.codebase属性值表示一个或多个URL位置,可以从中下载本地找不到的类,相当于一个代码库。代码库定义为将类加载到虚拟机的源或场所,可以将CLASSPATH视为“本地代码库”,因为它是磁盘上加载本地类的位置的列表。就像CLASSPATH”本地代码库”一样,小程序和远程对象使用的代码库可以被视为”远程代码库”。
RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的class文件可以使用http://、ftp://、file://进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,客户端就需要从服务端提供的java.rmi.server.codebaseURL去加载类;对于服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的java.rmi.server.codebaseURL去加载对应的类。客户端与服务端两边的java.rmi.server.codebaseURL都是互相传递的。无论是客户端还是服务端要远程加载类,都需要满足以下条件:
- 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy,这在后面的利用中可以看到。
- 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
注:在JNDI注入的利用方法中也借助了这种动态加载类的思路。
如果本地的ClassPath中找不到就从codebase中加载,那么如果这个codebase是可控的,也就意味着会从攻击者的服务器上去拉取对象,如果这个类是恶意类就可能造成RCE,这也是JNDI与RMI攻击结合的利用场景。
调试了半天,换了一亿个版本也没成功,算了- -,反正鸡肋。
System.setProperty(“com.sun.jndi.rmi.object.trustURLCodebase”,“true”);
场景四:JRMP
其实就是下面高版本JDK下的绕过,高版本能绕过,低版本自然也行。
注册端发起攻击
因为客户端和服务端都需要和注册中心进行通信,所以可以通过恶意的注册中心攻击客户端,也可以攻击服务端
但这两种攻击是一样的,都是他们连接注册中心,注册中心给随便给一个恶意对象就行。而且攻击服务端没什么意义,所以这里只演示攻击客户端。
具体攻击其实就是直接返回恶意数据就完事了,但这就要求我们去实现一个恶意的注册中心,
场景一:直接用ysoserial来完成即可。(所有版本均可反打,只要目标有对应的利用链)
而恶意注册中心这种东西,ysoserial中刚好就有一个JRMPListen,直接用它的就行。
我们只需要运行ysoserial里面的exploit/JRMPListener 类即可。看代码可知,它需要三个参数,第一个是恶意注册中心的监听端口,第二个是用的哪条链,第三个则是需要执行的命令。
PS:也可以直接命令执行java -cp .\ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 “calc”

只要客户端本地有存在漏洞的组件,一旦尝试和我们的注册中心进行数据请求就会被反打。

我们简单的分析下它的JRMPListener源码,从main入手,代码也不复杂。

然后run函数中就是监听,线程等待而已,当有连接,就通过case来排除协议类型。

最后就是去到doCall方法,把payload发过去了。

PS:这个和后面的JEP290绕过息息相关。而且我测试时发现,客户端哪怕是最新的JDK8u341也深受其害。唯一的要求就是需要目标主动连我们的RMI注册中心并执行bind、list、lookup之类的操作。
但这种情况下我们是不是可以用来反打攻击者,比如我们下文中的JEP290绕过就需要攻击者去连接我们的RMI,再或者前面提到过的BaRMIe,这个工具它会尝试枚举我们RMI的对象,这种情况势必会涉及lookup的操作,因此必然会受到我们的攻击。
这件事告诉我们不要乱用RMI利用工具去攻击网上存在的RMI,你永远也不知道你连上的RMI是不是恶意的。
这里因为BaRMle不含CC组件,这里用URLDNS来测试。可以看到我们成功反打了。



再或者含CC组件的ysomap,可以看到,我们kali开启了一个恶意的RMI服务,当使用ysomap去打这个RMI服务的时候直接反制成功。

场景二:标准的注册中心绑定恶意对象(利用有一定的限制)
PS:这里有个很严重的缺陷就是客户端必须去lookup指定恶意对象才行。所以建议还是用yoser去生成JRMPListen来反打客户端。
这里其实也很简单,就是直接返回一个Object对象给目标。但需要注意的是我们cc1生成的恶意对象类型不是Remote,这种类型没法提供远程服务,所以在这里通过动态代理把它变成了Remote远程对象类型。然后才能bind到注册中心。

客户端直接lookup就触发payload了。

服务端发起攻击
其实这里和前面我们使用标准化的注册中心打客户端是一样的,但不同的是,前面注册中心打客户端时,需要客户端去lookup获取远程对象。所以利用起来是更加的苛刻的。(客户端存在和服务端一样的接口,并且客户端调用了服务端的恶意方法)
这里服务端如果需要打成功是需要客户端再往下去调用远程对象的方法,并且这个方法返回一个恶意对象给客户端才行。
如下图,恶意服务端上的这个远程对象的setUser方法返回了一个恶意对象。当客户端调用这个方法后,就会触发payload。


高版本JDK下的绕过(谨慎,有被反打的风险)
摆烂人集合!不想分析原理了,头疼。大概原理就是通过JRMP来绕过的。说白了就是JRMPClient这个payload怎么(封装才能)绕过JEP290的白名单校验。
以下jdk开始引入JEP290
Java SE Development Kit 8, Update 121 (JDK 8u121)
Java SE Development Kit 7, Update 131 (JDK 7u131)
Java SE Development Kit 6, Update 141 (JDK 6u141)但需要小心了,你这种利用有可能会被反打,具体前面说过了。
想分析的跟着大佬学:
[https://lalajun.github.io/2020/06/22/RMI%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E6%B7%B1%E5%85%A5-%E4%B8%8B/#%E5%88%A9%E7%94%A8JRMP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%BB%95%E8%BF%87JEP290](https://lalajun.github.io/2020/06/22/RMI 反序列化-深入-下/#利用JRMP反序列化绕过JEP290)
直接复现怎么绕过利用,大致总结图如下:
其中已知RMI接口的攻击主要问题就是怎么去识别探测,无需多言。https://github.com/NickstaDB/BaRMIe
着重复现最后那个。

JRMP服务端打JRMP客户端(基础知识)
这其实就是接下来绕过的核心路径,其实这就是ysoserial中的exploit/JRMPListen攻击链。
当客户端主动连上一个恶意的注册中心时,它是不受JEP290保护的。并且我测试下,8u341都能利用。
具体就是通过ysoserial启动一个exploit/JRMPListen,然后我们的受害者连上去,Listen就会把恶意对象传个客户端,然后客户端就中招了。其实就是上面的注册中心反打客户端。

看到这里是不是大概有思路了。也就是说如果服务器主动连接exploit/JRMPListen,是不是就必然会被我们漏洞利用攻击了?
因此我们的难点就变成了怎么让受害服务器去主动连接我们的exploit/JRMPListen。
这里我们再引入payload/JRMPClient(见下述PS),只要目标服务器反序列化这个对象就会主动连接外部的RMI注册中心。
而我们的问题就变成了怎么让目标反序列化这个对象,在RMI的漏洞利用中也就是怎么让这个对象绕过JEP290的白名单过滤。
PS:exploit/JRMPListen配合上payload/JRMPClient可以让目标强行与我们的注册中心进行连接。
①攻击者在自己服务器使用exploit/JRMPListener开启一个rmi监听。
②攻击者往存在漏洞的服务器发生payload/JRMPClient,payload中已经设置了攻击者服务器ip及JRMPListener监听的端口,漏洞服务器反序列化该payload后,会去连接攻击者开启的rmi监听exploit/JRMPListener。然后exploit/JRMPListener就会发送预定的恶意payload给连接它的漏洞服务器。
怎么封装payload/JRMPClient以让它绕过JEP290白名单我是懒得分析了,直接用大佬封装好的Payload。
那么最后的问题就是怎么把这个对象发送给目标呢?
通过Bind去发送Payload/JRMPClient(8u141之前)
我们知道RMI服务段和注册端需要在同一台机子上,但是JRMP的反序列化发生在这个地址校验之前,因此这个限制对我们来说没有作用,因此我们可以直接通过bind去把这个对象给发送个注册中心。
此后,在8u141修复中对这个校验提前了,因此通过bind去发生payload不再可行。

其实就是攻击者自己做为服务端,然后把bind这个payload到注册中心。从而实现反序列化。
这里我用的Payload是ysoserial的,已经是帮我们封装好的,能够绕过JEP290的了,直接拿过来用就行。

接着开启我们的exploit/JRMPListener,然后将JRMPClient payload发过去就成功触发反序列化,让目标连接我们的恶意JRMPListen,从而实现JEP290绕过。


通过lookup去发送Payload/JRMPClient(8u231之前)
由于8u141之后修复了服务段和注册中心的地址校验问题,没办法再用Bind了,但我们还可以使用lookup方法来发送JRMPClient Payload。
但在8U231之后,Oracle开始对JEP290拦截的白名单进行增强修复,因此我们封装的JRMPClient无法通过JEP290的拦截了。
这里问题在于Lookup只接收String类型,我们需要想办法,让我们的Lookup能接收Object对象,这里要么实现一个HOOK拦截器,要么重写Lookup。
这里ysomap这个工具就重写了Lookup,我们只需要拿过来用就完事了。
ysomap.exploit.rmi.component.Naming#lookup


通过lookup去发送Payload/JRMPClient(8u241之前)
8u231主要修复的是
sun.rmi.registry.RegistryImpl_Skel#dispatch报错情况消除ref
sun.rmi.transport.DGCImpl_Stub#dirty提前了黑名单
也就是说我们的payload/JRMPClient需要想办法重新封装以绕过8u231的修复。
这里我们利用ysomap中封装好的对象,然后老样子通过lookup给服务器传进去即可。
https://github.com/wh1t3p1g/ysomap
