Tomcat AJP协议漏洞
CVE-2020-1938复现,漏洞利用难度不大,但是还是学到了很多新的东西。
# 简介
AJP协议是一种定向包协议,采用二进制形式代替文本形式,主要是为了提高性能和添加SSL的支持,在tomcat中使用的是AJP1.3,简称为ajp13。
在tomcat的配置文件server.xml
中配置了两种连接方式:
一个是默认开放在8080端口的HTTP Connector,另一个则是默认开放在8009端口的AJP Connertor,而在tomcat中这个协议的监听一直是默认开启的。
tomcat可以通过AJP协议和另一个web容器进行交互通信,比如Apache。tomcat可以作为Servlet或者JSP的容器,但是在处理一些静态资源的速度却不如其他的HTTP服务器(如IIS、Apache)。所以实际应用中常常把tomcat和其他服务器进行集成,对于不支持Servlet、JSP的服务器,可以通过AJP协议把请求转发给tomcat,从而实现优势互补。
正常情况下用户使用客户端通过HTTP协议来和tomcat服务器进行通信,但是也可以通过AJP协议来和tomcat通信,这时候就会由AJP Connertor来处理AJP请求。显然浏览器不直接支持AJP协议,所以要想使用AJP协议,要么通过中间代理服务器进行转发,要么自己根据AJP协议的格式来实现一个客户端。
但是Tomcat AJP协议存在安全漏洞(CVE-2020-1938),攻击者可以通过构造AJP协议请求中的特定属性值,来进行任意文件读取(一般为webapp/ROOT路径下),同时如果AJP服务器存在文件上传接口,则还可能通过漏洞进行文件包含,实现远程代码执行。
漏洞影响范围:
- Tomcat 6.x
- Tomcat 7.x < 7.0.100
- Tomcat 8.x < 8.5.51
- Tomcat 9.x < 9.0.31
# Tomcat架构简单分析
Tomcat架构分析:https://blog.csdn.net/xlgen157387/article/details/79006434
Tomcat Service主要包含两个部分:Conector和Container。
- Connector:主要用于处理连接,并提供socket和request及response直接的转化。
- Container:用于封装和管理Servlet,处理具体的请求。
在Container中有四个子容器:
- Engine:引擎,用来管理多个站点,一个service最多只能有一个Engine。
- Host:代表一个站点,也叫虚拟主机,通过配置Host可以添加站点。
- Context:代表一个应用程序,对应一套程序,或者一个WEB-INF目录以及目录中的web.xml文件。
- Wrapper:每一个Wrapper封装着一个Servlet。
以下面的tomcat文件目录做对照:
Webapps目录代表一个Host,而其下的每个目录都对应着一个Context,其中ROOT目录存放着主应用,其他目录存放着子应用。当我们访问Context的时候,如果是ROOT目录下的,则可以直接通过www.xxx.com/
来访问,而如果要访问其他子目录,则需要通过www.xxx.com/docs
来访问。当然,主应用目录是可以修改的,默认情况下为ROOT目录。
Tomcat处理一个请求的流程大致如下:
- 在获取到TCP/IP数据包的时候,会交给Processor将解析,并封装成我们熟悉的request和response对象,然后传递给下一步处理。
- Engine来进行Host、Context、Mapping中Servlet的匹配。
- Servlet中调用方法(service、doGet、doPost······)来进行处理,并返回结果。
# AJP数据包处理
对于HTTP请求和AJP请求的数据包,在封装成request和response对象之后的流程并无差别,主要区别就是对socket流量处理和Processor解析的过程不同。
org.apache.coyote.Processor
接口提供了这些功能,对于不同的协议,有不同的接口实现类。负责处理AJP请求的实现类为AjpProcessor
:
在AjpProcessor
的service()
方法中调用了prepareRequest()
方法进行数据预处理:
而之后会调用Adapter来将请求交给Container来处理:
AJP协议中的漏洞,就出现在prepareRequest()
方法中。
# 漏洞复现
poc:https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
# 环境搭建
在环境搭建这里我还是踩了一点小坑,先说一些我本地的环境:
- Windows10 专业版
- JDK 8u281
- IDEA专业版 2020.3.2
以下是我搭建出正常复现环境的步骤:
github上下载存在漏洞的Tomcat版本:https://github.com/apache/tomcat/archive/9.0.19.zip
解压,在文件目录中添加pom.xml,改为maven方式构建项目,其中pom.xml文件内容如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat</artifactId> <name>tomcat</name> <version>9.0.19</version> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.ant</groupId> <artifactId>ant</artifactId> <version>1.10.5</version> </dependency> <dependency> <groupId>wsdl4j</groupId> <artifactId>wsdl4j</artifactId> <version>1.6.3</version> </dependency> <dependency> <groupId>org.apache.geronimo.specs</groupId> <artifactId>geronimo-jaxrpc_1.1_spec</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>org.eclipse.jdt</groupId> <artifactId>ecj</artifactId> <version>3.17.0</version> </dependency> <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>4.0.2</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>
在根目录里新建
home
文件夹,把webapps
和conf
文件夹放入home
文件夹中:在IDEA中导入项目,选择我们新建的pom.xml文件:
导入项目之后,将
java
文件夹标记为Sources Root
,把test
文件夹标记为Test Sources Root
:把
home/webapps/examples/WEB-INF/classes/trailers
目录拷贝到test
目录下:把
home/webapps/examples/WEB-INF/classes/util/CookieFilter.java
文件拷贝到test/util
目录下:找到
BootStrap
类的main
方法,运行,这时候应该会出现下面错误:添加VM参数:
-Dcatalina.home={你的项目路径}\home
:在
org.apache.catalina.startup.ContextConfig
类中的configureStart
方法中添加context.addServletContainerInitializer(new JasperInitializer(), null);
,用于初始化JSP解析器:删除
home/webapps/examples
文件夹,然后运行main
方法,访问127.0.0.1:8080
:同时可以看到8009端口已经开放:
# 文件读取
在AjpProcessor#prepareRequest
方法打上断点,调试启动tomcat,并使用POC来发送ajp请求:
在699行出进行了数据包头部信息的读取,当头设置为SC_A_REQ_ATTRIBUTE
的时候,则会读取出n和v:
然后当n不为SC_A_REQ_LOCAL_ADDR
、SC_A_REQ_REMOTE_PORT
、SC_A_SSL_PROTOCOL
的时候,会用v来对n进行赋值:
POC中发送的数据包一共会读取三次:
n = javax.servlet.include.request_uri v = /
n = javax.servlet.include.path_info v = WEB-INF/web.xml
n = javax.servlet.include.servlet_path v = /
对应POC中的这一部分代码:
之后进入getAdapter().service()
方法中,这里可以看到我们请求的URL为/asdf
:
/asdf
这个路由是我们故意构造的一个不存在的路由,当路由无法匹配对应的servlet的时候会由org.apache.catalina.servlets.DefaultServlet
来处理请求,是在{CATALINA_HOME}/conf/web.xml
中默认配置的:
在DefaultServlet
的service()
方法中打下断点,当我们请求方式为GET
方式的时候,存在service()->doGet()->serveResource()
的调用链:
再贴一个调用栈:
在serveResource()
中调用了getRelativePath()
方法:
在这个方法中会读取出我们POC中构造的恶意属性,然后把两个属性拼接成一个path:
然后在serveResource()
方法中会把这个path传入resources.getResource()
方法中造成任意文件读取:
之后会把根据path获取到文件资源序列化输出,这样客户端再根据AJP协议的数据包进行解包,就能读取到文件内容了:
# JSP文件包含
Tomcat会将以.jsp、.jspx
结尾的url交给org.apache.jasper.servlet.JspServlet
来处理:
所以我们可以修改POC中的请求路径:
然后再JspServlet#service
方法中打下断点:
这里同样会获取INCLUDE_SERVLET_PATH
和INCLUDE_PATH_INFO
两个属性,而这两个属性是可通过构造AJP请求数据包来控制的,这两个属性拼接到jspUri
之后,会进入到serviceJsp
方法:
这个方法会把jspUri
(上述为/WEB-INF/testfile
)所表示的文件解析为JSP文件,从而形成文件包含:
值得注意的是,这个所包含的文件和我们请求的url(/asdf/f4de.jsp
)是没有关系的。
# 其他
# 读取其他Context
默认情况下是读取的主目录(ROOT目录)中的文件内容,此外还可以读取其他Context中文件的内容,只需要修改一下请求的url即可(文件包含同理):
# 目录穿越可读性
我们尝试穿越出webapps这个目录,读取其他文件夹中的内容:
DefaultServlet#serveResource
打下断点,在resources.getResource
方法中:
跟进RequestUtil.normalize
:
只要包含/../
,就会返回null
,直接抛出异常:
# 修复
升级Tomcat版本
禁用AJP协议(删除或注释conf/server.xml中对应的内容)
为AJP Connector配置secret,或为AJP设置协议认证凭证
<Connector port="8009"protocol="AJP/1.3" redirectPort="8443"address="YOUR_TOMCAT_IP_ADDRESS" secret="YOUR_TOMCAT_AJP_SECRET"/> <Connector port="8009"protocol="AJP/1.3" redirectPort="8443"address="YOUR_TOMCAT_IP_ADDRESS"requiredSecret="YOUR_TOMCAT_AJP_SECRET" />
# 参考
[攻击Java Web应用 - Java Web安全] (zhishihezi.net) (opens new window)
Tomcat Ajp协议漏洞 | l3yx's blog (opens new window)
IDEA 导入 Tomcat9 源码 - 江湖小小白 - 博客园 (cnblogs.com) (opens new window)
https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
四张图带你了解Tomcat系统架构--让面试官颤抖的Tomcat回答系列!_徐刘根的博客-CSDN博客 (opens new window)
apache tomcat AJP漏洞修复方案(2020-02-21) - Exception List (xieyonghui.com) (opens new window)
- 02
- CommonsBeanUtils04-19
- 03
- 基于Tomcat全局存储进行回显04-16