CVE-2020-1938 是 Tomcat-Ajp 协议漏洞分析,Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。
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
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
在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 调试原理
分为两种情况:
总的来说,两个VM之间通过debug协议进行通信,然后以达到远程调试的目的。两者之间可以通过socket进行通信。其中,调试的程序常常被称为debugger, 而被调试的程序称为 debuggee。两种调试情况如下图所示:
关于调试协议JDWP这里先不讲,等有机会在进行讲解。
在调试过程中,intellij反编译代码会有出入,直接下载tomcat相对应版本的源代码
https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.30/src/apache-tomcat-8.5.30-src.zip
将文件夹中的java 添加到intellij中,就可以对着源代码进行调试了
设置Use module classpath 为项目根目录,设置调试端口等信息,如下图所示:
Apache Tomcat服务器通过Connector连接器组件与客户程序建立连接,Connector表示接收请求并返回响应的端点。即Connector组件负责接收客户的请求,以及把Tomcat服务器的响应结果发送给客户。在Apache Tomcat服务器中我们平时用的最多的8080端口,就是所谓的Http Connector,使用Http(HTTP/1.1)协议.
在conf/server.xml文件里,他对应的配置为
而 AJP Connector,它使用的是 AJP 协议(Apache Jserv Protocol)是定向包协议。因为性能原因,使用二进制格式来传输可读性文本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。
Ajp协议对应的配置为
下图是tomcat 服务器开放连接端口情况。
在tomcat conf/web.xml中配置着tomcat的路由处理,主要有两个servlet分支
这个规则匹配/后没有后缀的
<servlet>
<servlet-name>defaultservlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServletservlet-class>
<init-param>
<param-name>debugparam-name>
<param-value>0param-value>
init-param>
<init-param>
<param-name>listingsparam-name>
<param-value>falseparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>defaultservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
这个规则匹配路径中有.jsp的路由
<servlet>
<servlet-name>jspservlet-name>
<servlet-class>org.apache.jasper.servlet.JspServletservlet-class>
<init-param>
<param-name>forkparam-name>
<param-value>falseparam-value>
init-param>
<init-param>
<param-name>xpoweredByparam-name>
<param-value>falseparam-value>
init-param>
<load-on-startup>3load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>jspservlet-name>
<url-pattern>*.jspurl-pattern>
<url-pattern>*.jspxurl-pattern>
servlet-mapping>
该漏洞通过AJP协议端口触发,正是由于上文所述,Ajp协议的请求在Tomcat内的处理流程与我们上文介绍的Tomcat处理HTTP请求流程类似。我们构造两个不同的请求,经过tomcat内部处理流程,一个走default servlet(DefaultServlet),另一个走jsp servlet(JspServlet),可导致的不同的漏洞。
# 请求url
req_uri = '/asdf'
# AJP协议请求中的三个属性
javax.servlet.include.request_uri = '/'
javax.servlet.include.path_info = 'WEB-INF/web.xml'
javax.servlet.include.servlet_path = '/'
在搭好的环境上,设置断点
进入prepareRequest函数,该函数处理Requests请求,重点在这一块
在该case分支request.setAttribute(n,v )
函数是解析函数,通过三次循环可见下图效果
通过三个循环将attributes变量赋值成如下所示:
随后将请求传给CoyoteAdapter,对request进行封装,将请求抓发给Container:
通过多级反射和调用,到达Servlet路由分发
Servlet 的service函数根据请求的方法,调用不同的处理函数如下图匹配Get 方法:
1588562393145.png
其中有个关键的函数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’。
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 |
该漏洞的触发与上个相似,只不过servlet的分支不同,该漏洞走的是JspServlet,所有的jsp文件的路由
将漏洞利用代码修改为
从CoyoteAdapter进行分发,这里分发到jspservlet路由,因此我们要在jspservlet上下断点