Tomcat-AJP协议漏洞分析

youncyb 发布于 2020-02-22 2745 次阅读 Java Web 安全



不积跬步,无以至千里;不积小流,无以成江海。

                      ——《劝学》

1. 前置知识

1. AJP协议

当启动tomcat时,tomcat会暴露两种连接方式:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

第一种开在8080端口,用于平常的http请求,以8080端口作为web服务时,tomcat不仅接管静态文件的解析,如:html、js、图片等,而且处理后端动态请求。但是由于tomcat对于静态文件的处理比不上apache等,所以设计者想出了一个办法将tomcat与其他的web服务器连接起来,即:例如apache专门用于处理静态文件,而tomcat用来处理servlet请求。而将两者连接起来的便是AJP协议。

我们知道运行在tomcat服务器上的是servlet就是一个个小程序,用于处理到来的请求。web服务器会将会将消息分为不同的种类发送给servlet container,类型如下:

Code 	Type of Packet 	Meaning
2 	    Forward Request 	Begin the request-processing cycle with the following data
7 	    Shutdown 	        The web server asks the container to shut itself down.
8 	    Ping 	            The web server asks the container to take control (secure login phase).
10 	    CPing 	            The web server asks the container to respond quickly with a CPong.
none 	Data 	            Size (2 bytes) and corresponding body data.

servlet container在处理完对应的消息后,也会将消息以不同类型的种类发送给web服务器:

Code 	Type of Packet 	Meaning
3 	    Send Body Chunk 	Send a chunk of the body from the servlet container to the web server (and presumably, onto the browser).
4 	    Send Headers 	    Send the response headers from the servlet container to the web server (and presumably, onto the browser).
5 	    End Response 	    Marks the end of the response (and thus the request-handling cycle).
6 	    Get Body Chunk 	    Get further data from the request if it hasn't all been transferred yet.
9 	    CPong Reply 	    The reply to a CPing request

web服务器到servlet container的消息会以0x1234作为开头然后是lengthcode type

servlet containerweb服务器则会以0xAB开头然后是lengthcode type

重点来看看Foward Request类型的结构体:

AJP13_FORWARD_REQUEST :=
    prefix_code      (byte) 0x02 = JK_AJP13_FORWARD_REQUEST
    method           (byte)
    protocol         (string)
    req_uri          (string)
    remote_addr      (string)
    remote_host      (string)
    server_name      (string)
    server_port      (integer)
    is_ssl           (boolean)
    num_headers      (integer)
    request_headers *(req_header_name req_header_value)
    attributes      *(attribut_name attribute_value)
    request_terminator (byte) OxFF
----------------------------------------------------------
The request_headers have the following structure:

req_header_name := 
    sc_req_header_name | (string)  [see below for how this is parsed]

sc_req_header_name := 0xA0xx (integer)

req_header_value := (string)
----------------------------------------------------------

The attributes are optional and have the following structure:

attribute_name := sc_a_name | (sc_a_req_attribute string)

attribute_value := (string)

其中method又有如下类型:

Command Name	Code
OPTIONS	        1
GET	            2
HEAD	        3
POST	        4
PUT	            5
DELETE      	6
TRACE	        7
PROPFIND	    8
PROPPATCH	    9
MKCOL	        10
COPY	        11
MOVE	        12
LOCK	        13
UNLOCK	        14
ACL	            15
REPORT	        16
VERSION-CONTROL	17
CHECKIN	        18
CHECKOUT	    19
UNCHECKOUT	    20
SEARCH	        21
MKWORKSPACE	    22
UPDATE	        23
LABEL	        24
MERGE	        25
BASELINE_CONTROL    26
MKACTIVITY	    27

attribute的类型:

Information	    Code Value	Note
?context	    0x01	    Not currently implemented
?servlet_path	0x02	    Not currently implemented
?remote_user	0x03	
?auth_type	    0x04	
?query_string	0x05	
?route	        0x06	
?ssl_cert	    0x07	
?ssl_cipher	    0x08	
?ssl_session	0x09	
?req_attribute	0x0A	    Name (the name of the attribut follows)
?ssl_key_size	0x0B	
?secret	0x0C	
?stored_method	0x0D	
are_done	    0xFF	    request_terminator
2. DefaultServlet

DefaultServlet配置位于tomcatconf/web.xml中,这个web.xml用来弥补我们开发程序中web.xml没有写到的地方,比如:一些静态资源我们并没有去匹配的url,这时default servlet就派上用场了。

    <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>
3. JspServlet

JspServlet同样位于tomcatconf/web.xml中,用于处理后缀为jsp、jspx的请求:

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

2. webapp目录下的任意文件下载分析

根据最开始安恒发出文章讲到AJP程序处理后的消息可以控制request的三个属性:

javax.servlet.include.request_uri
javax.servlet.include.path_info
javax.servlet.include.servlet_path

我们就开始分析为什么这三个属性会被控制?

从调用栈可以看到,消息先是被 socket程序处理,然后才传递到AjpProcesser:

跟到org/apache/coyote/ajp/AjpProcessor.class:122可以看到传入的消息类型是2,正好是Forward Request类型

继续往下调试,跟进位于org/apache/coyote/ajp/AjpProcessor.class:169的函数:this.prepareRequest()

可以看到prepareRequest()函数用于设置Forward Request结构体:

直接去找设置attribute的地方,可以看到在org/apache/coyote/ajp/AjpProcessor.class:391属性的值为10,根据之前讲到的来看10代表req_attribute,即用来设置request对象的属性:

跳到org/apache/coyote/ajp/AjpProcessor.class:433,可以看到javax.servlet.include.request_uri被赋值为/

继续循环,javax.servlet.include.path_info 被赋值为WEB-INF/web.xml

javax.servlet.include.servlet_path 被赋值为/

当三个属性赋值完成后,就进入了servlet的映射,跟进org/apache/coyote/ajp/AjpProcessor.class:187this.getAdapter().service(this.request, this.response)函数:

一直跟进invoke函数,然后来到org/apache/catalina/core/StandardContextValve.class:26,可以看到这里对WEB-INFMETA-INF进行了限制,这就是我们为什么不能直接访问WEB-INF目录的原因:

分析到这里,可能有同学就会发现和正常的http请求部分的调用栈开始重合了,没错,到了这里就会进入一系列的value类和filter类,最后把请求传入到DefaultServlet类:

来到org/apache/catalina/servlets/DefaultServlet.class:275doGet()函数,继续跟进this.serveResource(request, response, true, this.fileEncoding)方法:

继续跟进org/apache/catalina/servlets/DefaultServlet.class:486this.resources.getResource(path)函数:

发现在获取资源之前,tomcat还会对传入的路劲进行一次合法性验证,看到:org/apache/catalina/webresources/StandardRoot.class:205validate()函数:

继续跟进:org/apache/catalina/webresources/StandardRoot.class:213RequestUtil.normalize(path, false)函数,可以看到normalize函数会对目录穿越符号../或者./进行修正,这也就解释了我们为什么跳不到更上一级的目录去读文件:

3. 任意文件包含分析

跟到/org/apache/jasper/servlet/JspServlet.class:171service函数,可以看到jsp文件的路径会取javax.servlet.include.servlet_pathjavax.servlet.include.path_info的拼接:

接着传入到org/apache/jasper/servlet/JspServlet.class:201this.serviceJspFile(request, response, jspUri, precompile)去编译:

4. 参考

The Apache Tomcat Connectors - AJP Protocol Reference
Tomcat源码分析之 doGet方法
Web.xml详解
【WEB安全】Tomcat-Ajp协议漏洞分析
CNVD-2020-10487-Tomcat-Ajp-lfi