0x01 漏洞背景
CVE-2020-1938 是 Tomcat-Ajp 协议漏洞分析,Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。
0x02 影响版本
- Apache Tomcat 9.x < 9.0.31
- Apache Tomcat 8.x < 8.5.51
- Apache Tomcat 7.x < 7.0.100
- Apache Tomcat 6.x
0x03 环境搭建
0x1 JDK 安装
cp -r jdk-8u161-linux-x64.tar.gz /usr/local/java
cd /usr/local/java
tar -zxvf jdk-8u161-linux-x64.tar.gz
在/etc/profile文件中写入
export JAVA_HOME=/usr/local/java/jdk1.8.0_161
export JRE_HOME=/$JAVA_HOME/jre
export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
执行生效
source /etc/profile
0x2 Tomcat 安装
tar -zxvf apache-tomcat-8.5.30.tar.gz
在/etc/profile文件中写入
export CATALINA_HOME=/usr/local/tomcat/apache-tomcat-8.5.30
export CLASSPATH=.:$JAVA_HOME/lib:$CATALINA_HOME/lib
export PATH=$PATH:$CATALINA_HOME/bin
执行生效
source /etc/profile
启动
catalina.sh start
0x3 开启Tomcat调试
在tomcat bin 文件夹下的catalina.sh中添加如下代码
export JAVA_OPTS='-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005'
# OS specific support. $var _must_ be set to either true or false.
Tomcat 调试原理
分为两种情况:
- 调试器开端口,远程JVM连接本地端口
- 远程JVM开放端口,调试器连接
总的来说,两个VM之间通过debug协议进行通信,然后以达到远程调试的目的。两者之间可以通过socket进行通信。其中,调试的程序常常被称为debugger, 而被调试的程序称为 debuggee。两种调试情况如下图所示:
关于调试协议JDWP这里先不讲,等有机会在进行讲解。
0x4 添加tomcat源码
在调试过程中,intellij反编译代码会有出入,直接下载tomcat相对应版本的源代码
https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.30/src/apache-tomcat-8.5.30-src.zip
将文件夹中的java 添加到intellij中,就可以对着源代码进行调试了
0x5 开启Intellij调试
设置Use module classpath 为项目根目录,设置调试端口等信息,如下图所示:
0x03 知识补充
0x1 Tomcat Connector
Apache Tomcat服务器通过Connector连接器组件与客户程序建立连接,Connector表示接收请求并返回响应的端点。即Connector组件负责接收客户的请求,以及把Tomcat服务器的响应结果发送给客户。在Apache Tomcat服务器中我们平时用的最多的8080端口,就是所谓的Http Connector,使用Http(HTTP/1.1)协议.
在conf/server.xml文件里,他对应的配置为
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
而 AJP Connector,它使用的是 AJP 协议(Apache Jserv Protocol)是定向包协议。因为性能原因,使用二进制格式来传输可读性文本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。
Ajp协议对应的配置为
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
下图是tomcat 服务器开放连接端口情况。
0x2 Tomcat Servlet
在tomcat conf/web.xml中配置着tomcat的路由处理,主要有两个servlet分支
这个规则匹配/后没有后缀的
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
这个规则匹配路径中有.jsp的路由
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
0x3 Tomcat 请求处理
该图介绍了Tomcat内部处理HTTP请求的流程
- 用户发送请求至8080端口,被Connector获取后,Connector中的Processor用于封装Request,Adapter用于将封装好的Request交给Container。
- Connector把该请求交给Container中的Engine来处理,并等待Engine的回应。
- Engine 获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
- Engine搜索对应的主机,/test匹配到Context,
- path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类(匹配不到指定Servlet的请求对应DefaultServlet类)
- Wrapper是最底层的容器,负责管理一个Servlet。构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost(),执行业务逻辑、数据存储等程序。
- Context把执行完之后的HttpServletResponse对象返回给Host
- Host把HttpServletResponse对象返回给Engine
- Engine把HttpServletResponse对象返回Connector
- Connector把HttpServletResponse对象返回给客户Browser
0x04 漏洞分析
该漏洞通过AJP协议端口触发,正是由于上文所述,Ajp协议的请求在Tomcat内的处理流程与我们上文介绍的Tomcat处理HTTP请求流程类似。我们构造两个不同的请求,经过tomcat内部处理流程,一个走default servlet(DefaultServlet),另一个走jsp servlet(JspServlet),可导致的不同的漏洞。
0x1 文件读取漏洞
# 请求url
req_uri = '/asdf'
# AJP协议请求中的三个属性
javax.servlet.include.request_uri = '/'
javax.servlet.include.path_info = 'WEB-INF/web.xml'
javax.servlet.include.servlet_path = '/'
在搭好的环境上,设置断点
Step 1 AjpProcessor->service()->prepareRequest()
进入prepareRequest函数,该函数处理Requests请求,重点在这一块
在该case分支request.setAttribute(n,v )
函数是解析函数,通过三次循环可见下图效果
通过三个循环将attributes变量赋值成如下所示:
随后将请求传给CoyoteAdapter,对request进行封装,将请求抓发给Container:
通过多级反射和调用,到达Servlet路由分发
Step 2 DefaultServlet-> service() -> doGet()
从CoyoteAdapter到达doGet的调用链
Servlet 的service函数根据请求的方法,调用不同的处理函数如下图匹配Get 方法:
1588562393145.png
Step 3 getRelativePath()
从get函数进入后执行serveResource函数
其中有个关键的函数getRelativePath
protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
String servletPath;
String pathInfo;
if (request.getAttribute("javax.servlet.include.request_uri") != null) {
pathInfo = (String)request.getAttribute("javax.servlet.include.path_info");
servletPath = (String)request.getAttribute("javax.servlet.include.servlet_path");
} else {
pathInfo = request.getPathInfo();
servletPath = request.getServletPath();
}
StringBuilder result = new StringBuilder();
if (servletPath.length() > 0) {
result.append(servletPath);
}
if (pathInfo != null) {
result.append(pathInfo);
}
if (result.length() == 0 && !allowEmptyPath) {
result.append('/');
}
return result.toString();
}
从代码中可以看出request.getAttribute的变量正好是我们POC中的变量,POC中的参数代入getRelativePath()方法,RequestDispatcher.INCLUDE_REQUEST_URI的值为’/’,不为空。pathInfo和servletPath参数的值拼接成result,getRelativePath()方法将result返回,返回内容为:’/WEB-INF/web.xml’。
Step 4 getResource() -> validate() -> normalize()
serveResource()方法继续往下,可以看到这行代码:
该函数功能从字面意思上就是获取内容
进入了valiate方法,该方法为路径检测方法,其中主要调用了normalize方法,重点关注该方法:
if (normalized.endsWith("/.") || normalized.endsWith("/..")) {
normalized = normalized + "/";
addedTrailingSlash = true;
}
// Resolve occurrences of "//" in the normalized path
while (true) {
int index = normalized.indexOf("//");
if (index < 0) {
break;
}
normalized = normalized.substring(0, index) + normalized.substring(index + 1);
}
// Resolve occurrences of "/./" in the normalized path
while (true) {
int index = normalized.indexOf("/./");
if (index < 0) {
break;
}
normalized = normalized.substring(0, index) + normalized.substring(index + 2);
}
// Resolve occurrences of "/../" in the normalized path
while (true) {
int index = normalized.indexOf("/../");
if (index < 0) {
break;
}
过滤掉了请求中的路径穿越符号 /…/,也就导致了该漏洞只能读取webapps目录下的文件。
Step5 ServletOutputStream.write()
经过Tomcat内部流程处理,经过Tomcat的Container和Connector,最终返回给客户端。
特别注意
关键参数req_uri决定了我们可以读取webapps下其他目录的文件。当为’asdf’时无法匹配到webapps下的路径,所以路由到tomcat默认的ROOT目录;二来是为了让tomcat将请求流到DefaultServlet,从而触发漏洞。
真实文件 | 请求路径 |
---|---|
webapps/manager | manager/asdf |
webapps/ROOT | /asdf |
0x2 文件包含漏洞
该漏洞的触发与上个相似,只不过servlet的分支不同,该漏洞走的是JspServlet,所有的jsp文件的路由
将漏洞利用代码修改为
Step 1 JspServlet -> service() -> serviceJspFile()
从CoyoteAdapter进行分发,这里分发到jspservlet路由,因此我们要在jspservlet上下断点
0x05 后续
- 自己编写ajp协议
- tomcat web服务器交互原理
- java 反射调用链分析
0x06 参考链接
- https://xz.aliyun.com/t/7683
- https://www.anquanke.com/post/id/199448#h2-7
- https://www.freebuf.com/column/227973.html
- https://www.jianshu.com/p/f902ac5d29e4