目录
学习该漏洞时我翻阅了大量的博客,但是感觉大部分没有说到点子上,所以写这篇博客,记录一下学习历程,接下来我会从代码和协议两个方面来分析这个漏洞。
一、漏洞简介
该漏洞是由长亭科技安全研究员发现的存在于 Tomcat 中的安全漏洞,由于 Tomcat AJP 协议设计上存在缺陷,攻击者通过 Tomcat AJP Connector 可以读取或包含 Tomcat 上所有 webapp 目录下的任意文件,例如可以读取 webapp 配置文件或源代码。此外在目标应用有文件上传功能的情况下,配合文件包含的利用还可以达到远程代码执行的危害。
影响版本:
Apache Tomcat 6
Apache Tomcat 7 < 7.0.100
Apache Tomcat 8 < 8.5.51
Apache Tomcat 9 < 9.0.31
二、漏洞原理:
首先还是了解这个漏洞要有一些基础知识
这个漏洞是tomcat的漏洞,所以我们要对tomcat的运行原理有一些概念。
tomcat的大致体系结构如下:
Connector :用于在指定的端口上侦听客户请求,接收连接请求之后分配线程让 Container 来处理这个请求。
Container :由四个自容器组件构成,分别是Engine、Host、Context、 Wrapper。
Engine :Engine 容器,定义了一些基本的关联关系。
Host :Host 是 Engine 的字容器,Host 在 Engine 中代表一个虚拟主机,其作用 就是运行多个应用。
Context :Context 容器,拥有 Servlet 运行的基本环境,且负责管理其中的 Servlet 实例。 Wrapper :Wrapper 是最底层的容器,负责管理一个 Servlet。
Servlet :服务程序。
(1) Tomcat Connector(连接器)
首先来说一下Tomcat的Connector组件,Connector组件的主要职责就是负责接收客户端连接和客户端请求的处理加工。每个Connector会监听一个指定端口,分别负责对请求报文的解析和响应报文组装,解析过程封装Request对象,而组装过程封装Response对象。
上面说了那么多,其实意思就是Connector是tomcat的入口,一个其他博主的例子,如果把Tomcat比作一个城堡,那么Connector组件就是城堡的城门,为进出城堡的人们提供通道。当然,可能有多个城门,每个城门代表不同的通道。而Tomcat默认配置启动,开了两个城门(通道):一个是监听8080端口的HTTP Connector,另一个是监听8009端口的AJP Connector。
Tomcat组件相关的配置文件是在conf/server.xml
,配置文件中每一个元素都对应了Tomcat的一个组件(可以在配置文件中找到如下两项,配置了两个Connector组件):
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
HTTP Connector很好理解,通过浏览器访问Tomcat服务器的Web应用时,使用的就是这个连接器;
AJP Connector是通过AJP协议和一个Web容器进行交互。在将Tomcat与其他HTTP服务器(一般是Apache )集成时,就需要用到这个连接器。AJP协议是采用二进制形式代替文本形式传输,相比HTTP这种纯文本的协议来说,效率和性能更高,也做了很多优化。
两个Connector的原理如下图所示:
我们的浏览器肯定是不支持AJP协议的,所以要在AJP Connector前面加一个apache server的反向代理,我们先将http请求交到反向代理服务器,反向代理服务器再通过ajp协议在8009端口向tomcat发出请求。
(2)Servlet(服务程序)
Servlet意为服务程序,也可简单理解为是一种用来处理网络请求的一套规范。主要作用是给上级容器(Tomcat)提供doGet()和doPost()等方法,其生命周期实例化、初始化、调用、销毁受控于Tomcat容器。有个例子可以很好理解:想象一下,在一栋大楼里有非常多特殊服务者Servlet,这栋大楼有一套智能系统帮助接待顾客引导他们去所需的服务提供者(Servlet)那接受服务。这里顾客就是一个个请求,特殊服务者就是Servlet,而这套智能系统就是Tomcat容器。
tomcat默认配置了两个Servlet,一个是Jsp Servlert,另一个是Default Servlet,第一个是专门负责处理jsp类型的文件的,除了jsp文件的全都交给Default Servlet处理。Tomcat中Servlet的配置是在conf/web.xml 下面。
<!-- The default servlet for all web applications, that serves static -->
<!-- resources. It processes all requests that are not mapped to other -->
<!-- servlets with servlet mappings. -->
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
......
......
</servlet>
<!-- The JSP page compiler and execution servlet, which is the mechanism -->
<!-- used by Tomcat to support JSP pages. Traditionally, this servlet -->
<!-- is mapped to the URL pattern "*.jsp". -->
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
......
......
</servlet>
......
......
<!-- The mapping for the default servlet -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- The mappings for the JSP servlet -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
由以上配置文件也可以看出,如果是jsp文件的请求,回交给Jsp Servlert,其他的都交给Default Servlet进行处理。
(3)tomcat内部处理流程
在这里简单介绍一下处理流程就好,便于理解后续漏洞的分析。
- 用户点击网页内容,请求被发送到本机端口8080,被Connector获得(Connector中的Processor用于封装Request,Adapter用于将封装好的Request交给Container)。
- Connector把该请求交给Container中的Engine来处理,并等待Engine的回应。
- Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
- Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为" "的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。
在介绍漏洞之前还要配一下tomcat源码分析的环境,大家可以根据这篇博文Tomcat源码编译(IDEA)_tomcat 编译_ww0peo的博客-CSDN博客
来进行配置,配置的版本是apache-tomcat-8.5.46,配置好了之后就可以愉快的进行漏洞的代码分析了。
根据上文的Tomcat处理请求流程,请求首先到达Connector,Connector内使用AjpProcessor
解析Socket,将Socket中的内容封装到Request中。然后经过一系列的处理,到达Servlet。根据上文我们可以了解到,tomcat默认配置两个Servlet,一个是Default Servlet,另一个是Jsp Servlet,而这这里就分别对应了文件读取漏洞和任意代码执行漏洞。
1.文件读取漏洞
当url未匹配到Servlet时,回给到Default Servlet处理。
分析org.apache.catalina.servlets.DefaultServlet.java 。请求到达 DefaultServlet 后, 会执行 service() 方法,判断请求的 DispatcherType 是否为 ERROR ,若是则直接调用 doGet() 方法,否则调用父类的 Service() 方法。
DefaultServlet 的父类为 HttpServlet (javax.servlet.http.HttpServlet.java)。其 service() 方法根据请求方法(method)决定调用 doGet() 或是 doPost() ,代码如下。此 处我们是读取文件,使用的是 doGet 方法。
然后回到子类,查询doGet方法。 通过serveResource方法来获取资源。
在serveResource方法中,它定义了我们获取资源的路径
跟踪进去,
我们发现了三个重要的参数,
INCLUDE_REQUEST_URI, INCLUDE_PATH_INFO, INCLUDE_SERVLET_PATH
发现这三个参数可以控制我们请求资源的路径,继续跟踪,发现这三个属性都有默认值
static final String INCLUDE_REQUEST_URI = "javax.servlet.include.request_uri";static final String INCLUDE_CONTEXT_PATH = "javax.servlet.include.context_path";static final String INCLUDE_PATH_INFO = "javax.servlet.include.path_info";
这里就是我们利用的重点,我们可以通过发送数据包来伪造这几个默认值,从而控制路径,到后面我会用wireshark抓包看一下。
由上面截图中的代码可知,当 request 中 javax.servlet.include.request_uri 不为空时,将 request 的 javax.servlet.include.path_info 与 javax.servlet.include.servlet_path 分别赋值给 path_info 和 servlet_path。 而后只要 path_info 与 servlet_path 的值不为空,就将 path_info 与 servlet_path 拼接起来赋值给 result。
回到serveResource 方法,获取到路径后创建资源的实例并且获取资源。
跟踪进去getResource方法,(位于 org.apache.catalina.webresources.StandardRoot.java中)
我们发现上述代码调用了validate进行判断,继续跟着
在这里有主要用了normalize方法来进行校验,其主要功能如下:
1.判断path中是否有'\\',有就转换成'/';
2.判断path是否以'/'开头,若不是则在前面加上'/';
3.判断path是否以'/.'或'/..'结尾,若是则结尾加一个'/';
4.判断path是否含有'//',有则替换成'/';
5.判断path是否含有'/./',若有直接截断并重新拼接,如'/./abc'变为'/abc'
6.判断path是否含有'/../',若有则直接返回null;
首先补充一个知识,当我们访问tomcat时,它会默认访问取webapps下的默认文件,而在这里对相对路径的符号进行了限制,所以我们利用该漏洞只能访问webapps下的文件。
在验证完成之后 ,会继续将资源返回到输出流中,再交由tomcat的内部进行处理。
至此利用过程结束,而我们可以通过刚才在 DefaultServlet 的 getRelativePath() 中说过的三个参数进行访问资源路径的控制。而我们首先还需要让请求到达 DefaultServlet ,需要让请求的 url 为 “/asdf” ,此处的 asdf 为随机字符串,目的是让 Tomcat 找不到 webapps 下的至指定文件,从而请求会到 Tomcat 的默认目录。
若是想请求 webapps 目 录下的其他目录,则可以设置为 ‘/指定目录/asdf’,如’’/manager/asdf’。会访问到webapps/manager/下的目录,具体的文件看我们构造的参数。
如要访问”webapps/ROOT/WEB-INF/web.xml”,首先将请求 url 为’/asdf’,再将 javax.servlet.include.request_uri 值设置为’/‘(非空), javax.servlet.include.servlet_path 的值设置为 ‘/‘,javax.servlet.include.path_info 的值设置为 ‘WEB-INF/web.xml’。
poc的下载地址为GitHub - YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi: Tomcat-Ajp协议文件读取漏洞
但是在python3.7.9的版本下运行会报错,这里我修改了poc,修改的poc放在了tomcat_Ghostcat/ at main · fengdianxiong/tomcat_Ghostcat · GitHub 上,大家可以自取。
成功获取文件 。
我们再来看一下对应的数据包:
发现在poc的数据包中携带了我们上面三个重要的参数,从而实现文件读取。
2.任意代码执行漏洞
这个漏洞稍微有一点点鸡肋,需要提前先自己上传木马文件,然后利用文件包含漏洞来实现代码执行。
由上文可知,我们知道当请求jsp文件时,会走Jsp Servlet来进行处理。
首先来到JspServlert.java(位于org.apache.jasper.servlet.JspServlet.java),经过service方法处理,由标红部分可以看出,jspUri由servlet_path和path_info拼接而成。
然后进入到serviceJspFile方法中,方法利用 jspUri 生成 JspServletWrapper 实例,再到调用实例的 service() 方法。
获取到对应的Servlet
调用该Servlet的service方法
请求执行,从而造成文件包含。
利用过程简单就是,我们通过传入 servlet_path 和 path_info ,让其去将指定的文件作为 jsp 文件处理,Tomcat 会将根据指定的文件生成 java 代码并编译成 class ,而后加载 class 类实例化一个 servlet 并执行,从而造成文件包含。
在复现时,要修改一下poc的参数,然后在webapps目录下的ROOT下(apache-tomcat-8.5.46-source\catalina-home\webapps\ROOT),自己创建一个1.txt,
内容为
<%Runtime.getRuntime().exec("calc.exe");%>
然后再次利用poc,弹出计算器。
三、修复建议
(1)官方网站下载新版本进行升级。
(2)直接关闭 AJP Connector,或将其监听地址改为仅监听本机 localhost。
(3)若需使用 Tomcat AJP 协议,可根据使用版本配置协议属性设置认证凭证。