当我们谈论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 |
|
如果在本地classpath中找不到我们指定的factory类(1),那么就会去远程codebase(2)去下载class字节码(3)回来并实例化(4)。 图为JDK8u112的代码
若JDK8u112版本中,我们指定codebase为http地址,放置我们构造的恶意类,lookup发起请求即可执行Evil类静态代码块中的代码
1 |
|
修复: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 |
|
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 |
|
分割得到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 |
|
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、JNDI_LDAP的第二种javaReferenceAddress利用方式,对于RefAddr对象的数据构造可参考com.sun.jndi.ldap.Obj#decodeReference
1 |
|
恶意服务端设置三个属性objectClass、javaClassName、javaReferenceAddress
1 |
|
成功在JDK8系列最新版本完成利用
3、Tomcat BeanFactory#getObjectInstance的本地利用方式,可以看到在JDK8系列的最新版本8u341利用成功
1 |
|
5、参考
https://mp.weixin.qq.com/s/Dq1CPbUDLKH2IN0NA_nBDA
https://www.veracode.com/blog/research/exploiting-jndi-injections-java