JNDI注入分析

    渗透测试 lz520520 2年前 (2022-03-20) 742次浏览

    前言

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

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

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

    rmi/ldap client和server之间传输是通过序列化和反序列化传输对象的,这个过程暂且不提。

    这里理解有误

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

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

    jndi+ldap利用

    入口

    LdapCtx#c_lookup

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

    JNDI注入分析

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

    JNDI注入分析

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

    JNDI注入分析

    JAVA_ATTRIBUTES如下

    JNDI注入分析

    三个分支

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

    有三种选择

    第一个分支(反序列化)

    1. 属性里包含javaSerializedData,则进入第一个分支,getURLClassLoader获取类加载器,这里会判断trustURLCodebase是否为true,来选择URLClassLoader还是getContextClassLoader(),这个对于javaSerializedData其实不重要。

    JNDI注入分析

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

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

    JNDI注入分析

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

    JNDI注入分析

    总结

    ldap server需要返回两个属性

    • javaClassName:值无所谓
    • javaSerializedData:存储反序列化利用链
    1. 存在javaRemoteLocation属性,则进入第二个分支,

      decodeRmiObject里仅仅是根据javaClassName和javaRemoteLocation新建一个Reference对象,而且没有初始化Reference的classFactory和classFactoryLocation,导致后续无法利用,所以这里不做进一步分析了。

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

    1. 最后一条分支,虽然没有if判断,但需要调用decodeReference,需要满足objectClass的属性值为javaNamingReference。

      com.sun.jndi.ldap.Obj#decodeReference

    这里javaClassName就是最开始的要求,没有其他作用,然后获取javaFactory,用该值生成一个Reference对象,javaFactory在利用的时候是这样设置的#exp,接着的javaReferenceAddress在利用工具中没有写入,这一块后续再具体分析。后续就是返回Reference对象。

    JNDI注入分析

    JNDI注入分析

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

    LdapCtx#c_lookup

    JNDI注入分析

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

    JNDI注入分析

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

    JNDI注入分析

    JNDI注入分析

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

    JNDI注入分析

    上面是小于1.8.191的,高于这个版本有限制远程类加载,我们看下

    NameManager#getObjectFactoryFromReference

    看起来和原来没有区别,但是loadclass内部做了限制

    JNDI注入分析

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

    JNDI注入分析

    总结

    远程类加载方式,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)

    JNDI注入分析

    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,如果本地没有则会退出

    JNDI注入分析

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

    JNDI注入分析

    调用方法

    JNDI注入分析

    调用堆栈

    JNDI注入分析

    ldap小结

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

    jndi+rmi利用

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

    JNDI注入分析

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

    JNDI注入分析

    RMI反序列化

    所以第一种利用方式也由此诞生

    server端(应该是注册端)在写入头部字节后,直接写入一个反序列化利用链,这段利用是ysoserial里实现的

    JNDI注入分析

    客户端在接收到服务端的数据后,在如下位置触发反序列化

    StreamRemoteCall.class#executeCall()

    JNDI注入分析

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

    JNDI注入分析

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

    JNDI注入分析

    封装后

    JNDI注入分析

    引用类远程加载

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

    com.sun.jndi.rmi.registry.RegistryContext.class#lookup

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

    JNDI注入分析

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

    JNDI注入分析

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

    JNDI注入分析

    接着和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利用。

    JNDI注入分析

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

    JNDI注入分析

    动态类加载

    只能适用于低版本,<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小结

    调用堆栈大致如下

    JNDI与JEP290

    顺便提一下jep290是否对jndi注入有影响,答案是几乎没有。因为jep290只是提供了开启反序列化过滤

    的机制,但默认开启过滤的只有rmi服务端的几个类。而jndi注入是针对客户端的攻击,是不受影响的。

    实际上rmi自身的设计也意味着不可能针对客户端进行反序列化过滤,这种问题可能只能通过security

    manager解决。

    https://mp.weixin.qq.com/s/2IiRHOXvnm3hXylzuiOpow

    总结

    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

    JNDI注入分析

    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

    JNDI注入分析

    三梦师傅总结

    1. jndi rce的利用,rmi协议无论任何版本都是反序列化,只是说低版本在原生jre环境就可以反序列化后直接加载远程字节码进行rce,高版本需要通过tomcat-el的gadget进行触发rce。

    2. jndi rce的利用,ldap协议在低版本不需要通过反序列化,通过ldap服务返回的url,就能在原生jre环境下进行加载远程字节码rce,高版本则需要ldap服务返回tomcat-el的序列化gadget触发rce。

      参考

      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这个属性指定,属性具体在代码中怎么设置呢?

    按照上面这么设置过后,当本地找不到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的安全性。

    Security , 版权所有丨如未注明 , 均为原创丨
    转载请注明原文链接:JNDI注入分析
    喜欢 (0)