Apache-Http-Server-ND002-CVE-2021-40438 mod_proxy SSRF 漏洞

原文地址:

www.leavesongs.com/PENETRATION...

影响版本

2021年9月16日,Apache官方发布了Apache httpd mod_proxy SSRF漏洞CVE-2021-40438,影响v2.4.48及以下版本。

0x00 漏洞利用

一个 SSRF 漏洞,利用方式,请求路径:

arduino 复制代码
/?unix:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA|http://www.baidu.com

即可查看百度页面的内容,如图:

算是一个可以回显的 SSRF 漏洞。

0x01 Apache Module综述

如果我们要部署一个PHP运行环境,且将Apache作为Web应用服务器,那么常用的有三种方法:

  1. Apache以CGI的形式运行PHP脚本
  2. PHP以mod_php的方式作为Apache的一个模块运行
  3. PHP以FPM的方式运行为独立服务,Apache使用mod_proxy_fcgi模块作为反代服务器将请求代理给PHP-FPM

第一种方式比较古老,性能较差,基本已经淘汰;第二种方式在Apache环境下使用较广,配置最为简单;第三种方法也有较大用户体量,不过Apache仅作为一个中间的反代服务器,更多新的用户会选择使用性能更好的Nginx替代。

这其中,第三种方法使用的mod_proxy_fcgi就是本文主角mod_proxy模块的一个子模块。mod_proxy是Apache服务器中用于反代后端服务的一个模块,而它拥有数个不同功能的子模块,分别用于支持不同通信协议的后端,比如常见的有:

  • mod_proxy_fcgi 用于反代后端是fastcgi协议的服务,比如php-fpm

    • 通常通过 SetHandler "proxy:fcgi://..." 指令配置,指定 PHP-FPM 监听的地址(IP:端口 或 Unix Socket)
  • mod_proxy_http 用于反代后端是http、https协议的服务

    • 反向代理到后端的 Java 应用服务器(如 Tomcat 在 HTTP 模式下 、Jetty、WildFly)、Node.js 应用、Python WSGI 应用(如 Gunicorn, uWSGI 在 HTTP 模式)、Ruby on Rails 应用服务器(如 Puma, Unicorn)等任何提供 HTTP(S) 接口的服务。
  • mod_proxy_uwsgi 用于反代后端是uwsgi协议的服务,主要针对uWSGI

    • 用于代理 Python WSGI 应用、Ruby Rack 应用、Perl PSGI 应用等运行在 uWSGI 服务器上的应用。
    • 特点: uWSGI 协议是一个高效的二进制协议,相比纯 HTTP 代理(mod_proxy_http)可能有轻微的性能优势,尤其是在高并发下,并支持更多 uWSGI 特有的特性。但配置通常比 HTTP 代理复杂一些。
  • mod_proxy_ajp 用于反代后端是ajp协议的服务,主要针对Tomcat

    • Apache Tomcat: 这是 AJP 协议最主要的使用场景。配置 Apache 作为 Tomcat 的前端 Web 服务器/反向代理,处理静态文件并将 JSP/Servlet 请求通过 AJP 转发给 Tomcat。
  • mod_proxy_ftp 用于反代后端是ftp协议的服务

除去mod_proxy_fcgi用于反代PHP,我们在使用Node.js、Python等脚本语言编写的应用也常常会使用mod_proxy_http作为一层反代服务器,这样中间层可以做ACL、静态文件服务等。

这次的SSRF漏洞是出在mod_proxy这个模块中的,我们就来从代码的层面分析一下它的原理是什么,究竟影响有多大。

0x02 漏洞原理分析

Building a POC for CVE-2021-40438》这篇文章中提到了这个漏洞的复现方法:当目标环境使用了mod_proxy做反向代理,比如ProxyPass / "http://localhost:8000/",此时通过请求http://target/?unix:{'A'*5000}|http://example.com/即可向http://example.com发送请求,造成一个SSRF攻击。

这里面,Apache代码中犯得错误是在modules/proxy/proxy_util.c的fix_uds_filename函数:

ini 复制代码
/*
 * In the case of the reverse proxy, we need to see if we
 * were passed a UDS url (eg: from mod_proxy) and adjust uds_path
 * as required.  
 */
static void fix_uds_filename(request_rec *r, char **url) 
{
    char *ptr, *ptr2;
    if (!r || !r->filename) return;
    if (!strncmp(r->filename, "proxy:", 6) &&
            (ptr2 = ap_strcasestr(r->filename, "unix:")) &&
            (ptr = ap_strchr(ptr2, '|'))) {
 /*
 检查三个条件必须同时满足:
文件名以 proxy: 开头(前6字符)
文件名中包含 unix: 子串(不区分大小写)
在 unix: 之后存在管道符 |
符合格式示例:
proxy:unix:/var/run/app.sock|http://backend/app
 
 */
        apr_uri_t urisock;
        apr_status_t rv;
        *ptr = '\0';
        rv = apr_uri_parse(r->pool, ptr2, &urisock);
        if (rv == APR_SUCCESS) {
            char *rurl = ptr+1;
            char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path);
            apr_table_setn(r->notes, "uds_path", sockpath);
/*
提取 UDS 路径:
解析 Unix 套接字路径(如 /var/run/app.sock)
转换为绝对路径(处理相对路径情况)
存储在请求的 notes 中,键为 uds_path(供后续模块使用)
*/
            *url = apr_pstrdup(r->pool, rurl); /* so we get the scheme for the uds */
            /* r->filename starts w/ "proxy:", so add after that */
            memmove(r->filename+6, rurl, strlen(rurl)+1);
            ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
                    "*: rewrite of url due to UDS(%s): %s (%s)",
                    sockpath, *url, r->filename);
        }
        else {
            *ptr = '|';
        }
    }
}

Apache在配置反代的后端服务器时,有两种情况:

  • 直接使用某个协议反代到某个IP和端口,比如ProxyPass / "http://localhost:8080"
  • 使用某个协议反代到unix套接字,比如ProxyPass / "unix:/var/run/www.sock|http://localhost:8080/"

第一种情况比较好理解,第二种情况的设计我觉得不是很好,相当于让用户可以使用一个Apache自创的写法来配置后端地址。那么这时候就会涉及到parse的过程,需要将这种自创的语法转换成能兼容正常socket连接的结构,而fix_uds_filename函数就是做这个事情的。

使用字符串文法来表示多种含义的方式通常暗藏一些漏洞,比如这里,进入这个if语句需要满足三个条件:

  • r->filename的前6个字符等于proxy:
  • r->filename的字符串中含有关键字unix:
  • unix:关键字后的部分含有字符|

当满足这三个条件后,将unix:后面的内容进行解析,设置成uds_path的值;将字符|后面的内容,设置成rurl的值。

举个例子,前面介绍中的ProxyPass / "unix:/var/run/www.sock|http://localhost:8080/",在解析完成后,uds_path的值等于/var/run/www.sockrurl的值等于http://localhost:8080/

看到这里其实都没有什么问题,那么我们肯定会思考,r->filename是从哪来的,用户可控吗,为什么?

这时就要说到另一个函数,proxy_hook_canon_handler,这个函数用于注册canon handler,比如:

可以看到,每一个mod_proxy_xxx都会注册一个自己的canon handler,canon handler会在反代的时候被调用,用于告诉Apache主程序它应该把这个请求交给哪个处理方法来处理。

比如,我们看到mod_proxy_httpproxy_http_canon函数:

c 复制代码
static int proxy_http_canon(request_rec *r, char *url)
{
    // ...(省略变量声明部分)
    // 第一部分:协议识别
    // 检查URL是否以"http:"开头(不区分大小写)
    if (strncasecmp(url, "http:", 5) == 0) {
        url += 5;        // 跳过协议前缀
        scheme = "http"; // 设置协议类型
    }
    // 检查URL是否以"https:"开头
    else if (strncasecmp(url, "https:", 6) == 0) {
        url += 6;         // 跳过协议前缀
        scheme = "https"; // 设置协议类型
    }
    // 如果不是HTTP/HTTPS协议,直接返回拒绝处理
    else {
        return DECLINED;
    }
    // 获取协议对应的默认端口(HTTP=80, HTTPS=443)
    port = def_port = ap_proxy_port_of_scheme(scheme);
    // 第二部分:网络位置解析
    // 解析主机名和端口号(处理可能包含的用户名/密码等)
    ap_proxy_canon_netloc(r->pool, &url, NULL, NULL, &host, &port);
    
    // 第三部分:路径处理(根据代理类型)
    switch (r->proxyreq) {
    default: /* 未知代理类型 */
    case PROXYREQ_REVERSE: // 反向代理
        if (apr_table_get(r->notes, "proxy-nocanon")) {
            // 禁用规范化时直接使用原始路径
            path = url;   /* this is the raw path */
        }
        else {
            // 规范化URL路径(处理编码等)
            path = ap_proxy_canonenc(r->pool, url, strlen(url),
                                     enc_path, 0, r->proxyreq);
            // 保留查询字符串(GET参数)
            search = r->args;
        }
        break;
        
    case PROXYREQ_PROXY: // 正向代理
        // 直接使用原始URL(包含完整路径和查询参数)
        path = url;
        break;
    }
    
    // 路径处理失败检查
    if (path == NULL)
        return HTTP_BAD_REQUEST; // 返回400错误
    
    // 第四部分:端口处理
    // 如果使用非默认端口,生成":端口号"格式的字符串
    if (port != def_port)
        apr_snprintf(sport, sizeof(sport), ":%d", port);
    else
        sport[0] = '\0'; // 默认端口则留空
    
    // 第五部分:IPv6地址特殊处理
    // 如果主机名包含冒号(IPv6地址),添加方括号包裹
    if (ap_strchr_c(host, ':')) { /* if literal IPv6 address */
        host = apr_pstrcat(r->pool, "[", host, "]", NULL);
    }
    
    // 第六部分:构建最终文件名
    // 格式:proxy:[scheme]://[host][:port]/[path][?query]
    r->filename = apr_pstrcat(r->pool, 
        "proxy:",         // 代理标识前缀
        scheme, "://",    // 协议
        host,             // 主机名(可能是域名或IP)
        sport,            // 端口(非默认时包含冒号前缀)
        "/",              // 路径分隔符
        path,             // URL路径
        (search) ? "?" : "",       // 查询参数起始符
        (search) ? search : "",    // 实际查询参数
        NULL);
    
    return OK; // 返回成功
}

这个函数中有三个主要的部分,第一部分检查了配置中的url的开头是不是http:https:,如果不是,说明这个请求不该由mod_proxy_http模块处理,后续的过程跳过;第二部分,用各种方式获取到scheme、host、port、path、search等几个URL的组成变量;第三部分,拼接proxy:、scheme、://、host、sport、/、path、search,成为一个字符串,赋值给r->filename

这里面,scheme、host、sport来自于配置文件中配置的ProxyPass,而path、search来自于用户发送的数据包。也就是说,r->filename中的后半部分是用户可控的。

那我们回看前面的fix_uds_filename函数,它在r->filename中查找关键字unix:,并将这个关键字后面直到|的部分作为unix套接字地址,而将|后面的部分作为反代的后端地址。

我们可以通过请求的path或者search来控制这两个部分,控制了反代的后端地址,这也就是为什么这里会出现SSRF的原因。

0x03 限制绕过

当然,这里面有一个问题,那就是Apache在正常情况下,因为识别到了unix套接字,所以会把用户请求发送给这个本地文件套接字,而不是后端URL。

可以来做个测试,我们发送这样一个请求:

javascript 复制代码
GET /?unix:/var/run/test.sock|http://example.com/ HTTP/1.1...

此时会得到一个503错误,错误日志会反馈这样的结果:

less 复制代码
[Mon Oct 18 00:14:38.634795 2021] [proxy:error] [pid 782180:tid 140737306797824] (2)No such file or directory: AH02454: HTTP: attempt to connect to Unix domain socket /var/run/test.sock (192.168.1.1) failed
[Mon Oct 18 00:14:38.634875 2021] [proxy_http:error] [pid 782180:tid 140737306797824] [client 192.168.1.142:59696] AH01114: HTTP: failed to make connection to backend: httpd-UDS

找不到unix套接字/var/run/test.sock,这是当然。

我们不能让他把请求发送到unix套接字上,而是发送给我们需要的|后面的地址。

国外那位作者给出了一个非常巧妙的方法,在fix_uds_filename函数中,unix套接字的地址来自于下面这两行代码:

scss 复制代码
char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path);
apr_table_setn(r->notes, "uds_path", sockpath);

如果这里ap_runtime_dir_relative函数返回值是null,则后面获取uds_path时将不会使用unix套接字地址,而变成普通的TCP连接:

rust 复制代码
uds_path = (*worker->s->uds_path ? worker->s->uds_path : apr_table_get(r->notes, "uds_path"));
if (uds_path) {
    if (conn->uds_path == NULL) {
        /* use (*conn)->pool instead of worker->cp->pool to match lifetime */
        conn->uds_path = apr_pstrdup(conn->pool, uds_path);
    }
    // ...
    conn->hostname = "httpd-UDS";
    conn->port = 0;
}
else {
    // ...
    conn->hostname = apr_pstrdup(conn->pool, uri->hostname);
    conn->port = uri->port;
    // ...
}

那么如何让ap_runtime_dir_relative的返回值是null?ap_runtime_dir_relative函数最后引用了apr库中的apr_filepath_merge函数,它的主要作用就是路径的join,用于处理相对路径、绝对路径、../连接。

这个函数中,当待join的两段路径长度+4大于APR_PATH_MAX,也就是4096的时候,则函数会返回一个路径过长的状态码,导致最后unix套接字的值是null:

ini 复制代码
rootlen = strlen(rootpath);
maxlen = rootlen + strlen(addpath) + 4; 
/* 
    * 4 for slashes at start, after
    * root, and at end, plus trailing
    * null 
*/
if (maxlen > APR_PATH_MAX) {
    return APR_ENAMETOOLONG;
}

也就是说,我们只需要在unix:|之间传入内容长度大概超过4092的字符串,就能构造出uds_path为null的结果,让Apache不再发送请求给unix套接字。

最后,这样构造出的请求成功触发SSRF漏洞:

Apache官方对这个漏洞的修复也比较简单,因为用户只能控制r->filename的后半部分,而前半部分proxy:{scheme}://{host}{sport}/来自于配置文件,所以最新版改成检查其开头是不是`proxy![:unix:],这一部分用户无法控制。

0x04 mod_proxy_fcgi是否存在漏洞?

我们前文都以mod_proxy_http作为例子来研究,而在Apache+PHP环境下,mod_proxy_fcgi的使用频率更高,那么它是否也会被SSRF漏洞影响呢?

这个漏洞出现在modules/proxy/proxy_util.c的fix_uds_filename函数,理论上是mod_proxy的漏洞,那么它的子模块应该都会被影响,但这个漏洞中有一个很关键的变量是r->filename,他是否可控决定了后面的利用是否可以成功。

我们看一下mod_proxy_fcgi的canon函数:

arduino 复制代码
static int proxy_fcgi_canon(request_rec *r, char *url)
{
    char *host, sport[7];
    const char *err;
    char *path;
    apr_port_t port, def_port;
    fcgi_req_config_t *rconf = NULL;
    const char *pathinfo_type = NULL;
    if (ap_cstr_casecmpn(url, "fcgi:", 5) == 0) {
        url += 5;
    }
    else {
        return DECLINED;
    }
    // ...
    if (apr_table_get(r->notes, "proxy-nocanon")) {
        path = url;   /* this is the raw path */
    }
    else {
        path = ap_proxy_canonenc(r->pool, url, strlen(url), enc_path, 0,
                             r->proxyreq);
    }
    if (path == NULL)
        return HTTP_BAD_REQUEST;
    r->filename = apr_pstrcat(r->pool, "proxy:fcgi://", host, sport, "/",
                              path, NULL);
    // ...
}

可见,这里的r->filename等于proxy![:fcgi:](https://wiki.xazlsec.com/static/editor.md/plugins/emoji-dialog/emoji/fcgi.png)//{host}{sport}/{path},相比于mod_proxy_http少了search。不过,path仍然是用户可以控制的,我们可以尝试发送这样的数据包:

bash 复制代码
GET /unix:testtest|http://example.com/1.php HTTP/1.1
...

经过调试可见,path中的|ap_proxy_canonenc函数编码成了%7C:

没有|,后面也就无法完成SSRF利用了。

0x05 哪些模块受到影响

那么,我们其实可以认为,如果r->filename有部分可控,且可控的部分没有被编码(不是path),这个模块就会受到SSRF漏洞的影响。

对这个结论我没有逐一测试考证,我仅挑选另一个较为常用的模块mod_proxy_ajp来复现漏洞。

mod_proxy_ajp是用于反代Tomcat的一个Apache模块,Tomcat在8.5.51版本以前默认会开启两个端口8080和8009(所以同时存在apache和8009端口可以作为漏洞存在的一个表现),分别对应HTTP协议和AJP协议。HTTP协议好理解,AJP协议是一个二进制协议,通信协议相比起来效率更高。所以以前很多运维人员会将Tomcat假设在Apache之后,然后二者之间使用AJP协议通信。

Tomcat 8.5.51之后的版本受到Ghostcat漏洞影响不再默认开放8009端口。

Apache下有两个模块能实现AJP的反代通信:

  • mod_proxy_ajp 这就是mod_proxy的一个子模块,由Apache HTTPd官方维护
  • mod_jk 这是Tomcat官方维护的一个Apache模块,更加出名用户也更多

由于mod_jk不是用mod_proxy的代码,所以不受到影响,我们今天仅测试mod_proxy_ajp。

简单部署一个开放8009端口的Tomcat服务器,并配置好mod_proxy_ajp进行调试,可见其proxy_ajp_canon函数r->filename中是包含search的:

arduino 复制代码
static int proxy_ajp_canon(request_rec *r, char *url)
{
    char *host, *path, sport[7];
    char *search = NULL;
    const char *err;
    apr_port_t port, def_port;
    /* ap_port_of_scheme() */
    if (strncasecmp(url, "ajp:", 4) == 0) {
        url += 4;
    }
    else {
        return DECLINED;
    }
    // ...
    r->filename = apr_pstrcat(r->pool, "proxy:ajp://", host, sport,
                              "/", path, (search) ? "?" : "",
                              (search) ? search : "", NULL);
    return OK;
}

那么按照我们的预测,这里也会存在SSRF漏洞。果然测试成功:

那么,mod_proxy_http2、mod_proxy_balancer、mod_proxy_wstunnel等这些模块也会受到影响,而mod_proxy_uwsgi、mod_proxy_scgi等模块不受影响。我没有严格验证,有兴趣的同学可以自己下去调试一下,也许还能找到绕过方法。

0x06 几个常见问题和总结

一个大家问的比较多的问题:这个SSRF漏洞是否能够POST?答案是肯定的,理解了原理的同学肯定能明白,我们实际上是控制了反向代理的目标服务器地址。既然是反向代理,那么实际上用户请求的大部分原始数据都会被直接转发给后端,所以,我们只需要发送POST请求,即可让执行POST的SSRF,比如:

另一个,这个SSRF漏洞是否可以打本地的unix socket?答案是肯定的。原本这个漏洞的第一请求目标就是本地的unix套接字,我们使用4092个超长search绕过了这个限制让他可以打任意远程地址,只要让它回归原本的方法就可以打本地的unix套接字了:

打本地unix套接字的好处是可以攻击类似于Docker、Supervisor这样的本地服务。

最后一个问题,这个SSRF漏洞是否可以攻击一些非HTTP协议的服务?答案也是肯定的。TCP是一个数据流,即使我们打出的数据包前面有HTTP头,这并不影响后续正常的满足二进制协议的数据流的发送与接收。不过有一个例外情况,如果目标服务有一些特殊的操作,类似于高版本redis读取到一些特殊的HTTP数据段就断开TCP连接这样的操作,那么可能需要进行一些额外绕过了。

总结一下,这个SSRF漏洞的本质是Apache在解析反代服务URL的时候,由于对unix:位置要求不严格,导致用户的输入可以控制反代的逻辑,最终导致反代URL被控制,造成SSRF漏洞。

0X07靶场复现

arduino 复制代码
vulhub/httpd/CVE-2021-40438 
http://192.168.180.129:8080/

使用burpsuite发送对应的poc

可以看到对应的网络响应,但是在测试的过程中,我发现有些其他的问题,有一些外部网站无法访问,比如

但是在其后再次添加一个'/'之后,又重新可以访问到对应的页面,但是302同样代表了重定向,姑且也算利用成功

0x08漏洞定位

要判断 Apache 服务器是否使用了 mod_proxy 模块作为反向代理,可以通过以下外部观察方法(无需服务器内部访问权限):


1. 检查 HTTP 响应头

使用 curl 或浏览器开发者工具检查响应头,寻找代理特征字段:

arduino 复制代码
curl -I http://example.com

关键标志:

  • Via 字段 : 若出现 Via: 1.1 apache(或类似值),表明请求经过 Apache 代理。

    makefile 复制代码
    HTTP/1.1 200 OK
    Via: 1.1 apache
  • X-Forwarded-* 系列头 : 如 X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto 等,是代理的常见标识。

    makefile 复制代码
    X-Forwarded-For: 192.168.1.100
    X-Forwarded-Host: example.com

2. 分析错误页面内容

触发后端错误(如访问不存在的路径),观察错误页面的 Server 标识

  • 若错误页面由 后端服务生成(如 Tomcat、Node.js 等),而非 Apache 默认错误页,则存在反向代理。
  • 示例:访问 http://example.com/404-test 返回 Tomcat 的 404 页面。

3. 测试非常规路径

访问后端服务特有的路径(如 /actuator/info (Spring Boot) 或 /phpinfo.php (PHP)):

  • 若路径能访问但内容与主站无关,表明代理将请求转发到了特定后端。

    bash 复制代码
    curl http://example.com/phpinfo.php  # 返回 PHP 配置而非 Apache 默认页

4. 对比端口服务

检查非标准端口(如 8080)是否提供相同服务:

arduino 复制代码
curl http://example.com:8080
  • http://example.com:80http://example.com:8080 内容一致,说明 Apache 在 80 端口代理了后端 8080 端口的服务。

5. SSL/TLS 证书分析

对 HTTPS 站点:

  • 若证书的 Subject 域名 与访问域名不符(如证书为 *.internal.com 但访问的是 public-site.com),则存在反向代理。

6. 路径重定向行为

测试路径重定向:

arduino 复制代码
curl -L http://example.com/api
  • /api 被重定向到其他域名(如 http://backend-server/api),或返回内容与主站不同,则可能配置了代理:

    bash 复制代码
    # Apache 配置示例:代理 /api 到后端
    ProxyPass "/api" "http://backend-server/"

7. 使用在线扫描工具

利用第三方服务探测:

  • Wappalyzer(浏览器插件):检测服务器技术栈,若显示后端技术(如 Nginx、Tomcat)但实际由 Apache 提供服务,则存在代理。
  • SecurityHeaders.io :检查响应头中的代理特征(如 Via)。
相关推荐
Wcy30765190667 小时前
初级安全课第二次作业
安全
刘哥测评技术zcwz6268 小时前
亚马逊自养号测评实战指南:从环境搭建到安全提排名
网络·安全·网络安全
忘川w8 小时前
NISP-PTE基础实操——代码审计
android·笔记·安全·网络安全
wey chan15 小时前
Elasticsearch X-Pack安全功能未启用的解决方案
安全·elasticsearch·jenkins
Synfuture阳途16 小时前
2025年终端安全管理系统的全方位解析,桌面管理软件的分析
网络·安全·web安全
rongqing201916 小时前
安全告警研判流程
安全
花海如潮淹17 小时前
云原生安全工具:数字基础设施的免疫长城
经验分享·笔记·安全·云原生
wey chan17 小时前
Docker安装Elasticsearch 7.17.0和Kibana 7.17.0并配置基础安全
安全·elasticsearch·docker
Neolock18 小时前
从一开始的网络攻防(四):XSS
前端·网络·安全·web安全·xss