编号:CNVD-2020-10487/CVE-2020-1938
描述:由于 Tomact AJP 协议的设计缺陷,攻击者可以通过漏洞读取 webapp 目录下的任意文件,或者通过文件包含 getshell。
影响版本:Tomcat 9/8/7/6
Tomcat Connector
Connector 是 Tomcat 用来处理外部连接的核心组件,它将底层的 socket 信道封装成 Request 和 Response 两个对象,再交给 Container(servlet 容器)进行处理。
Connector 的源码在 org.apache.coyote
目录下,默认有两个——分别处理 HTTP 协议和 AJP
协议。
![/ox-hugo/tomcat.png](/ox-hugo/tomcat.png)
Apache Jserv Protocol
AJP 协议可以简单理解为 HTTP 协议的二进制性能优化版本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用,默认开放在 8009 端口。
浏览器无法直接处理 AJP 协议,所以通常用于 Tomcat 和其他 web 服务器的通信,比如 Apache 的 proxy_ajp 模块就可以代理 AJP 协议。
![/ox-hugo/ajp.png](/ox-hugo/ajp.png)
POC
部署好环境后,在 github 找个脚本复现一下漏洞。
![/ox-hugo/2021-03-23_22-53-01_screenshot.png](/ox-hugo/2021-03-23_22-53-01_screenshot.png)
可见通过 AJP 协议请求 /asdf
路径,却读取了 web.xml 文件的内容。
用 wireshark 看一下 AJP 请求的内容:
![/ox-hugo/2021-03-23_22-54-13_Snipaste_2021-03-23_18-36-54.png](/ox-hugo/2021-03-23_22-54-13_Snipaste_2021-03-23_18-36-54.png)
红框中的三个由用户控制的参数就是导致文件读取的罪魁祸首,下面通过调试 Tomcat 看看怎么处理它们的。
文件读取
处理请求的类是 org.apache.coyote.ajp.AbstractAjpProcessor
,进入 prepareRequest
方法,这里是漏洞的入口点,此时的调用栈如下:
![/ox-hugo/2021-03-23_21-47-36_screenshot.png](/ox-hugo/2021-03-23_21-47-36_screenshot.png)
来到关键的 while 语句,在这里对上面提到的三个参数进行设置:
![/ox-hugo/2021-03-23_23-01-39_screenshot.png](/ox-hugo/2021-03-23_23-01-39_screenshot.png)
request.setAttribute()
就是简单的 HashMap 赋值操作,循环结束后 attributes 属性如下:
![/ox-hugo/2021-03-23_23-04-45_screenshot.png](/ox-hugo/2021-03-23_23-04-45_screenshot.png)
AJP Connector 封装好 request 对象后,会将其传给 Container 处理。
![/ox-hugo/2021-03-24_14-13-34_screenshot.png](/ox-hugo/2021-03-24_14-13-34_screenshot.png)
Container 即 servlet 容器,相关设置可以在 web.xml 文件找到,下面是默认开启的两个 servlet 容器:
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
...
</servlet>
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
...
</servlet>
<!-- The mapping for the default servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- The mappings for the JSP servlet -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
当请求目标的后缀是 jsp 或者 jspx 时由 JspServlet 处理请求,其他情况则由 DefaultServlet
处理。因此为了确保由 DefaultServlet 处理我们的请求,而使用 /asdf
这种大概率不存在的路径。
代码执行到 DefaultServlet#doGet 方法中:
![/ox-hugo/2021-03-24_14-46-56_screenshot.png](/ox-hugo/2021-03-24_14-46-56_screenshot.png)
一直跟进到 getRelativePath 方法,关键代码如下:
![/ox-hugo/2021-03-24_14-51-56_screenshot.png](/ox-hugo/2021-03-24_14-51-56_screenshot.png)
上图的第一个 if 语句拼接了 path_info 和 servlet_path 参数得到 /WEB-INF/web.xml
,而进入 if 的条件是 request_uri 不为空(不一定要是 /
),这就是 poc 设置该参数的原因。
获取路径后调用 getResource 方法读取文件:
![/ox-hugo/2021-03-24_16-01-27_screenshot.png](/ox-hugo/2021-03-24_16-01-27_screenshot.png)
![/ox-hugo/2021-03-24_16-02-07_screenshot.png](/ox-hugo/2021-03-24_16-02-07_screenshot.png)
红框中的 validate 方法用来检查文件的路径,确保路径以 /
开头,且不会跨越到 webapp
目录以外。代码如下:
![/ox-hugo/2021-03-24_16-09-10_screenshot.png](/ox-hugo/2021-03-24_16-09-10_screenshot.png)
从上图可以看到代码会校验路径是否以 /
开头,且标准化了路径分隔符,但是怎么防止目录穿越呢?在处理路径分隔符的部分调用了 RequestUtil#normalize 方法,我们跟进一下:
![/ox-hugo/2021-03-24_16-40-44_screenshot.png](/ox-hugo/2021-03-24_16-40-44_screenshot.png)
方法开头会替换 windows 系统的 \\
,然后根据情况在开头和末尾加上 /
,在后面执行以下
while 语句:
while (true) {
int index = normalized.indexOf("/../");
if (index < 0) {
break;
}
if (index == 0) {
return null; // Trying to go outside our context
}
int index2 = normalized.lastIndexOf('/', index - 1);
normalized = normalized.substring(0, index2) + normalized.substring(index + 3);
}
上述代码会将 /../
和前一个目录名抵消,如果最后 /../
出现在开头,即尝试逃出
webapp 目录,就会返回 null。理解这段代码的关键在于理解 lastIndexOf 方法,它返回第一个参数字符串最后出现的位置,第二个参数是表示位置的整数,可以理解为”只搜索该位置前的内容“。
"aaabbbaaa".lastIndexOf("aaa"); // 6
"aaabbbaaa".lastIndexOf("aaa", 4); // 0
校验完路径后,返回 getResource 方法读取文件资源。
![/ox-hugo/2021-03-24_16-59-36_screenshot.png](/ox-hugo/2021-03-24_16-59-36_screenshot.png)
文件包含
前面说过 Tomcat 默认的 servlet 容器有两个,当访问的是 jsp 文件时会由 JspServlet 来处理请求。这个处理实际就是解析 jsp 文件,但前提是这个 jsp 文件是存在的,所以首先要想办法上传到 webapp 目录下。
我们先在 webapp 目录下创建一个 test.jsp:
<%Runtime.getRuntime().exec("calc.exe");%>
修改一下 poc,发送的 AJP 请求如下:
![/ox-hugo/2021-03-24_18-13-11_screenshot.png](/ox-hugo/2021-03-24_18-13-11_screenshot.png)
AJP Connector 的处理和之前类似,我们直接跟进到 JspServlet 类中。这次处理请求的方法是 service,关键代码如下:
![/ox-hugo/2021-03-24_18-20-01_screenshot.png](/ox-hugo/2021-03-24_18-20-01_screenshot.png)
这里拼接 path_info 和 servlet_path 参数得到文件路径,接着调用 serviceJspFile 方法解析目标 jsp 文件。
![/ox-hugo/2021-03-24_18-23-41_screenshot.png](/ox-hugo/2021-03-24_18-23-41_screenshot.png)
所以找到目标文件的关键在于 path_info 和 servlet_path 参数,如果上传的 jsp 文件在 upload
目录下,servlet_path 就要设置为 /upload/
,而 URL 的路径并不重要,只要后缀是 jsp 或者
jspx 即可。
再跟进一下 serviceJspFile 方法:
![/ox-hugo/2021-03-24_18-30-29_screenshot.png](/ox-hugo/2021-03-24_18-30-29_screenshot.png)
在 JspServletWrapper#service 方法中编译好目标 jsp 文件,获取对应的 servlet 对象,再执行它的 service 方法弹出计算器。
![/ox-hugo/2021-03-24_18-36-19_screenshot.png](/ox-hugo/2021-03-24_18-36-19_screenshot.png)
总结
导致文件读取漏洞的是 AJP 请求中的三个用户可控参数:
- javax.servlet.include.request_uri:不为空即可
- javax.servlet.include.path_info:文件名
- javax.servlet.include.servlet_path:根路径
而当请求资源是 jsp 或 jspx 文件时,由于 JspServlet 类会编译执行目标文件,导致文件包含漏洞,不过前提是能把恶意文件上传到 webapp 目录下。