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 过滤:
打成 war 包后,lib 下面同时有两个版本的 serialkiller.jar
,0.4 版本和 0.5 版本(0.4 是通过 pom.xml 加进去的,0.5 是编译 https://github.com/ikkisoft/SerialKiller 源码 ,然后直接放到 lib 下的):
二者都能处理 xml 的配置文件,但是默认情况下,规则有些差别:
0.4 版本适配的白名单:
也就是类似这样的 xml :
1 | <whitelist> |
0.5 版本适配的白名单:
也就是类似这样的 xml :
1 | <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.StandardRoot
的 processWebInfLib()
来得到所有 possible jars ,其逻辑是 list /WEB-INF/lib
下的所有内容:
跟进这个 list, 一直到 org.apache.catalina.webresources.DirResourceSet
的 list()
,可以看到其底层是调用了 一个 File
对象的 list()
方法,而由 f.list()
的返回值可以看到 serialkiller-0.4.jar
在 serialkiller-0.5.jar
前面 :
serialkiller-0.5.jar
和 serialkiller-0.4.jar
谁在前,决定了 tomcat 在 加载类的时候优先是用的哪个 jar ,也就是说在本 windows 环境下,会用 serialkiller-0.4.jar
而不是 0.5
linux 环境下
而本环境的 linux 下,由 f.list()
的返回值可以看到 serialkiller-0.5.jar
在 serialkiller-0.4.jar
前面 :
serialkiller-0.5.jar
和 serialkiller-0.4.jar
谁在前,决定了 tomcat 在 加载类的时候优先是用的哪个 jar ,也就是说在本 linux 环境下,会用 serialkiller-0.5.jar
而不是 0.4
因而可以看到,在 windows 和 linux 下,tomcat 加载同名不同版本的 lib 是有差异的,那么在不同发行版本的 linux 中,tomcat 加载同名不同版本的 lib 又是否就表现一致呢?
从 c++ 源码 和 libc 的角度探究 linux 下的类加载差异:
unix 环境,究其底层是调用了 UnixFileSystem
的 list()
:
windows 环境,究其底层是调用了 WinNTFileSystem
的 list()
:
它们都是 native 方法,所以需要去看 c++ 代码:
下载 openjdk 源码,切换版本为 jdk8u181
:
https://github.com/AdoptOpenJDK/openjdk-jdk8u
1 | git checkout jdk8u181-b10 |
阅读 jdk/src/solaris/native/java/io/UnixFileSystem_md.c
的 Java_java_io_UnixFileSystem_list
函数,可以知道是调用了 readdir64_r
实现的目录扫描:
而这个 readdir64_r
实际上就是 readdir_r
,是一个 libc 函数:
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_r
和 readdir
底层类似:
The readdir_r function was invented as a reentrant version of readdir.
readdir_r
是readdir
的可重入版本,线程安全。readdir
因为直接返回了一个 static 的 struct dirent,因此是非线程安全。
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 的差异有关。
写 poc,验证 readdir_r
的特点
用
ls -f
测试也可以
先运行命令行,创建一个 serialkiller-0.4.jar
和 serialkiller-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 |
|
在 Ubuntu 20.04.4 上测试的结果:
可以看到 serialkiller-0.5.jar
在 serialkiller-0.4.jar
前面
在 Ubuntu 20.04.3 上测试的结果:
但是如果在 Ubuntu 20.04.3 中, serialkiller-0.4.jar
在 serialkiller-0.5.jar
前面 :
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 :
可以看到,也是用的 readdir_r
:
所以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 的情况:
20.04.4 的情况:
所以可以做出推测,readdir_r
的结果和宿主机有关,而与镜像无关。
也就是说,对于一个 WEB-INF/lib
下有多个版本不同的相同依赖的linux + tomcat + jdk8 环境,在不同的宿主机中,因为选择加载的 jar 不同,可能表现得也不一样。甚至会导致一个环境打得通,另一个环境打不通的情况,而且不好预测,相当随机。
exploit 打 demo
因为黑名单为空,所以直接打:
exp :
1 | package top.inhann; |
打 ubuntu 20.04.4 :
被拦截,因为白名单为空(即使是 URLDNS 也会被拦截):
打 ubuntu 20.04.3 :
反序列化成功,并回显:
总结
如果一个 linux,用的 jdk8 + tomcat ,而 WEB-INF/lib
下面有多个版本不同的相同依赖,这时候 tomcat 究竟用哪个依赖是 随机的,其底层原因 是 readdir_r
这个libc 函数返回 文件名的顺序是随机的,而 readdir_r
这个libc 函数返回 文件名的顺序 的不同,源于 不同 linux 的 filesystem 的差异。
而且,这个 filesystem 的差异会影响到 docker 容器, readdir_r
在 docker 容器内外 的反应一致,也就是说,即使是相同镜像,在不同宿主机上,也可能表现得不一样。
总之,不要往lib下面放同一个依赖的不同版本。