Tomcat 文件读取/文件包含漏洞(CVE-2020-1938)
Tomcat 文件读取/文件包含漏洞(CVE-2020-1938)
一、漏洞简介
2020年2月20日,国家信息安全漏洞共享平台(CNVD)发布了关于Apache Tomcat存在文件包含漏洞的安全公告(CNVD-2020-10487,对应CVE-2020-1938)。攻击者可利用该漏洞读取或包含 Tomcat 上所有 webapp 目录下的任意文件,如:webapp 配置文件或源代码等。
影响范围:
Apache Tomcat 6
Apache Tomcat 7 < 7.0.100
Apache Tomcat 8 < 8.5.51
Apache Tomcat 9 < 9.0.31
二、 漏洞原理
2.1 基础简介
2.1.1 Tomcat 的体系结构
Tomcat大致的体系结构如下:
- Connector :用于在指定的端口上侦听客户请求,接收连接请求之后分配线程让 Container 来处理这个请求。
- Container :由四个自容器组件构成,分别是Engine、Host、Context、Wrapper。
- Engine :Engine 容器,定义了一些基本的关联关系
- Host :Host 是 Engine 的字容器,Host 在 Engine 中代表一个虚拟主机,其作用就是运行多个应用。
- Context :Context 容器,拥有 Servlet 运行的基本环境,且负责管理其中的 Servlet 实例。
- Wrapper :Wrapper 是最底层的容器,负责管理一个 Servlet。
- Servlet :服务程序。
2.1.2 Tomcat Connector
Tomcat 最主要的功能是提供 Servlet/JSP 容器,它在对静态资源(如 HTML 文件或图像文件)的处理速度,以及提供的 Web 服务器管理功能方面都不如其他专业的 HTTP 服务器,如 IIS 和 Apache 服务器。因此在实际应用中,常常把 Tomcat 与其他 HTTP 服务器集成,二者通过 AJP 协议来通信。其他 HTTP 服务器可以将 Servlet 与 JSP 服务交给 Tomcat 服务器来处理。
而 Tomcat 的 Connector 组件主要职责就是负责接收客户端连接和客户端请求的处理加工。每个 Connector 会监听一个指定端口,分别负责对请求报文的解析和响应报文组装,解析过程封装 Request 对象,而组装过程封装 Response 对象。Tomcat 服务器默认开启了两个 Connector,分别监听8080端口与8009端口,其中 HTTP Connector 是用于正常 HTTP 协议通信的连接器,AJP Connector 则是使用 AJP 协议与其他 HTTP 服务器进行通信,使用二进制格式来传输可读性文本,降低了 HTTP 请求的处理成本。Tomact 与 Apache 集成工作过程如下图所示。
2.1.3 Servlet
Servlet(服务程序)简单理解起来就是一种用来处理网络请求的一套规范,作用是给上级容器(Tomcat)提供 doGet() 和 doPost() 等方法。所有发送至 Tomcat 的请求都会根据相应的规则交由指定 Servlet 来处理。Tomcat 默认配置了两个 Servlet,即 DefaultServlet 和 JspServlet。JspServlet 负责处理所有JSP文件的请求,而当请求没有匹配到任何指定 Servlet 则交由 DefaultServlet 处理。( conf/web.xml 配置文件中相关的配置如下。)
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
......
</servlet>
<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>
2.1.4 Tomcat 处理HTTP请求过程
用户输入URL:
http://localhost:8080/test/index.jsp,请求被发送到本机端口8080,Connector接收后,由Connector 中的 Processor 类来封装 Request 。
Connector 中的 Adapter 类将封装好的 Request 交给 Container 中的 Engine 容器来处理,并等待 Engine 容器的回应。
Engine 容器收到请求,进行匹配,找到指定的虚拟主机Host。
Engine 容器匹配到名为localhost 的 Host 容器,该 Host 容器收到请求 /test/index.jsp,匹配其拥有的 Context 容器,寻找路径为/test的 Context 容器。
path="/test" 的 Context 获得请求 /index.jsp,在其 mapping table 中匹配对应的 Servlet。找到 url-pattern 为 *.jsp 的 JspServlet(匹配不到则默认交由 DefaultServlet 类 )。
Wrapper 中的 Servlet 构造 HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 JspServlet的 doGet() 方法或 doPost() 方法,执行业务逻辑、数据存储等程序。
Context 将 Servlet 执行完之后的 HttpServletResponse 对象返回给 Host 容器。
Host 容器把 HttpServletResponse 对象返回给 Engine 容器。
Engine 容器把 HttpServletResponse 对象返回 Connector。
Connector 把 HttpServletResponse 对象返回给客户的浏览器。
2.2 漏洞分析
官网下载Tomcat源码进行分析。(apache-tomcat-8.5.46-src:https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.46/src/apache-tomcat-8.5.46-src.zip)
其他服务器发送 AJP 请求给Tomcat,而后组件 Connector 接收并交由自己的 AjpProcessor 类进行处理。此次造成漏洞的核心代码就在 org.apache.coyote.ajp.AjpProcessor.java 中 prepareRequest() 方法下处理额外属性的地方,如下。
在读取 AJP 请求头后,prepareRequest() 方法将解析 AJP 请求信息,并将其中的属性与值取出放入 request 中。
1、创建 attributeCode 变量,从 AJP 请求取出值赋给它,而后用其与 Constants.SC_A_ARE_DONE (值为0XFF,代表请求结束) 比较的结果作为 while 循环条件。
2、当取出 attributeCode 的值为 Constants.
SC_A_REQ_ATTRIBUTE (值为 10,非通用请求属性名称的整数代码)时,即取出的值为额外属性。会将属性名称与值取出分别使用n、v 变量存放。
3、当属性名称不是 AJP 本地地址、AJP 远程端口等属性时,会将取出的属性与值直接存放进 request 对象中。
接下来会将将请求传给 CoyoteAdapter ,由它负责对请求进行封装,构造 Request 和 Response 对象,并将请求发送给 Container。
之后就是 Tomcat 内部处理过程,最后到达 Servlet 。而 Tomcat 默认配置了两个 Servlet ,DefaultServlet 与 JspServlet ,也就分别对应了文件读取和文件包含漏洞。
文件读取漏洞:
当URL请求未匹配到指定 Servlet 时,最后都会交由 DefaultServlet 来处理。分析
org.apache.catalina.servlets.DefaultServlet.java 。请求到达 DefaultServlet 后,会执行 service() 方法,判断请求的 DispatcherType 是否为 ERROR ,若是则直接调用 doGet() 方法,否则调用父类的 Service() 方法。
DefaultServlet 的父类为 HttpServlet
(javax.servlet.http.HttpServlet.java)。其 service() 方法根据请求方法(method)决定调用 doGet() 或是 doPost() ,代码如下。此处我们是读取文件,使用的是 Get 方法。
DefaultServlet 的父类为 HttpServlet
(javax.servlet.http.HttpServlet.java)。其 service() 方法根据请求方法(method)决定调用 doGet() 或是 doPost() ,代码如下。此处我们是读取文件,使用的是 Get 方法。
接下来在 doGet() 方法中,调用 serveResource() 方法去获取资源。
在 serveResource() 方法中,会调用 getRelativePath() 方法去获取资源路径,并使用 path 变量存储路径。
在 getRelativePath() 方法中有三个重要的路径参数,如下。
在 javax.servlet.RequestDispatcher.java 中,查看定义的值,如下。
static final String INCLUDE_REQUEST_URI = "javax.servlet.include.request_uri";
static final String INCLUDE_PATH_INFO = "javax.servlet.include.path_info";
static final String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";
由截图中的代码可知,当 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 ,代码如下。所以之前的 path 的值为 result。
回到 serveResource() 方法,获取到资源路径后,创建 resources 对象并调用其 getResource() 方法根据 path 获取资源,代码如下。
WebResource resource = resources.getResource(path);
在 getResource() 方法中(位于
org.apache.catalina.webresources.StandardRoot.java),会调用 validate() 方法进行校验。
validate() 方法中主要调用 normalize() 方法进行校验。
normalize() 方法的主要功能如下:
1.判断path中是否有'\\',有就转换成'/';
2.判断path是否以'/'开头,若不是则在前面加上'/';
3.判断path是否以'/.'或'/..'结尾,若是则结尾加一个'/';
4.判断path是否含有'//',有则替换成'/';
5.判断path是否含有'/./',若有直接截断并重新拼接,如'/./abc'变为'/abc'
6.判断path是否含有'/../',若有则直接返回null;
由第六点可知我们的请求路径中不能包含"/../",也就导致了该漏洞只能读取webapps目录下的文件。
验证完成之后,使用 getOutputStream() 获取 ServletOutputStream 的实例
再用 write() 方法将获取资源的内容写入输出流。
随后再经过 Tomcat 内部流程处理,最终返回给客户端。
至此利用过程结束,而我们可以通过刚才在 DefaultServlet 的 getRelativePath() 中说过的三个参数进行访问资源路径的控制。而我们首先还需要让请求到达 DefaultServlet ,需要让请求的 url 为 "/asdf" ,此处的 asdf 为随机字符串,目的是让 Tomcat 找不到 webapps 下的至指定文件,从而请求会到 Tomcat 的默认目录。若是想请求 webapps 目录下的其他目录,则可以设置为 '/指定目录/asdf',如''/manager/asdf'。
如要访问"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'。
文件包含漏洞:
若请求的是 jsp 文件,则请求在经过 AjpProcessor 类后,就交由 JspServlet 进行处理。
请求到达 JspServlet 后,经 service() 方法处理,将 servlet_path 和 path_info 拼接在一起,赋值给 jspUri (相当于请求的 jsp 的路径)。代码如下(位于 org.apache.jasper.servlet.JspServlet.java):
之后使用 serviceJspFile() 方法利用 jspUri 生成 JspServletWrapper 实例,也会初始化一个也会初始化一个JspCompilationContext ,再到调用实例的 service() 方法。
JspServletWrapper 中的 service() 方法会判断 options.getDevelopment() 或 mustCompile 值为 True (即判断系统是否允许重加载),而后调用 JspCompilationContext 的 compile() 方法。代码位于org.apache.jasper.servlet.JspServletWrapper.java 。
在 JspCompilationContext 的 compile() 方法会调用 Compiler 的 compile() 方法,
而该方法首先会根据请求的 jsp 文件,生成对应的 java 代码,再将代码文件编译成 class 。代码如下。
之后 JspServletWrapper 使用 getServlet() 方法获取编译后生成的 Servlet 。
最后调用 service() 方法,请求执行。
利用过程简单就是,我们通过传入 servlet_path 和 path_info ,让其去将指定的文件作为 jsp 文件处理,Tomcat 会将根据指定的文件生成 java 代码并编译成 class ,而后加载 class 类实例化一个 servlet 并执行,从而造成文件包含。
三、复现过程
服务器:Windows Server 2008 (IP:192.168.219.181)
Tomcat 版本:Apache Tomcat 8.5.47
攻击机:Kali (IP:192.168.219.134)
POC :CNVD-2020-10487-Tomcat-Ajp-lfi
( https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi ).
3.1 任意文件读取
使用 POC 访问 Windows 服务器上 WEB-INF 下的 web.xml 文件
python CNVD-2020-10487-Tomcat-Ajp-lfi.py 服务器IP -p 8009 -f WEB-INF/web.xml
3.2 文件包含
利用文件包含漏洞实现命令执行
1.在服务器的webapps下的ROOT目录下生成一个 shell.txt 文件,使用命令打开计算器,内容如下:
2.更改POC,将asdf加上.jsp,如下:
3.使用POC将生成的文件包含,执行命令。
Windows 服务器上的计算器被打开。
3.4 修复方法
(1)官方网站下载新版本进行升级。
(2)直接关闭 AJP Connector,或将其监听地址改为仅监听本机 localhost。
(3)若需使用 Tomcat AJP 协议,可根据使用版本配置协议属性设置认证凭证。
四、参考文章
2、一文详解Tomcat Ghostcat-AJP协议文件读取/文件包含漏洞CVE-2020-1938 - raul17的文章 - 知乎
登录
前往注册账号注册
注册成功,默认密码已通过短信发送给您
欢迎来到蜗牛学苑~
我们仍旧想要当初想要的不一样,世间浮沉,到最后还是想要和当初想要的和别人不一样。
3小时前 2人点赞