# 简介
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 反序列化 - 深入 - 上 /](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 反序列化 - 深入 - 上 /# 利用 Object 类型参数](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 反序列化 - 深入 - 下 /# 利用 JRMP 反序列化绕过 JEP290](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