关于 GhostCat
Tomcat 最近曝出了一个新的高危漏洞,由长亭命名为“GhostCat”,该漏洞可以读取和包含 Tomcat 应用的任意文件。
通常只要开启了 ajp 端口就可以利用,需要注意是否为修复后的版本。
Tomcat server.xml 中默认配置的 ajp 端口是 8009。
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
而且很多的 Tomcat 是默认开启着的,并且对外开放。
fofa、zoomeye 就能搜到不少,内网环境更是重灾区
漏洞检测
现在网上已经有很多个检测和利用的工具了,可以使用 xray、YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi 来验证是否存在漏洞。
关于 AJP
简单来说,正常情况下我们使用的都是 HTTP 连接器去访问的 Tomcat 服务器。
我们直接通过浏览器去访问,就进的是 HTTP 的连接器。
而 AJP 连接器则更多出现在反向代理,由 浏览器 -> Apache 服务器 -> AJP -> Tomcat 应用
比如 NGINX 是这样配置 AJP 反代的
location /{
ajp_pass 127.0.0.1:8009;
}
再举例 Apache
<VirtualHost *:8088>
... ...
ProxyPass /examples/ ajp://192.168.17.93:8009/examples/
ProxyPassReverse /examples/ ajp://192.168.17.93:8009/examples/
</VirtualHost>
也就是说我们有时候遇见的看似是 Apache 实际上是 java 应用,基本上就是用了反向代理。
而反向代理有些是正常的 HTTP 反向代理,剩下就是 AJP 反向代理了。
漏洞复现
先启动一个漏洞影响版本内的 Tomcat 服务。
启动后可以看到除了 HTTP 8080 端口外还监听了一个 8009 端口,这正是 server.xml 配置中默认设置的那个 ajp 端口
使用已公开的工具可以读取到 ROOT 目录下的 WEB-INF/web.xml。
漏洞分析
POC 分析
既然已经有了 POC ,不如先来分析一下 POC 。
先从黑盒角度来分析一下这个 POC 都做了什么。
首先是文件读取的数据包,虽然不是标准的 HTTP 数据包,但基本上和 HTTP 无异。
然后是文件包含的。
把两段数据包内容经过文本 diff 对比一下可以看出,实际上文件包含只是在请求地址后加了一个.jsp
,告诉服务器这是一个 jsp 文件,但是这个 asdf.jsp
又不存在,那就从 javax.servlet.include.path_info
中取文件地址来当做 jsp 文件执行。
以上为暂时的猜测,还需要后面从代码层面分析才行。
在 POC 中这些就是最主要的部分。
其他部分就是按照 Tomcat 指定的 AJP 协议格式构建数据包,详细可以参考官方文档。
代码分析
代码部分的分析可以通过 debug 在java.io.File
类的构造方法上打断点追溯方法栈的代码,这样比较方便。
为了能更清楚摸清逻辑这里用正向分析的方法。
先看 AJP 协议处理类 AjpProcessor
的父类 AbstractAjpProcessor
,在process()
方法里从二进制流里组装好请求对象后再去执行 Servlet。
跟进 prepareRequest()
方法
它把键和值存在了 request 的 attributes。
再来看 Tomcat 的父 web.xml,可以在 Tomcat 安装目录下的 /conf/
目录找到。
里面有两个重要的 Servlet,一个是 DefaultServlet 另一个是 JspServlet。
文件读取(DefaultServlet)
<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>
这个对应的就是文件读取漏洞,他在 url-pattern 设置了接受所有 URL 请求。
它通常用来处理静态文件,如 txt、jpg,jsp 另由JspServlet
处理。
所以现在要跟进 DefaultServlet 这个类看一下代码。
btw,这里可以看到 Tomcat 的一个比较鸡肋的PUT文件上传漏洞 CVE-2017-12615,只要设置 readOnly 为 false,就可以利用。
关于这个文件读取漏洞,则要从 DefaultServlet.serveResource() 看起,这个方法由 doGet 调用。
大概逻辑就是先从请求中获取路径,再读从 WEB 目录读取这个文件,如果读取到了就直接把内容写进输出流里。
这里重点是如何从请求中获取路径的。
org.apache.catalina.servlets.DefaultServlet#getRelativePath
这个方法是用来从请求中获取路径的,我把代码片段贴出来。
protected String getRelativePath(HttpServletRequest request) {
String result;
if (request.getAttribute("javax.servlet.include.request_uri") != null) {
result = (String)request.getAttribute("javax.servlet.include.path_info");
if (result == null) {
result = (String)request.getAttribute("javax.servlet.include.servlet_path");
} else {
result = (String)request.getAttribute("javax.servlet.include.servlet_path") + result;
}
if (result == null || result.equals("")) {
result = "/";
}
return result;
} else {
result = request.getPathInfo();
if (result == null) {
result = request.getServletPath();
} else {
result = request.getServletPath() + result;
}
if (result == null || result.equals("")) {
result = "/";
}
return result;
}
}
这里优先获取javax.servlet.include.request_uri
属性值判断如不为空。
则将javax.servlet.include.servlet_path
和javax.servlet.include.path_info
拼接组成一个相对路径。
反之为空则取正常的请求路径。
实际上就是在取 AbstractAjpProcessor.prepareRequest()
方法里往 request.attributes 填充的那几条数据。
再回顾一下 POC 的代码,这样就能明白是怎么回事了。
这里假设拼接后得到的 path 变量为 “/WEB-INF/web.xml”
后面的部分就是从 WEB 目录读取 “/WEB-INF/web.xml” 然后再写入到响应输出流里。
另外要注意的是这个漏洞只能读 WEB 目录下的文件,没法跳转到其他目录。
因为最终在 org.apache.catalina.webresources.AbstractFileResourceSet#file
有一个判断,如果最终组成的绝对路径开头不是 WEB 目录的话就返回null导致无法读取到文件。
文件包含(JspServlet)
<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>
文件包含更简单,只要请求地址结尾为 .jsp
或者.jspx
就会进入 JspServlet
service()
方法的代码很直观,只需要设定“org.apache.catalina.jsp_file”或者“javax.servlet.include.servlet_path”和“javax.servlet.include.path_info”的值为指定的文件路径,就可以将它当做 jsp 文件执行。
比如我在根目录创建了一个 test.log
的文件,内容是一段执行系统命令的 jsp 代码。
<%@page
import="java.io.*,java.util.*"%>
<%
Runtime run = Runtime.getRuntime();
Process p = run.exec("ipconfig");
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader inBr = new BufferedReader(new InputStreamReader(in));
String a = "";
String lineStr;
while ((lineStr = inBr.readLine()) != null)
a+=lineStr;
response.getWriter().println(a);
%>
然后我将 payload 部分替换成
{'name':'req_attribute','value':['org.apache.catalina.jsp_file','/test.log']}
请求的地址后面加上一个 .jsp
让它进入 JspServlet
还有要注意,如果请求地址是 /asdf.jsp
,那 WEB 目录下就不能存在 asdf.jsp。
最终将 test.log 当成 jsp 文件执行,俗称“文件包含漏洞”。
这个漏洞可以在有上传附件功能的环境下利用,随便上传一个包含JSP恶意代码的图片,然后利用这个漏洞让它解析为 jsp 文件。
修复建议
- server.xml 里注释这段代码
<!--<Connectorport="8009" protocol="AJP/1.3"redirectPort="8443" />-->
- 设定只允许监听 127.0.0.1 并且加一个 secret
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" address="YOUR_TOMCAT_IP_ADDRESS" secret="YOUR_TOMCAT_AJP_SECRET"/>
- 更新到 Tomcat 最新版本
总结
漏洞曝出来后拖了两天才开始写文章,还是朋友让写的,不然这个月可能都不更博客了。
没啥总结的,赶紧修漏洞吧。