漏洞描述

编号:CVE-2020-1938/CNVD-2020-10487

细节:Tomcat服务器存在文件包含漏洞,攻击者可利用该漏洞读取或包含Tomcat上所有webapp目录下的任意文件,如:webapp配置文件或源代码等。

POC:https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi

影响范围

  • Apache Tomcat 6
  • Apache Tomcat 7 < 7.0.100
  • Apache Tomcat 8 < 8.5.51
  • Apache Tomcat 9 < 9.0.31

漏洞成因

两个配置文件

Tomcat在部署时有两个重要的配置文件conf/server.xmlconf/web.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" />

与此对应,tomcat启动后会监听8080、8009端口,它们分别负责接受http、ajp协议的数据。后者则和普通的java Web应用一样,用来定义servlet,这里是tomcat内建的几个servlet:

<!-- 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 (defined either here or in your own   -->
  <!-- web.xml file).                                                       -->
    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        **** SNIP ****
        <load-on-startup>1</load-on-startup>
    </servlet>
  <!-- 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 (defined either here or in your own   -->
  <!-- web.xml file).                                                       -->
    <servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        **** SNIP ****
        <load-on-startup>3</load-on-startup>
    </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>

就像注解中描述的default servlet用来处理所有未被匹配到其他servlet的uri请求,jsp servlet用来处理以.jsp、.jspxz做后缀名的uri请求,这俩都随tomcat一起启动。

tomcat结构简介

tomcat的整体架构如上图所示,一个tomcat就是一个server,其中可以包含多个service(这里指是一个抽象的逻辑层)。而每个service由Connector、Container、Jsp引擎、日志等组件构成,与此次漏洞相关的组件主要是前两者。

Connector前面已经说过,是用来接受客户端的请求,请求中的数据包在被Connector解析后就会由Container处理。这个过程大致如下图:

Container中可以包含多个Host(虚拟主机,同Apache中定义),一个Host对应一个域名,因此Tomcat也可以配置多域名;每个Host又可以有多个Context,每个context其实就是一个web应用;而context下又有多个Wrapper,wrapper和servlet一一对应,只是它封装了一些管理servlet的函数。更进一步,客户端请求就交由servlet进入应用级的处理逻辑。

有问题的代码段一

tomcat默认监听的8009端口用来处理AJP协议。AJP协议建立在TCP socket通信之上,tomcat使用该协议和前级的Web Server传递信息,这次的漏洞就出在客户端可以利用ajp协议数据包控制request对象的一些字段。

具体地,tomcat源码的org.apache.coyote.ajp.AjpProcessor类的service()方法如下:

它调用的prepareRequest()方法用来解析一些请求头,部分内容如下:

可以看到,当ajp数据包的头设置为SC_REQ_ATTRIBUTE时(具体数值可以查询AJP协议规范),Connector会紧接着读取变量n(属性名)和v(值),当n不是SC_A_REQ_LOCAL_ADDRSC_A_REQ_REMOTE_PORTSC_A_SSL_PROTOCOL时,就会用v来赋值属性n。接着,service()方法将修改过的request代入后面的调用。

前面提到,当请求的uri无法匹配其他servlet时会由DefaultServlet处理,其中的调用流程如下图所示:

org.apache.catalina.servlets.DefaultServlet中,当我们的请求声明的是GET方法时,存在调用service()->doGet()->serveResource(),分析serveResource()代码如下:

其调用的getRelativePath()方法内容如下:

protected String getRelativePath(HttpServletRequest request, boolean allowEmptyPath) {
    String servletPath;
    String pathInfo;

    if (request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI) != null) {
        pathInfo = (String) request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
        servletPath = (String) request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
    } else{
        ......
    }
    StringBuilder result = new StringBuilder();
    if (servletPath.length() > 0) {
        result.append(servletPath);
    }
    if (pathInfo != null) {
        result.append(pathInfo);
    }
    ......
    return result.toString();
}

javax.servlet.RequestDispatcher中可以看到这三个属性的名称:

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

所以,我们就能通过AJP协议改变request的这三个属性来控制请求的路径,serveResource()方法获得path后的代码大致如下:

/*
*   serveResource()方法后续
*/
    ......  
    WebResource resource = resources.getResource(path);
    ......
    ServletOutputStream ostream = null;
    ostream = response.getOutputStream();
    ......
    byte[] resourceBody = resource.getContent();
    if (resourceBody == null) {
    ......
    } else {
    // Use the resource content directly
        ostream.write(resourceBody);
    }
    ......

它会直接把通过path获取的资源序列化输出,因此客户端再按照AJP协议解析数据包就能得到文件内容。

有问题的代码段二

同样的道理,tomcat默认将jsp/jspx结尾的请求交给org.apache.jasper.servlet.JspServlet处理,它的service()方法如下:

可以看到jspUri也是由两个可控的属性定义的,后续代码:

/**
*   service()方法后续 
*/
    ......
    try {
        boolean precompile = preCompile(request);
        serviceJspFile(request, response, jspUri, precompile);
    } catch (RuntimeException e) {
    ......
    }
}

private void serviceJspFile(HttpServletRequest request,
                            HttpServletResponse response, String jspUri,
                            boolean precompile) throws ServletException, IOException {
    JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
    if (wrapper == null) {
        synchronized(this) {
            wrapper = rctxt.getWrapper(jspUri);
            ......
        }
    }
    try {
        wrapper.service(request, response, precompile);
    } catch (FileNotFoundException fnfe) {
        ......
    }
}

代码在这里根据jspUri生成了一个JspServletWrapper,它会调用service()方法完成jsp代码的编译,将其转换成一个servlet。该servlet最终会以.java文件的形式写入%CATALINA_HOME%/work/Engine/Host/Context目录下:

/**
*   org.apache.jasper.servlet.JspServletWrapper.java
*/ 
public void service(HttpServletRequest request,
                    HttpServletResponse response,
                    boolean precompile)
    throws ServletException, IOException, FileNotFoundException {

    Servlet servlet;
    try {
        ......
        if (options.getDevelopment() || mustCompile) {
            synchronized (this) {
                if (options.getDevelopment() || mustCompile) {
                    // The following sets reload to true, if necessary
                    ctxt.compile();
                    mustCompile = false;
                }
            }
        } 
        ......
        /** 获取jsp编译后生成的servlet,并调用它的service()方法 **/
        servlet = getServlet();
        ......
    }

    try {
        ......
            servlet.service(request, response);
        }
    } catch (UnavailableException ex) {
        ......
    }
}

经过上述调用,这就形成了文件包含漏洞。当Web应用上有某个文件内容可被我们控制时,譬如某应用头像上传后的路径在webapp目录下,就能构造RCE。

复现过程

在复现时我在一台内网的虚拟机上搭建了Tomcat8.5.40,IP是10.0.12.93。然后下载了github上别人的POC,稍微修改了里面的代码,使它支持jsp-servlet的利用方式。首先测试任意文件下载漏洞,结果如下:

然后测试文件包含漏洞,我在webapps/examples/新建一个文件evilman.png,当做通过某接口上传的头像:

其中只有一句jsp:

<% out.println(new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec("whoami").getInputStream())).readLine()); %>

最后,验证结果如下:

work/Catalina/localhost/examples/org/apache/jsp/目录下有新生成的编译后的jsp代码,可以看到其中_jspSerive()方法的实现方式:

防御措施

  • tomcat升级到最新版
  • 临时禁用AJP协议端口,将conf/server.xml文件中8009的Connector注释掉

参考链接

  1. https://blog.csdn.net/qq_35262405/article/details/101780644
  2. https://blog.csdn.net/w1992wishes/article/details/79242797
  3. https://blog.csdn.net/u010900754/article/details/80154933
  4. https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html
  5. https://www.anquanke.com/post/id/199448
  6. https://www.anquanke.com/post/id/199347

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖