Tomcat-AJP文件包含漏洞
slug
series-status
status
summary
date
series
type
password
icon
tags
category
Tomcat AJP 文件包含漏洞
CVE Num: CVE-2020-1938
Tags: RCE, Tomcat, 安全配置错误, 文件包含漏洞
环境搭建
- 借助Vulhub搭建基础环境容器
- 在其中配置好ssh等环境,并开启tomcat远程调试(catalina)
🖥️ 环境版本:Tomcat 8.0.50
EXP
- 链接
- 关键程序
复现结果
任意文件读取
- 读取 /WEB-INF/web.xml

- 自己在 /usr/local/tomcat/webapps/ROOT/WEB-INF 路径下写入 a.txt,内容为 Hello River

- 借助AjpShotter进行读取

- 尝试读取 /etc/passwd 失败

RCE
- 首先上传jsp🐴 到ROOT路径下(/usr/local/tomcat/webapps/ROOT/a.jsp)
- 借助AjpShooter执行该后门文件

- 发现成功执行系统命令
任意文件读取漏洞原理分析
关键漏洞点
- tomcat-coyote.jar
- org.apache.ajp.AjpProcessor.java
- 449行
分析过程中执行的命令
步进分析
- AjpProcessor.prepareRequest()方法下的这个位置打断点后步入调试

- 发现这里会将几个参数存入这个HashMap中

- 循环结束时HashMap中的内容

- 借助wireshark抓取exp执行时发出的数据包

可以看到这里发起了针对/WEB-INF/a.txt的访问请求
- 寻找这个数据包最后由哪个servlet接收
- /usr/local/tomcat/conf/web.xml文件中的配置如下


- 根据配置不难看出,其中定义了两个Servlet
- 在用户请求的URI不能和其他的Servlet匹配的时候,就会交给默认的Servlet处理
- 其中,如下图的配置所示,静态资源的请求主要由前者负责,而入jsp文件的请求则交给后者处理

- 回顾抓到的数据包,请求的URI为index,匹配default情况,因此必然会处罚DefaultServlet中的方法
- 在DefaultServlet中的doGet方法处设置断点

<aside>
💯 这里之所以是get方法,是因为在exp中默认选择的是get方法
</aside>
- 跟随函数步进观察代码
- 在这里获取到请求的目标路径

即这里这个路径:

- 这里可以看出来,如果没有传递servlet_path,则会默认添加 ’/’ 到路径中,即访问webapp根路径

- 回到doGet,如果路径为空,则直接重定向

- 随后,在这里获取路径对应的资源

- 进入getResource方法,可以看到,其中比较关键的应该就是这个validate方法

这个方法主要负责校验路径是否有效
- 其中核心就是这个normalize方法

- 而在这部分,针对 ../ 目录穿越的情况进行了判断

<aside>
💯 可能这里会有疑惑:为什么它判断index == 0就可以确定存在呢?如果不在开头呢?
答:因为这里的判断是在循环里,每次检测一部分,检测完某一部分以后就会截取掉那部分。
</aside>
- 判断结束回来,这里如果请求的资源不存在,则会直接返回404,或其他状态码

同时,在这一部分,如果请求中不包含request_uri的话,则会导致漏洞利用失败
- 这一部分判断是否有权限获取对应资源,如果没有则返回403

- 在这一部分开始准备输出

- 最后附上完整的调用链结构

RCE漏洞原理分析
执行RCE对应的EXP
- 执行exp

- 抓包

进入IDEA分析
定位到JspServlet
- 根据前文的分析可知,针对jsp文件的请求会调用JspServlet,因此在该文件的service方法设定断点

- 放行调试至此断点处,随后继续执行,可以看到首先会将servlet_path的值存储到jspUri中

这里可以看到,如果请求中带有path_info的话会将path_info的内容与servlet_path的值进行拼接
- 这里还有一个预编译的函数用来针对含参的请求进行预处理

由于我们这次的请求是不含参数的,因此先跳过此部分
- 随后进入关键的函数serviceJspFile

- 首先生成JspServletWrapper对象

<aside>
💯 这里补充一下JSP是如何转化成Servlet的:
服务端对外提供JSP请求服务的是JspServlet,继承自HttpServlet。核心服务入口在service方法,大体流程如下:
- 首先获取请求的jspUri,如果客户端发起请求:https://xxx.xx.com/jsp/test.jsp,那么获取到的jspUri为:/jsp/test.jsp
- 然后查看缓存(Map结构)中是否包含该jspUri的JspServletWrapper,如果没有就需要创建一个JspServletWrapper并且缓存起来,并调用JspServletWrapper的service方法
- 如果为development模式,或者首次请求,那么就需要执行JspCompilationContext.compile() 方法
- 在JspCompilationContext.compile方法中,会根据jsp文件的lastModified判断文件是否已经被更新(out dated),如果被更新过了,就需要删除之前生成的相关文件,然后将jspLoader置空(后面需要加载的时候如果jspLoader为空,就会创建一个新的jspLoader),调用Compiler.compile方法生成servlet,设置reload为true(默认为true),后面会根据reload参数判断是否需要重新加载该servlet
- 调用JspServletWrapper.getServlet方法获取最终提供服务的servlet,这个过程会根据reload参数看是否需要重载servlet,如果需要重载,那么就会获取jspLoader实例化一个新的servlet(如果前面发现jsp文件过期,那么此时获取的jspLoader为空,则会创建一个新的jspLoader),并且设置reload为false
- 调用servlet的service方法提供服务,如果servlet实现了SingleThreadModel接口,那么会用synchronized做同步控制 </aside>
- 随后调用JspServletWrapper的service方法:

- 进入该方法可以看到在这一步获取到了Jsp转换为的Servlet

- 最后附上RCE的执行调用链

修复建议
- 官网已经发布修复漏洞的Apache Tomcat版本,版本分别为:Tomcat9.0.31、Tomcat8.5.51、Tomcat7.0.100,可以通过Apache Tomcat官网下载相应的版本进行更新;
- 如有更老版本或者无法立即进行版本更新的用户,建议直接关闭AJPConnector,或将其监听地址改为仅监听本机localhost:
- 编辑<CATALINA_BASE>/conf/server.xml,找到如下行(<CATALINA_BASE>为Tomcat的工作目录):
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443/">
- 将此行注释(也可删掉改行) <!--<Connector port=”8009” protocol=”AJP/1.3” redirectPort=”8443/”>-->
- 如果使用了Tomcat AJP协议:
- 建议将 Tomcat 立即升级到 9.0.31、8.5.51 或 7.0.100 版本进行修复,同时为 AJP Connector 配置 secret 来设置 AJP 协议的认证凭证。例如(注意必须将 YOUR_TOMCAT_AJP_SECRET 更改为一个安全性高、无法被轻易猜解的值)
Loading...