LOFTER for ipad —— 让兴趣,更有趣

点击下载 关闭
JNDI注入原理及利用以及RMI使用

前言

本篇主要讲述了,RMI的简单使用和JNDI注入产生的原理,不涉及其他方面,使用的jdk版本是jdk1.7,了解RMI使用,有助于理解漏洞原理。

Java RMI概念

Java RMI 指的是远程方法调用 (Remote Method Invocation)。它是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。

在Java中,只要一个类继承了java.rmi.Remote接口,即可成为存在于服务器端的远程对象,供客户端访问并提供一定的服务。JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口”(扩展 java.rmi.Remote 的接口)中指定的这些方法才可远程使用。


编写一个RMI的步骤

1. 定义一个远程接口,此接口需要继承Remote

2. 开发远程接口的实现类

3. 创建一个server并把远程对象注册到端口

4. 创建一个client查找远程对象,调用远程方法

一个Hello Word的远程调用实例

定义一个远程接口

编写RMI应用的第一步就是先定义远程接口。远程接口必须继承java.rmi.Remote接口,并且声明自己的远程方法。为了处理远程方法发生的各种异常,每一个远程方法必须抛出一个java.rmi.RemoteException异常。

public interface HelloWorld extends Remote {
            String sayHello() throws RemoteException;
}

这个远程接口只定义了一个远程方法 sayHello(),远程方法在调用的时候有可能失败比如发生网络问题或者server挂掉,此时远程方法会抛出RemoteException异常。

开发接口的实现类

开发接口的实现类,即具体的远程对象,在远程对象中实现远程接口中定义的方法。

public class HelloWroldImpl implements HelloWorld {
        public String sayHello() throws RemoteException {
                return "hello world!";
            }
}

创建一个Server并把对象注册到端口

在server端只需要做两件事:

  1. 创建并导出远程对象

  2. 用Java RMI registry 注册远程对象

下面是一个server端的程序:

public class RMIServer {

public static void main(String[] args) {

    try {

        HelloWord hello=new HelloWordImpl();

        HelloWord stub=(HelloWord)UnicastRemoteObject.exportObject(hello, 9999);

        LocateRegistry.createRegistry(1099);

        Registry registry=LocateRegistry.getRegistry();

        registry.bind("helloword", stub);

        System.out.println("绑定成功!");

    } catch (RemoteException e) {

        e.printStackTrace();

    } catch (AlreadyBoundException e) {

        e.printStackTrace();

    }

}

}

关于创建和导出远程对象

HelloWord stub=(HelloWord)UnicastRemoteObject.exportObject(hello, 9999);

Server端的main方法在创建一个远程对象来提供服务时,此远程对象必须被导出才能被远程调用者调用。静态方法UnicastRemoteObject.exportObject()负责导出我们定义好的远程对象,并用任意一个tcp端口来接收远程方法调用,同时,它还会返回一个存根,这个存根将会发送给client端进行调用。当exportObject()方法被执行后,运行时会在一个新的Server Socket或共享Server Socket上进行监听,来接收对远程对象的远程调用。返回的存根对象和远程对象继承的是同一套remote接口(为了实现代理模式),并且还它还包含了供client端口访问的主机IP和端口信息。(这里很关键,记住存根这个概念

用Java RMI registry注册远程对象

Registry registry=LocateRegistry.getRegistry();

registry.bind( "helloword", stub);

为了使client能够调用远程对象的方法,client必须持有远程对象的存根,为此,Java RMI 提供了registry API 可以允许应用程序把一个名称远程对象的存根绑定在一起,这样client就可以通过这个绑定的名称很方便的查找到需要调用的远程对象了,在这里可以把registry看做是一个名称服务,实现了工厂模式(提供具体的远程对象)和代理模式(代理server端具体处理client端的请求)。


一旦远程对象在server端导出并注册,client就可以通过绑定的名称获得远程对象的引用,然后调用远程方法。


静态方法Registry registry=LocateRegistry.getRegistry()会返回一个实现了java.rmi.registry.Registry接口的存根,并且在服务器本机的端口(默认是1099)上进行注册,返回的registry存根通过调用bind()方法在registry中把一个字符串名称和远程对象存根绑定在一起。

创建一个client查找远程对象,调用远程方法

public class RMIClient {

  public static void main(String[] args) {

         try {

              Registry registry = LocateRegistry.getRegistry(1099);

              HelloWord hello = (HelloWord) registry.lookup( "helloword");

              String ret = hello.sayHello();

              System. out.println( ret);

        } catch (RemoteException e) {

               e.printStackTrace();

        } catch (NotBoundException e) {

               e.printStackTrace();

        }

  }

客户端首先通过LocateRegistry.getRegistry("localhost")方法获得registry的存根,然后再执行registry存根的lookup()方法从服务器registry中获得远程对象的存根,最后客户端在远程对象存根上执行sayHello()方法。

这里有个方法,我也研究了下,LocateRegistry.getRegistry(),这个方法有几个重载方法,可以直接传入远程服务器ip,也就是LocateRegistry.getRegistry("127.0.0.1"),这样默认连接的端口就是1099。也可以直接写端口LocateRegistry.getRegistry(1099),这样就是默认查找本地的rmi服务器。最后是两个参数的方法LocateRegistry.getRegistry("127.0.0.1", 1099),可以指定ip和端口获取registry存根。

整个过程可以描述为:

  • 客户端通过远程对象存根中的IP和端口打开一个服务器连接,并且序列化请求数据

  • 服务器端接收请求并且转发请求到远程对象调用服务方法,并且序列化运行结果发送给客户端

  • 客户端接收数据反序列化,把最终结果返回给调用者


结果测试

启动server,然后在启动client,控制台打印:

Hello Word!

Java RMI中用到的设计模式

Java RMI中用到了经典的工厂模式和代理模式,先介绍下Java RMI应用的一些角色:        

    1.server:生产各种远程对象,也就是攻击端

    2.client:通过命名服务器rmiregistry获取远程对象的存根,也就是受害端

    3.rmiregistry:具体处理client与server的交流


JNDI注入

现在让我们回到JNDI注入,我也是建议初学者能够去学习下RMI的工作原理,有助于理解JNDI注入漏洞,因为JNDI注入漏洞的核心就是RMI服务,当然也还有其他的一些服务,如LDAP等等,这里我只是针对RMI展开的。

JNDI - Java Naming and Directory Interface 名为 Java命名和目录接口,具体的概念还是比较复杂难懂,具体结构设计细节可以不用了解,简单来说就是 JNDI 提供了一组通用的接口可供应用很方便地去访问不同的后端服务,例如 LDAP、RMI、CORBA 等。

常用代码格式:

String jndiName = ...;//指定需要查找name名称

Context context = new InitialContext();//初始化默认环境

DataSource ds= (DataSourse)context.lookup(jndiName);//查找该name的数据

RMI格式:

Context var1 = new InitialContext();

DataSource var2 = (DataSource)var1.lookup("rmi://127.0.0.1:1099/aa");

所谓的JNDI注入就是当上文代码中jndiName这个变量可控时,引发的漏洞,它将导致远程class文件加载,从而导致远程代码执行。

我们先看payload,分为服务端(攻击端)和客户端(受害端),还有执行命令的class文件

服务端,如图


客户端,如图

编译前的class文件,如图

这里需要注意的是,因为漏洞触发环境是在jdk1.7,所以需要安装jdk1.7,然后将EvilObject.java进行编译,编译时,需要把包名给去除掉,否则会报错。然后将class和java文件都放到搭建好的http服务器上,我是放在本地服务器测试的

主要是需要class文件,我只是顺便都放在一起,如果不把这俩个文件单独拿出来,可能远程资源下载测试就会失败,因为,编译器会读取本地的文件,那样就没有意义了。

现在我们开始测试,我红色标记的地方都是比较重要的步骤

先运行RMIServer

可以看到,程序已经运行起来了,然后我们来启动客户端

可以看到,我们的EvilObject被执行了,弹出了计算器

漏洞原理

服务端我们暂且可以理解为,注册了一个RMI服务,然后绑定了一个存根,就是前面讲的远程对象,只不过这个存根是一个Reference对象,后面会讲到。

我们来看一下客户端执行流程

Context ctx = new InitialContext();    初始化了一个InitialContext对象,这是程序的入口。

然后调用了lookup方法,我们进入到类里看看

public Object lookup(String name) throws NamingException {
        return getURLOrDefaultInitCtx(name).lookup(name);
}

可以看到我们传入的字符串先被带入了getURLOrDefaultInitCtx(name),我们进去看看

可以看到,因为NamingManager.hasInitialContextFactoryBuilder()返回的是false,所以不会进入if判断,接着往下走,String scheme = getURLScheme(name);,这里的getURLScheme(name),其实就是对字符串进行截取,截取从第一个字符到:的所有字符,不包括:,如图

说白了就是截取协议头

因为schema不为null,所以进入Context ctx = NamingManager.getURLContext(scheme, myProps);方法,我们进去看看

可以看到schema和environment被传进了getURLObject(scheme, null, null, null, environment);

这里schema的值就是rmi,environment是一个Hashtable集合,size为0,我们继续跟进

可以看到又被带进了,ObjectFactory factory = (ObjectFactory)ResourceManager.getFactory(
    Context.URL_PKG_PREFIXES, environment, nameCtx,
"." + scheme + "." + scheme + "URLContextFactory", defaultPkgPrefix);

这里的URL_PKG_PREFIXES = "java.naming.factory.url.pkgs",environment为0,nameCtx=null,schema拼接后=".rmi.rmiURLContextFactory",defaultPkgPrefix = "com.sun.jndi.url"

看到这里大家应该能明白为什么要截取协议头了吧,不同的协议调用不同的工厂类,这也是JNDI的通用性

继续跟进

由于这个方法代码比较多,所以只截取关键的代码,getProperty方法返回值为null,所以facProp=null,跳过if判断,进入else,赋值facProp="com.sun.jndi.url"

这里的key是需要注意的,key=".rmi.rmiURLContextFactory com.sun.jndi.url",再看下一段代码

这里我们只关注三处,className="com.sun.jndi.url.rmi.rmiURLContextFactory",其实就是重新拼接了一下

然后本地加载这个类,实例化返回一个factory对象,最后返回这个对象,返回给ObjectFactory factory = factory对象;,可能需要再倒着往回推,因为不为null,所以进入try代码块,return factory.getObjectInstance(urlInfo, name, nameCtx, environment);

我们先理一下参数,urlInfo=null,name=null,nameCtx=null,environment大小为0,然后我们进入getObjectInstance方法看看

注意,我们进入的是rmiURLContextFactory.class因为,之前返回的是这个factory对象,因为传入的var1为null,进入if判断,返回return new rmiURLContext(var4);,其实就是返回了一个rmiURLContext对象

然后反回到return factory.getObjectInstance(urlInfo, name, nameCtx, environment);调用处,再返回到Object answer = getURLObject(scheme, null, null, null, environment);调用处,往下看

if (answer instanceof Context) {
    return (Context)answer;
} else {
    return null;
}

因为answer是rmiURLContext对象,所以属于Context类,成立,返回answer到Context ctx = NamingManager.getURLContext(scheme, myProps);

然后接着往下

if (ctx != null) {
    return ctx;
}

因为不为null,所以getURLOrDefaultInitCtx方法直接返回rmiURLContext对象,也就相当于调用了rmiURLContext对象的lookup方法,我们进去看看

可以看到,类里面没有这个方法,那应该是在父类里面,继续跟进

可以看到,的确是父类的方法,这里的this.getRootURLContext(var1, this.myEnv)调用的是rmiURLContext的方法,如图

我的理解是,这是一个对我们传入的字符串进行分割的过程,分别取出,ip、端口、还有aa也就是绑定存根时用的名字,如图

然后带入new RegistryContext(var3, var4, var2);,返回一个RegistryContext对象,这个是用于获取RMI注册服务的,也就是用来获取存根的

进入ResolveResult(var10, var11)方法

可以看到将名称和RegistryContext对象封装到了这个类里面,然后返回到ResolveResult var2 = this.getRootURLContext(var1, this.myEnv);这里

往下走,Context var3 = (Context)var2.getResolvedObj();获取RegistryContext对象

var4 = var3.lookup(var2.getRemainingName());,调用RegistryContext对象的lookup方法,查找"aa",也就等同于我们前面rmi的使用方法,registry.lookup( "aa");

进入RegistryContext的lookup方法

可以看到调用了RegistryImpl_Stub去获取服务端registry注册的远程对象,也就是refObjWrapper对象,赋值给var2,继续跟进decodeObject方法

可以看到获取到的就是ReferenceWrapper_Stub对象也就是存根,里面包含了远程服务器的ip和端口等信息

Object var3 = Reference对象

然后带入NamingManager.getObjectInstance(var3, var2, this, this.environment);

这里var3 = Reference对象,var2="aa",this=RegistryContext对象,this.environment大小为0

进入方法,如图

这里的2和3都是可以触发漏洞的,这里我们说的是2

ref的值为Reference对象,通过ref.getFactoryClassName()获取类名EvilObject,然后进入if判断,传入getObjectFactoryFromReference(ref, f),跟进

主要看红色标记的地方,会先尝试从本地查找EvilObject这个类文件,因为本地没有所以clas为null

然后获取codebase为我们写的地址https://127.0.0.1/evil/,然后尝试从远程地址下载EvilObject类文件,此时clas的值为EvilObject类对象,最后实例化类对象,触发构造方法里面的命令,弹出计算器

到这里分析就结束了,以下是借鉴的博客

Java RMI原理与使用

JNDI注入原理及利用

推荐文章
评论(0)
分享到
转载我的主页