0x01.JNDI
JNDI是啥东西可以看看相关文章,或者问问LLM,这里跳过,我也讲不清楚。
0x02.JNDI注入
如下是一个JNDI注入代码,恶意LDAP服务用marshalsec启动。尝试调试一下java.naming.InitialContext#lookup做了什么。
import javax.naming.*; |

首先调用InitialContext#getURLOrDefaultInitCtx解析JDNI原生支持的协议,这里即为LDAP,除此之外,还包括常见的RMI、DNS等等。而下面这部分充分体现了JNDI的SPI(Service Provider Interface),最终根据scheme(ldap)获取到ldapURLContext类。
protected Context getURLOrDefaultInitCtx(String name) |

往下调用ldapURLContext#lookup,传入的name不能带URL查询参数,最后调用父类GenericURLContext#lookup。

继续调用PartialCompositeContext#lookup,传入参数为恶意的class名称,这里即为ExploitClass。

进一步调用ComponentContext#p_lookup。

此方法中会进入case 2分支,表示正常状态,没有遇到隐式下一个命名系统,调用当前上下文的具体实现方法 c_lookup,传入头部名称进行查找。

首先向恶意LDAP服务器发起一次真实的搜索请求,过滤器是(objectClass=*)(匹配所有对象)。doSearchOnce表示只执行一次搜索,不处理分页或引用跳转。同时保存服务器返回的响应控制控件resControls。往后处理搜索状态与提取属性。
往下的操作非常关键,com.sun.jndi.ldap.Obj.JAVA_ATTRIBUTES[2]为javaClassName,var4临时存储从LDAP服务器查找到的条目属性集合,内容如下图,其中有javaClassName,因为据RFC 2713规范,当一个LDAP节点用于存储 Java对象时,javaClassName是一个必填的核心标识属性,用于记录该Java对象的全限定类名,那么就会执行其中的分支代码。
protected Object c_lookup(Name var1, Continuation var2) throws NamingException { |


进入com.sun.jndi.ldap.Obj#decodeObject,首先获取CodeBase地址:http://127.0.0.1:8888/。然后进入三个判断分支,第一个对应javaSerializedData、第二个对应javaRemoteLocation,LDAP会走else分支,代表JNDI Reference 引用,下面是几种属性的对应:
- JAVA_ATTRIBUTES[0]:objectClass
- JAVA_OBJECT_CLASSES[2]:javaNamingReference
- JAVA_OBJECT_CLASSES_LOWER[2]:javanamingreference
会调用com.sun.jndi.ldap#decodeReference

else { |
在com.sun.jndi.ldap#decodeReference中会返回一个Reference对象。也就是marshalsec构造的恶意Reference。其中的那个classFactoryLocation成员就是很多文章说的那个JNDI注入原理相关的:本地CLASSPATH找不到就提取的这个成员变量加载。往下分析会发现这个成员变量是会被当作CodeBase远程加载恶意类的。

返回后,进入javax.naming.spi.DirectoryManager#getObjectInstance。

其中会调用javax.naming.spi.NamingManager#getObjectFactoryFromReference方法。之前的那个Reference实例就会传入到这个方法中。

最后进入javax.naming.spi.NamingManager#getObjectFactoryFromReference,会先在本地加载恶意类ExploitClass,显然是null,然后在往下,根据CodeBase远程加载恶意类。

利用RMI协议最后也会走到上面的方法,所以原理都差不多,暂时不分析了。
本次调试的jdk版本为8u66,并且使用的LDAP协议,所以能够成功加载。官方防止JNDI注入,推出了一系列限制(来源: https://hasegawaazusa.github.io/jndi-injection-note.html ):
- 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协议的攻击途径也给禁了。
在jdk 8u191调试发现确实会判断trustURLCodebase,默认为false。

对于RMI,在其com.sun.jndi.rmi.registry.RegistryContext#decodeObject就拦了。

看到RMI的判断,其实还是有绕过方法的嘛:
- Reference对象为null
- Reference对象调用getFactoryClassLocation()为null
- trustURLCodebase为true
第二种方法是有可能的,javax.naming.Reference#getFactoryClassLocation就是返回Reference的classFactoryLocation成员变量,如果此值为空就可以Bypass。
那么我就需要构造本地classFactoryLocation的Reference对象,能用到的一个本地类是org.apache.naming.factory.BeanFactory,该类存在于Tomcat依赖中,添加pom如下:
<dependency> |
这时候Server代码这样构造:
package org.Server; |
这时候就会调用javax.naming.spi.NamingManager#getObjectInstance,在调用到javax.naming.spi.NamingManager#getObjectFactoryFromReference。
在getObjectFactoryFromReference就和之前分析LDAP协议调用一样了,只是在classFactoryLocation为null的情况下不会直接loadClass(这样会直接被trustCodeBase拦死),会返回classFactory的实例,这里就是BeanFactory。

返回后调用BeaconFactory#getObjectInstance方法,

在此方法中就会执行构造的payload,一个EL表达式。

利用本地类来绕过的种类还有很多包括:JavaBeanObjectFactory、FactoryBase等等。