Author: lz520520@深蓝攻防实验室

前言

之前没有具体跟过JNDI注入的流程,以及一些JDK限制绕过姿势,所以这里详细记录下这个过程。

首先JNDI注入主要通过rmi和ldap利用,分为三种利用方式,第一种仅限于低版本,后续会一个个调试

  1. rmi/ldap 请求vps远程加载恶意class,不需要本地依赖
  2. rmi/ldap 请求vps直接反序列化gadgets执行代码
  3. rmi/ldap 请求vps调用本地工厂类来执行代码

rmi client和server之间确实是通过序列化传输数据的,但ldap不是,就是ldap标准协议传输。

这里调试使用了jdk1.8.201/1.8.131/1.8.20做测试

jndi+ldap利用

入口

LdapCtx#c_lookup

通过this.doSearchOnce请求ldap server获取LdapResult

LdapResult如下,server返回了两个属性,javaserializeddata和javaclassname,这是是忽略大小写的。

进一步会判断属性里是否有JAVA_ATTRIBUTES[2]=javaClassName,有则进一步调用com.sun.jndi.ldap.Obj#decodeObject用于解码对象,所以为啥server需要返回一个无关紧要的javaclassname,就是这里需要判断

JAVA_ATTRIBUTES如下

JAVA_ATTRIBUTES

0 = "objectClass"
1 = "javaSerializedData"
2 = "javaClassName"
3 = "javaFactory"
4 = "javaCodeBase"
5 = "javaReferenceAddress"
6 = "javaClassNames"
7 = "javaRemoteLocation"


JAVA_OBJECT_CLASSES

0 = "javaContainer"
1 = "javaObject"
2 = "javaNamingReference"
3 = "javaSerializedObject"
4 = "javaMarshalledObject"


JAVA_OBJECT_CLASSES_LOWER

0 = "javacontainer"
1 = "javaobject"
2 = "javanamingreference"
3 = "javaserializedobject"
4 = "javamarshalledobject"

三个分支

com.sun.jndi.ldap.Obj.class#decodeObject解析如下,emmm反编译有些问题,var1值和var2有复用情况
有三种选择

static Object decodeObject(Attributes var0) throws NamingException {
        String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4])); // javaCodeBase

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

第一个分支(反序列化)

if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) { // javaSerializedData
                ClassLoader var3 = helper.getURLClassLoader(var2);
                return deserializeObject((byte[])((byte[])var1.get()), var3);
            }
  1. 属性里包含javaSerializedData,则进入第一个分支,getURLClassLoader获取类加载器,这里会判断trustURLCodebase是否为true,来选择URLClassLoader还是getContextClassLoader(),这个对于javaSerializedData其实不重要。


这里的trustURLCodebase其实就是jdk高版本限制JNDI注入的系统变量com.sun.jndi.ldap.object.trustURLCodebase

private static final String trustURLCodebase = (String)AccessController.doPrivileged(new PrivilegedAction<String>() {
        public String run() {
            return System.getProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");
        }
    });

接着通过deserializeObject来使用上面获取到的类加载器对javaSerializedData的值进行反序列化,从而触发反序列化利用链。

ldap server对应处理如下,添加javaClassName和javaSerializedData,序列化gadget存储在javaSerializedData里

总结
ldap server需要返回两个属性

  • javaClassName:值无所谓
  • javaSerializedData:存储反序列化利用链

  1. 存在javaRemoteLocation属性,则进入第二个分支,
    else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) { // javaRemoteLocation
                 return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2); // javaClassName
             }
    
    decodeRmiObject里仅仅是根据javaClassName和javaRemoteLocation新建一个Reference对象,而且没有初始化Reference的classFactory和classFactoryLocation,导致后续无法利用,所以这里不做进一步分析了。

第三个分支(引用类远程加载)

  1. 最后一条分支,虽然没有if判断,但需要调用decodeReference,需要满足objectClass的属性值为javaNamingReference。
    var1 = var0.get(JAVA_ATTRIBUTES[0]); // objectClass
    return var1 == null || 
     !var1.contains(JAVA_OBJECT_CLASSES[2]) // javaNamingReference
     && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2); // javanamingreference
    
    com.sun.jndi.ldap.Obj#decodeReference
    这里javaClassName就是最开始的要求,没有其他作用,然后获取javaFactory,用该值生成一个Reference对象,javaFactory在利用的时候是这样设置的#exp,接着的javaReferenceAddress在利用工具中没有写入,这一块后续再具体分析。后续就是返回Reference对象。

Obj.decodeObject获取到Reference对象后,会通过DirectoryManager.getObjectInstance进行实例化
LdapCtx#c_lookup

javax.naming.spi.DirectoryManager#getObjectInstance会调用javax.naming.spi.NamingManager#getObjectFactoryFromReference获取ObjectFactory对象

javax.naming.spi.NamingManager#getObjectFactoryFromReference先会用this.getContextClassLoader()加载reference对象里的类名对应的本地类,如果找不到本地类,就会getFactoryClassLocation()获取之前javaCodeBase里的URL,通过URLClassLoader来进行远程类加载,最后调用无参构造方法实例化。

远程类加载,会自动根据类名添加.class后缀

上面是小于1.8.191的,高于这个版本有限制远程类加载,我们看下
NameManager#getObjectFactoryFromReference
看起来和原来没有区别,但是loadclass内部做了限制

需要设置系统变量com.sun.jndi.ldap.object.trustURLCodebase=true

总结
远程类加载方式,ldap server需要返回的属性

  1. javaClassName:任意值
  2. javaCodeBase:远程类加载地址,http://x.x.x.x/
  3. objectClass: 固定值为javaNamingReference
  4. javaFactory: 远程类加载的类名,如exp,http server上就需要放置一个exp.Class

第一个分支(本地工厂类)

在通过javaSerializedData进行反序列化时,如果本地没有利用链就无法利用,但这里其实还有另外一个思路,就是找本地的工厂类,这个类首先要实现ObjectFactory接口,并且其getObjectInstance方法实现中有可以被用来构造exp的逻辑,org.apache.naming.factory.BeanFactory类是tomcat容器catalina.jar里的,被广泛使用,getObjectInstance中会实例化beanClass并反射调用其方法。

  1. Obj#decodeObject返回对象,这里无反序列化利用链触发
  2. 接着会进入NamingManager#getObjectFactoryFromReference,如果是Reference对象,则会返回一个ObjectFactory对象(这里实现类是BeanFactory)
  3. 进而调用factory.getObjectInstance实例化BeanFactory对象里的beanClass
  4. 实例化beanClass后,会获取Reference对象里的forceString属性值
  5. 将属性值会以逗号和等号分割,格式如param1=methodName1,param2=methodName2
  6. 接着会反射调用beanClass对象里名为methodName1的方法,并传入参数,限定参数类型为String,参数通过Reference对象里param1属性获取。

简单来讲,原先server返回一个反序列化利用链,而现在本地构造不成利用链,就通过非反序列化方式执行,条件是

  1. ObjectFactory的实现类,其getObjectInstance方法里有可被用来构造exp的逻辑。

org.apache.naming.factory.BeanFactory就是一个,而BeanFactory是用来实例化beanClass的,所以还需要再找一个类,而这个类又有条件限定

  1. 本地classpath里存在
  2. 具有无参构造方法
  3. 有直接或间接执行代码的方法,并且方法只能传入一个字符串参数。

根据这些限定,可以找到tomcat8里的javax.el.ELProcessor#eval(String) 以及springboot 1.2.x自带的groovy.lang.GroovyShell#evaluate(String)

ldap server实例化ResourceRef传入ObjectClass(org.apache.naming.factory.BeanFactory)和beanClass(如javax.el.ELProcessor)

  1. forceString:paramxxxxx=method
  2. paramxxxxx: 执行代码

这里只是简单总结了下,具体内容可参考这篇文章https://www.cnblogs.com/Welk1n/p/11066397.html

PS: 简单记录下调试点
LdapCtx #c_lookup:1085, -> DirectoryManager#getObjectInstance:194 (factory.getObjectInstance) -> BeanFactory#getObjectInstance:193,
获取beanClass,如果本地没有则会退出

通过forceString值,并通过等号(61)分割值,进而反射获取方法

调用方法

调用堆栈

ldap小结

调用堆栈和触发点大致如下

com.sun.jndi.ldap.LdapCtx#c_lookup
    com.sun.jndi.ldap.Obj#decodeObject
        com.sun.jndi.ldap.Obj#deserializeObject             // 1 触发反序列化利用链 3 获取本地工厂类的payload
        com.sun.jndi.ldap.Obj#decodeReference               // 2 获取远程类加载的Reference对象    
    javax.naming.spi.DirectoryManager#getObjectInstance
        javax.naming.spi.NamingManager#getObjectFactoryFromReference        // 2 远程类加载
        factory.getObjectInstance                                           // 3 本地工厂类利用链触发

jndi+rmi利用

RMI在数据传输过程中是通过序列化传输,所以就有了反制server端的情况出现。

而ldap就是标准的ldap协议进行通信,协议交互不需要序列化

RMI反序列化

所以第一种利用方式也由此诞生
server端(应该是注册端)在写入头部字节后,直接写入一个反序列化利用链,这段利用是ysoserial里实现的

客户端在接收到服务端的数据后,在如下位置触发反序列化
StreamRemoteCall.class#executeCall()

executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
lookup:119, RegistryImpl_Stub (sun.rmi.registry)
lookup:132, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)

流量,client发送一个Call消息,远端恢复一个ReturnData消息。

上述反序列化利用链用BadAttributeValueExpException封装了一层,其实不用封装也能触发,但作者估计是为了隐藏实际报错,封装了一个异常类,如下是未封装,提示rmi反序列化异常

封装后

引用类远程加载

和ldap类似,也只能适用于低版本,<8u113

com.sun.jndi.rmi.registry.RegistryContext.class#lookup
第一种方法就是在this.registry.lookup直接触发反序列化,但通过远程类加载方式,那么这里会返回一个ReferenceWrapper对象

该对象里封装着Reference,可以看到和ldap里一样的

接着调用com.sun.jndi.rmi.registry.RegistryContext#decodeObject,步入NamingManager.getObjectInstance

接着和ldap差不多的步骤,javax.naming.spi.NamingManager#getObjectFactoryFromReference里调用URLClassLoader进行远程类加载

在jdk大于8u113时,com.sun.jndi.rmi.registry.RegistryContext#decodeObject里增加了trustURLCodebase判断,只有判断通过才会按照原来流程调用NamingManager.getObjectInstance。
而ldap是com.sun.jndi.ldap.Obj.class#decodeObject,所以该修复并不影响ldap,直到8u191之后,在类加载器里做了限制,才减缓ldap利用。

RMI服务端,由于RMI是可以直接序列化对象进行传输,所以直接写入一个Reference对象进行序列化传输。

动态类加载

只能适用于低版本,<8u121

RMI核心特点之一就是动态类加载,实现效果和引用类加载差不多

看似这个利用很简单,而且听起来很普遍的样子,其实这个利用是有前提条件的

  1. 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy。
  2. 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

所以这个方法就不做演示了。

本地工厂类

这个不赘述了,和ldap基本一样调用NamingManager#getObjectFactoryFromReference获取工厂类后,通过factory.getObjectInstance触发利用

rmi小结

调用堆栈大致如下

com.sun.jndi.rmi.registry.RegistryContext#lookup
    sun.rmi.registry.RegistryImpl_Stub#lookup //  
        sun.rmi.transport.StreamRemoteCall#executeCall() //  1 触发反序列化利用链  2、3 反序列化获取ReferenceWrapper对象
    com.sun.jndi.rmi.registry.RegistryContext#decodeObject
        javax.naming.spi.NamingManager#getObjectInstance
            javax.naming.spi.NamingManager#getObjectFactoryFromReference  // 2 远程类加载
            factory.getObjectInstance        // 3 本地工厂类利用链触发

总结

jndi的利用方式主要就是上面三种
jdk低版本 ldap<1.8.191、rmi < 1.8.113,可使用远程类加载方式;
高版本通过trustURLCodebase限制了远程类加载,从而衍生出了两种其他姿势;
ldap通过javaSerializedData直接返回一个恶意的序列化数据触发反序列化利用链,或找寻本地可利用工厂类,通过getObjectInstance来实现恶意利用;
rmi可直接传输序列化对象,从而直接触发反序列化利用链,或者生成ReferenceWrapper对象,触发本地工厂类利用。

PS:
rmi的远程类加载也是先生成的ReferenceWrapper对象,然后后续触发。
rmi还有一种动态类加载的方式,暂且不提。
ldap在高版本两个方式其实都需要通过反序列化,只是一个是在反序列化点直接构造gadget触发执行,另一个是反序列化时不构造传统的yso利用链,而是生成一个有效的恶意Reference对象,在后续的操作中在触发利用,其实也算是gadget,算一种后反序列化利用。

ldap利用原理可从两个角度分类

  1. 按照执行触发点分类:远程类加载和本地工厂类,均会生成Reference对象,然后在javax.naming.spi.DirectoryManager#getObjectInstance才触发利用;反序列化,在生成如Reference对象时直接触发反序列化利用链。
  2. 按照是否反序列化分类:直接反序列化和本地工厂类利用,均是通过javaSerializedData反序列化;远程类加载无需触发反序列化

rmi利用原理分类

  1. 按照反序列化和后反序列化分类:远程类加载和本地工厂类利用均是通过反序列化生成恶意ReferenceWrapper对象,在javax.naming.spi.NamingManager#getObjectInstance才触发利用;直接反序列化在生成如ReferenceWrapper对象时直接触发反序列化利用链。

使用场景
远程类加载:rmi<8u113、ldap < 8u191等低版本可使用,无需本地依赖
反序列化:暂时无jdk版本限制,但需要本地有利用链
本地工厂类:暂时无jdk版本限制,但需要本地有相应恶意工厂类

PS: 这里rmi-jndi不太准确,CVE应该写的8u113,但好像存在绕过,最终版本是8u121

rmi引用类远程加载是在decodeObject里受com.sun.jndi.rmi.object.trustURLCodebase=false属性的限制。

但是java.rmi.server.useCodebaseOnly没限制。如下,所以为啥rmi最后限制版本是8u121,因为trustURLCodebase方案修复只是在decodeObject里转换成Reference对象时限制,而rmi本身就可以进行远程类加载,这就导致8u121又发布了新的类加载修复,限制动态类加载。

  1. JDK 5U45、6U45、7u21、8u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true
  2. JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false
  3. JDK 11.0.1、8u191、7u201、6u211 com.sun.jndi.ldap.object.trustURLCodebase 默认为false

参考

https://zhuanlan.zhihu.com/p/375732935
https://y4er.com/post/attack-java-jndi-rmi-ldap-1/
https://y4er.com/post/attack-java-jndi-rmi-ldap-2/
https://paper.seebug.org/1091/
http://rui0.cn/archives/1338
https://threedr3am.github.io/2020/03/03/%E6%90%9E%E6%87%82RMI%E3%80%81JRMP%E3%80%81JNDI-%E7%BB%88%E7%BB%93%E7%AF%87/

补充

java.rmi.server.useCodebaseOnly
RMI核心特点之一就是动态类加载,如果当前JVM中没有某个类的定义,它可以从远程URL去下载这个类的class,动态加载的对象class文件可以使用Web服务的方式进行托管。这可以动态的扩展远程应用的功能,RMI注册表上可以动态的加载绑定多个RMI应用。对于客户端而言,服务端返回值也可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,则同样需要有运行时动态加载额外类的能力。客户端使用了与RMI注册表相同的机制。RMI服务端将URL传递给客户端,客户端通过HTTP请求下载这些类。
关于rmi的动态类加载,又分为两种比较典型的攻击方式,一种是大名鼎鼎的JNDI注入,还有一种就是codebase的安全问题。
前面大概提到了动态类加载可以从一个URL中加载本地不存在的类文件,那么这个URL在哪里指定呢?其实就是通过java.rmi.server.codebase这个属性指定,属性具体在代码中怎么设置呢?

System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");

按照上面这么设置过后,当本地找不到com.axin.hello这个类时就会到地址:http://127.0.0.1:8000/com/axin/hello.class下载类文件到本地,从而保证能够正确调用
前面说道如果能够控制客户端从哪里加载类,就可以完成攻击对吧,那怎么控制呢?其实codebase的值是相互指定的,也就是客户端告诉服务端去哪里加载类,服务端告诉客户端去哪里加载类,这才是codebase的正确用法,也就是说codebase的值是对方可控的,而不是采用本地指定的这个codebase,当服务端利用上面的代码设置了codebase过后,在发送对象到客户端的时候会带上服务端设置的codebase的值,客户端收到服务端返回的对象后发现本地没有找到类文件,会去检查服务端传过来的codebase属性,然后去对象地址加载类文件,如果对方没有提供codebase,才会错误的使用自己本地设置的codebase去加载类。

看似这个利用很简单,而且听起来很普遍的样子,其实这个利用是有前提条件的

  1. 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置java.security.policy。
  2. 属性 java.rmi.server.useCodebaseOnly 的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖