SpringBoot框架下的JNDI利用法探索

前言

JNDI注入在高版本下显得比较鸡肋,不过基于本地Factory的利用方式还是值得学习的。


Tomcat-BeanFactory

比较出名的可利用Factory,而且Tomcat使用范围比较广。

该类中的getObjectInstance函数可以调用一个对象的函数,要求满足三个条件。

  • 要有public的无参构造函数:
1
Object bean = beanClass.getConstructor().newInstance();
  • 要调用的函数为public:
1
2
3
Method method = forced.get(propName);
...
method.invoke(bean, valueArray);
  • 要调用的函数有且只有一个String类型的参数
1
2
3
4
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
...
forced.put(param, beanClass.getMethod(setterName, paramTypes));
  • 其实还有一些隐含的条件,比如它的bean在通过无参构造函数实例化后,到调用该函数之间没有其他操作,也就是说可控输入就只剩下参数了(或许可以通过多次调用提前控制一些成员)。又比如它无法进行链式调用,所以反射链难以实现,要找到利用方式基本只有那种输入命令->执行,或者ClassLoader的奇妙玩法了。

存在漏洞或危险操作的依赖

通过ELProcessor、GroovyShell、Yaml、MVEL、Xstream、bsh等存在漏洞的工具的玩法。

Mlet

MLet是一个ClassLoader,继承自URLClassLoader,其public函数addURL可以通过一个字符串URL定义远程资源:

1
2
3
4
5
6
7
8
9
public void addURL(String url) throws ServiceNotFoundException {
try {
URL ur = new URL(url);
if (!Arrays.asList(getURLs()).contains(ur))
super.addURL(ur);
} catch (MalformedURLException e) {
...
}
}

远程类需要加载并初始化才能执行其中的静态代码块,问题在于我们通过BeanFactory实例化并调用的Mlet是一个与环境隔离的ClassLoader,我们无法通过这里之外的加载/实例化点完成利用,因为环境中使用的ClassLoader不是这个Mlet。

而在Mlet类中,可以调用的loadClass函数来自父类的父类ClassLoader,而loadClass在加载失败时会调用findClass从资源中定义类:

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
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}


protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
}
...

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

...
}
}
...
}
}

// Mlet
protected Class<?> findClass(String name) throws ClassNotFoundException {
return findClass(name, currentClr);
}

Class<?> findClass(String name, ClassLoaderRepository clr)
throws ClassNotFoundException {
Class<?> c = null;
MLET_LOGGER.logp(Level.FINER, MLet.class.getName(), "findClass", name);
// Try looking in the JAR:
try {
c = super.findClass(name);
...
}
}

// URLClassLoader
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
}
...
} else {
return null;
}
}
}, acc);
}
...
}

然而loadClass的第二个参数固定为false,也就是说仅仅是加载也无法执行静态代码块,必须要走到实例化的步骤才行。而ClassLoader中自然是不会有实例化类的操作的,所以这种方式也就走不通了。

NashornScriptEngineFactory

有无参构造函数:

1
2
public NashornScriptEngineFactory() {
}

有危险函数:

1
2
3
public ScriptEngine getScriptEngine(String... args) {
return this.newEngine((String[])Objects.requireNonNull(args), getAppClassLoader(), (ClassFilter)null);
}

可以链式利用:

1
new NashornScriptEngineFactory().getScriptEngine("js").eval("java.lang.Runtime.getRuntime().exec('calc')");

然而BeanFactory似乎实现不了链式调用,寄。

Mvel

一种表达式,需要依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.mvel/mvel2 -->
<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.4.14.Final</version>
</dependency>

MVEL类没有无参构造函数,但是有很多静态public函数,比如eval、evalToString和evalToBoolean,可以在mvel2包里面其他public类中找一找有没有调用这些静态函数的地方。

单纯用文本搜索做起来会比较麻烦,因为调用了eval的地方实在有点多,看起来很麻烦,如果用codeQL等工具污点分析就会方便很多,详见参考文章。

ShellSession类有一个无参构造函数:

1
2
3
public ShellSession() {
...
}

其exec函数只有一个String类型参数:

1
2
3
4
5
6
public void exec(String command) {
for (String c : command.split("\n")) {
inBuffer.append(c);
_exec();
}
}

其_exec函数会调用内部定义好的Command们的execute函数:

1
2
3
try {
commands.get(inTokens[0]).execute(this, passParameters);
}

PushContext的execute函数中会调用MVEL类的eval函数:

1
2
3
4
public Object execute(ShellSession session, String[] args) {
session.setCtxObject(MVEL.eval(args[0], session.getCtxObject(), session.getVariables()));
return "Changed Context";
}

MemoryUserDatabaseFactory

继承了ObjectFactory接口,其getObjectInstance函数中会实例化一个MemoryUserDatabase对象,该对象可以设置几个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
MemoryUserDatabase database = new MemoryUserDatabase(name.toString());
RefAddr ra = null;

ra = ref.get("pathname");
if (ra != null) {
database.setPathname(ra.getContent().toString());
}

ra = ref.get("readonly");
if (ra != null) {
database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
}

ra = ref.get("watchSource");
if (ra != null) {
database.setWatchSource(Boolean.parseBoolean(ra.getContent().toString()));
}

然后调用其open和save函数,分别可以触发xxe和tomcat下的rce漏洞。

JDBC

详见参考文章。


参考

https://xz.aliyun.com/t/10829

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


SpringBoot框架下的JNDI利用法探索
http://yoursite.com/2022/03/03/SpringBoot框架下的JNDI利用法探索/
作者
Aluvion
发布于
2022年3月3日
许可协议