Tomcat AJP协议文件读取/包含漏洞(CVE-2020-1938)

漏洞条件

  • Apache Tomcat 6.x
  • Apache Tomcat 7 < 7.0.100
  • Apache Tomcat 8 < 8.5.51
  • Apache Tomcat 9 < 9.0.31
  • 开启 AJP Connector(默认开启监听8009端口)

前置知识

Tomcat Connector(连接器)

Tomcat Connector是Tomcat与外部连接的通道,它使得Catalina能够接收来自外部的请求,传递给对应的Web应用程序处理,并返回请求的响应结果。

默认情况下,Tomcat配置了两个Connector,它们分别是HTTP ConnectorAJP Connector

  • HTTP Connector:用于处理HTTP协议的请求(HTTP/1.1),默认监听地址为0.0.0.0:8080
  • AJP Connector:用于处理AJP协议的请求(AJP/1.3),默认监听地址为0.0.0.0:8009

Tomcat组件相关的配置文件是在conf/server.xml,配置文件中每一个元素都对应了Tomcat的一个组件(可以在配置文件中找到如下两项,配置了两个Connector组件):

<!-- A "Connector" represents an endpoint by which requests are received
     and responses are returned. Documentation at :
     Java HTTP Connector: /docs/config/http.html
     Java AJP  Connector: /docs/config/ajp.html
     APR (HTTP/AJP) Connector: /docs/apr.html
     Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
-->
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />
...
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

HTTP Connector就是用来提供我们经常用到的HTTP Web服务。而AJP Connector,它使用的是AJP协议(Apache Jserv Protocol),AJP协议可以理解为HTTP协议的二进制性能优化版本,它能降低HTTP请求的处理成本,因此主要在需要集群、反向代理的场景被使用。

显然,浏览器只支持HTTP协议,并不能直接支持AJP协议。所以实际情况是,通过Apache的proxy_ajp模块进行反向代理,暴露成http协议(8009端口)给客户端访问,大致如下图所示:

Servlet(服务程序)

Servlet意为服务程序,也可简单理解为是一种用来处理网络请求的一套规范。主要作用是给上级容器(Tomcat) 提供doGet()doPost()等方法,其生命周期实例化、初始化、调用、销毁受控于Tomcat容器。

Tomcat中Servlet的配置是在conf/web.xml。Tomcat默认配置定义了两个servlet,分别为DefaultServletJspServlet

<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>
  <init-param>
      <param-name>readonly</param-name>
      <param-value>false</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
...
<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>

所有的请求进入tomcat,都会流经servlet。如果没有匹配到任何应用指定的servlet,那么就会流到默认的servlet(即DefaultServlet),而JspServlet负责处理所有JSP文件的请求。

Tomcat内部处理请求流程

  1. 用户点击网页内容,请求被发送到本机端口8080,被Connector获得(Connector中的Processor用于封装Request,Adapter用于将封装好的Request交给Container)。
  2. Connector把该请求交给Container中的Engine来处理,并等待Engine的回应。
  3. Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host。
  4. Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有的Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为" "的Context去处理)。
  5. path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL PATTERN为*.jsp的Servlet,对应于JspServlet类(匹配不到指定Servlet的请求对应DefaultServlet类)。
  6. Wrapper是最底层的容器,负责管理一个Servlet。构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()doPost(),执行业务逻辑、数据存储等程序。
  7. Context把执行完之后的HttpServletResponse对象返回给Host。
  8. Host把HttpServletResponse对象返回给Engine。
  9. Engine把HttpServletResponse对象返回Connector。
  10. Connector把HttpServletResponse对象返回给客户Browser。

漏洞原理

这个漏洞主要是通过AJP协议(8009端口)触发。正是由于上文所述,Ajp协议的请求在Tomcat内的处理流程与我们上文介绍的Tomcat处理HTTP请求流程类似。我们构造两个不同的请求,经过tomcat内部处理流程,一个走default servlet(DefaultServlet),另一个走jsp servlet(JspServlet),可导致的不同的漏洞。

文件读取漏洞走的是DefaultServlet,文件包含漏洞走的是JspServlet。

PoC:

YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi

文件读取漏洞

通过构造AJP协议请求,我们可以读取到 我们以读取WEB-INF/web.xml文件为例。

POC中赋值了四个很重要的参数,先在此说明:

# 请求url
req_uri = '/asdf'

# AJP协议请求中的三个属性
javax.servlet.include.request_uri = '/'
javax.servlet.include.path_info = 'WEB-INF/web.xml'
javax.servlet.include.servlet_path = '/'

**关键点1:AjpProcessor类 -> service() -> **prepareRequest()

根据上文的Tomcat处理请求流程,请求首先到达Connector,Connector内使用AjpProcessor解析Socket,将Socket中的内容封装到Request中。

所以我们首先将断点打到AjpProcessor类的service()方法,然后发送PoC进入断点:

一步步请求,随后跟入prepareRequest()方法。

该方法解析请求,将相关属性匹配到该request的属性里。在一个while 循环分别将3个参数赋值(v赋值给n),放到request中。

放到request对象中的三个参数和对应参数值如下:

随后将请求传给CoyoteAdapter

CoyoteAdapter内部对request进行封装,将请求转发给Container:

随后直接走跟到ApplicationFilterChain类的internalDoFilter()方法将流程走到Servlet。

**关键点2:DefaultServlet类 -> service() -> **doGet()

由上文介绍的Servlet相关基础知识可知,该请求是非JSP文件请求,匹配不到指定的servlet,所以会映射到默认的servlet(default servlet)处理。tomcat源码有个DefaultServlet类(路径:org/apache/catalina/servlets/DefaultServlet.java),我们断点也打到这个类,Debug看一下相关请求流程。

这里还要科普一下Servlet如何处理请求:一般请求到达servlet后先执行service()方法,在方法中根据请求方式决定执行doGet()还是doPost()方法。

流程进入DefaultServlet类service()方法,这里调用其父类的service() 方法:

随后进入HttpServlet类service()的方法,在这里调用了 doGet()方法:

关键点3:****getRelativePath()

doGet()方法内直接进入serveResource()方法:

serveResource()方法内首先是进入getRelativePath()方法

该方法的作用是确认请求的资源路径,进入该方法,可以看到三个很重要的参数(红框):

这三个参数所对应的值为:

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";

与我们的POC中的三个赋值参数对应,POC中的参数代入getRelativePath()方法,RequestDispatcher.INCLUDE_REQUEST_URI的值为’/’,不为空。pathInfo和servletPath参数的值拼接成result,getRelativePath()方法将result返回,返回内容为:’/WEB-INF/web.xml’。

**关键点4:getResource() -> validate() -> **normalize()

serveResource()方法继续往下,看到调用了getResource()方法:

跟入getResource()方法,可以看到调用了validate()方法:

validate()方法内主要调用了normalize()方法对path参数进行校验:

normalize()方法内做了那些校验:

这样一来如果想通过/WEB-INF/../../../../../etc/passwd目录穿越的形式来读取文件是行不通的,因为当/../在开头就会直接返回null,回到validate()方法,就会报**IllegalArgumentException(非法参数)**的异常并终止本次操作。这导致了只能获取当前目录下(即webapps)的文件。

经过validate()方法校验后,getResources()方法随后的一系列操作就通过路径读取到了资源,并且为response的HTTP各种字段设置值。

关键点5:****ServletOutputStream.write()

最后通过getOutputStream()方法获得ServletOutputStream的实例:

使用resource.getContent()获取资源内容,并利用ServletOutputStream.write()向输出流写入返回内容。

随后再经过Tomcat内部流程处理,经过Tomcat的ContainerConnector,最终返回给客户端。

关键点6:POC中的请求url(读取webapps下其他目录的文件)

前文提到POC中还有个关键参数req_uri,这个参数的设置决定了我们可以读取webapps下其他目录的文件。设置其值为一个随意字符串’asdf’,一来是无法匹配到webapps下的路径,走tomcat默认的ROOT目录;二来是为了让tomcat将请求流到DefaultServlet,从而触发漏洞。当请求读取WEB-INF/web.xml文件,则读取的就是webapps/ROOT/WEB-INF/目录下的web.xml。

当读取webapps/manager目录下的文件,只需修改POC中req_uri参数为’manager/asdf’,读取WEB-INF/web.xml文件则是读取webapps/manager/WEB-INF/目录下的web.xml。

因此可以尝试修改PoC,增加一个-w 参数,指定要读取的目录:

总结:至此,理解了如上6个关键点,整体漏洞流程也比较清晰了。

文件包含漏洞 (可致RCE)

理解了上文的文件读取漏洞的分析,接下来的内容很好理解。与上文不同的是,请求经过AjpProcessor类的处理,随后将请求转发给了JspServlet(该原理上文也有介绍,POC中的请求url是.jsp文件,而JspServlet负责处理所有JSP文件的请求)。

首先在webapps/manager目录下新建文件test.txt,内容为:

<%Runtime.getRuntime().exec("calc.exe");%>

修改POC进行调试。POC中的四个关键参数,也先在此说明:

# 请求url,这个参数一定要是以“.jsp”结尾
req_uri = '/manager/ddd.jsp'

# AJP协议请求中的三个属性
javax.servlet.include.request_uri = '/'
javax.servlet.include.path_info = 'test.txt'
javax.servlet.include.servlet_path = '/'

**关键点1:JspServlet类 -> service() -> **serviceJspFile()

断点打到JspServlet类的service()方法,先将servlet_path和path_info拼接在一起,赋值给jspUri(故这个参数是可控的)。

随后进入serviceJspFile()方法,将/test.txt带入Tomcat加载和处理jsp的流程里。具体处理流程就不描述了,根据网上的一张图做了些修改,大致画了下Tomcat加载和处理jsp的流程图,能很清晰的看懂处理流程:

**关键点2:JspServletWrapper类getServlet() -> **service()

最后返回到JspServletWrapper类,获取jsp编译后生成的servlet,随后调用service()方法,请求被执行。

**总结:**简单理解就是我们传入的”/test.txt”被当成jsp编译执行。带入了Tomcat处理jsp的处理流程,将jsp(test.txt)转义成Servlet源代码.java(test_txt.java),将Servlet源代码.java编译成Servlet类.class(test_txt.class),Servlet类执行后,响应结果至客户端。

该漏洞造成RCE的条件是:在webapps目录下上传文件(可以是任意文件),随后通过该文件包含漏洞,造成RCE。

漏洞利用

文件读取

尝试读取刚才写到manage目录下的test.txt,注意请求URL最后不能为 .jsp结尾,才能走到DefaultServlet导致文件读取

文件包含

接着我们去执行test.txt,这时候需要请求的URL为 .jsp结尾,才能走到JspServlet导致文件包含

参考资料

Apache Tomcat 曝出 Ghostcat 高危文件读取/包含漏洞

Tomcat内核详解(六):Connector组件

Tomcat整体架构浅析

解析Tomcat内部结构和请求过程

Apache Tomcat 远程文件包含漏洞深入分析 by 天融信阿尔法实验室

Tomcat Ajp协议文件包含漏洞分析 by d00ms

CVE-2020-1938:Tomcat AJP文件包含漏洞分析 by c0ny1

不调试源码重现 Ghostcat 漏洞 (CVE-2020-1938) by xax007

Busting Ghostcat: An Analysis of the Apache Tomcat Vulnerability (CVE-2020-1938 and CNVD-2020-10487) by 趋势科技

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇