Apache 是什麼?模組是幹嘛?

Apache 是高度模組化的 Web 伺服器;處理一個 HTTP 請求時,很多模組會共同讀寫 request_rec 的欄位(如 r->filenamer->args)。不同模組對同一欄位的「語意」若不一致,就可能誤判。Orange Tsai 的研究把這些不一致系統化,歸納出三大類 Confusion 攻擊,其中之一就是 Filename Confusion

Filename Confusion 的核心

多個模組對 r->filename 的理解不同——有的把它當檔案路徑,有的把它當URL。這種不一致會造成安全問題。

關鍵:URL 編碼 %3F

  • 在網址裡 ? 代表「後面是 query string」。
  • 如果要在網址中表示字面上的 ?,需要 URL 編碼:?%3F
  • Apache 在不同處理階段可能把 %3F 解回 ?,這就是漏洞能被利用的時機差

1. RewriteRule(mod_rewrite

RewriteRulemod_rewrite 用來改寫 URL的指令。語法:

1
RewriteRule Pattern Substitution [Flags]
  • Pattern(樣式):正則表達式,用來比對目前請求的 URL 路徑。
  • Substitution(替換結果):匹配後要改到哪裡(可為檔案路徑或 URL)。可用 $1$2 這類反向引用插入 Pattern 抓到的內容。
  • Flags(旗標,可選):放在 [] 內,調整這條規則的行為,例如:

L:命中後停止再處理後續規則(Last)。
QSA:保留原始 query string 並附加到新 URL。
H=…:指定處理器(Handler),例如 H=application/x-httpd-php

小例子(Pattern / Substitution / Flags)

想把 https://example.com/user/orange 對應到伺服器檔案 /var/www/user/orange/profile.yml

1
2
RewriteEngine On
RewriteRule "^/user/(.+)$" "/var/www/user/$1/profile.yml"
  • Pattern:^/user/(.+)$ 會抓到 /user/ 後面的使用者名稱到 $1
  • Substitution:/var/www/user/$1/profile.yml → 例如 $1=orange 就是 /var/www/user/orange/profile.yml
  • 這裡沒寫 Flags,使用預設行為。

1‑1-1 Path Truncation(路徑截斷)

成因

mod_rewrite 在套用 RewriteRule 後,會把替換結果當「URL」處理,呼叫 splitout_queryargs()? 後的部分視為 query 分離。當替換結果其實是檔案路徑時,還是會被「當 URL 來切」,於是產生可控截斷

1
2
3
4
RewriteEngine On
RewriteRule "^/user/(.+)$" "/var/user/$1/profile.yml"
# 攻擊請求(%2F = /, %3F = ?)
# /user/orange%2Fsecret.yml%3F  → 讀到 /var/user/orange/secret.yml

因為 %3F 在後續階段被視為 ?/profile.yml 被當作 query 切掉了。

splitout_queryargs() 在幹嘛?

它把替換結果中的 ? 後段剝離到 r->args(query),? 前面留在 r->filename

r->filename = "/var/user/orange/profile.yml?debug=1"
               └─── 路徑 ───┘ └─ query ─┘
r->args     = "debug=1"   ← 被 splitout_queryargs() 剝出去

1‑1‑2 Mislead RewriteFlag Assignment(誤導旗標設定)

利用點

先 Pattern 比對 → 套 Flags → 再把結果當 URL 拆 query

比對階段使用 %3F(編碼的 ?)讓請求看起來像是 .php,使規則套上不該有的 Flag(例如指定 PHP Handler)。等到拆 query之後,實際要處理的檔案變回 .gif,但 Flag 不會被移除,導致圖片被當作 PHP 來跑。

規則長這樣

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

攻擊步驟

  1. 上傳看似普通的圖片 /upload/1.gif,實際內容藏有 PHP
    例如:

    1
    
    <?php echo shell_exec('id'); ?>
    
  2. 送出請求:

    /upload/1.gif%3Faaa.php
    
  3. Pattern 比對%3F 尚未解碼,/upload/1.gif%3Faaa.php 尾端看起來是 .php → 命中 → 套上 H=application/x-httpd-php

  4. 拆 query%3F 解為 ??aaa.php 被分離到 r->args,留下 /upload/1.gif

  5. 實際處理:雖然是 .gif,但已指定 PHP Handler → 由 PHP 執行圖片內的程式碼。

對照表

步驟正常(安全)漏洞利用(1‑1‑2)
1. 上傳檔案只能上傳圖片;.php 拒絕上傳 .gif(內藏 PHP)通過檢查
2. 發送請求/upload/1.gif → 回傳圖片/upload/1.gif%3Faaa.php
3. Rewrite 比對.gif 不匹配 ^(.+\.php)$,不套 Flag%3Faaa.php 讓 Pattern 命中,套 H=application/x-httpd-php
4. splitout_queryargs無變化%3F??aaa.php 被拆到 r->args,目標成 /upload/1.gif
5. 處理器靜態檔案處理器仍由 PHP Handler 處理 .gif
6. 結果顯示圖片圖片被當 PHP 執行(RCE 風險)

1-2. ACL(Access Control List)與 <Files> / PHP‑FPM

要先了解以下三個角色:

  • mod_proxy:代理模組,負責把請求轉送到別的服務。

mod_proxy_fcgi:把請求用 FastCGI 協定轉給 PHP‑FPM。

  • PHP‑FPMPHP FastCGI Process Manager,Apache 把 .php 請求交給它執行。
  • <Files>(ACL):針對「檔案名稱」設定限制,例如特定檔案需登入才可訪問。

常見把 .php 交給 PHP‑FPM 的做法

1
2
3
<FilesMatch ".+\.ph(?:ar|p|tml)$">
  SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost"
</FilesMatch>

意思是:

遇到 .php.phtml… 就不要由 Apache 直接讀檔,而是交給 mod_proxy_fcgi → PHP‑FPM。


1‑2-1 ACL Bypass(存取控制繞過)

情境:用 <Files> 來保護單一檔案,例如:

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

正常流程(安全)

  1. 請求 /admin.php
  2. ACL 檢查:r->filename = admin.php → 命中 <Files "admin.php"> → 要求驗證。
  3. 驗證通過後,交給 PHP‑FPM 執行 admin.php

漏洞流程(繞過)

  1. 請求:/admin.php%3Fooo.php%3F?

  2. ACL 檢查:此時尚未解碼 → r->filename = admin.php%3Fooo.php不等於 admin.php → 不要求驗證。

  3. SetHandler 命中:仍視為 .php → 交給 mod_proxy_fcgi

  4. mod_proxy_fcgi 轉交:把 r->filename 改寫為:

    proxy:fcgi://127.0.0.1:9000/var/www/html/admin.php?ooo.php
    
  5. PHP‑FPM 行為

  • 去掉 proxy:fcgi://… 前綴與主機資訊,只留 /var/www/html/admin.php?ooo.php
  • 找到第一個 ?截斷/var/www/html/admin.php
  • 實際執行 admin.php
  1. 結果:受保護的 admin.php 被直接執行,完全跳過 ACL

對照表

步驟正常(安全)漏洞利用(ACL Bypass)
1. 請求 URL/admin.php/admin.php%3Fooo.php
2. ACL 檢查(<Files>r->filename = admin.php → 要驗證r->filename = admin.php%3Fooo.php → 覺得不需驗證
3. SetHandler命中 → 交給 mod_proxy_fcgi同左
4. mod_proxy_fcgiproxy:fcgi://…/admin.phpproxy:fcgi://…/admin.php?ooo.php
5. PHP‑FPM執行 admin.php(無 query)截斷 ?ooo.php 後仍執行 admin.php
6. 結果驗證通過者才能執行未驗證也能執行(ACL 被繞過)

受影響的常見保護寫法(都可能被 %3F 打穿):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<Files "php-info.php">   
Require ip 127.0.0.1   
</Files>

<Files "adminer.php">    
Deny from all          
</Files>

<Files "xmlrpc.php">     
Deny from all          
</Files>

<Files "cron.php">       
Deny from all          
</Files>

修補辦法

  • 升級 Apache 至 2.4.60+:新版本把舊有危險行為改為預設拒絕;請勿開啟 RewriteOptions UnsafeAllow3F(除非完全理解風險)。
  • 避免用 <Files> 保護單一 PHP 檔(在 PHP‑FPM 架構下)。改用 <Location> / <LocationMatch>(比對 URL 而非檔名),或把敏感腳本移出 DocumentRoot,只透過應用層載入。
  • Rewrite 規則審視:不要讓「使用者可控片段」直接拼到檔案路徑;能用 mod_aliasAlias/Redirect)就避免過度使用 mod_rewrite 做檔案映射。
  • 監控與偵測
  • 日誌尋找可疑編碼:%3F%2F
  • 特別是「非 .php 路徑 + %3F...php」的請求模式;
  • 除錯時可暫時開 LogLevel rewrite:trace2 觀察改寫流程(完畢務必關閉)。

誤區/備註

  • 不是「PHP Handler 會神奇多做什麼」,而是被錯誤套用到不該由 PHP 處理的檔案上。
  • %3F 的解碼與處理發生在不同階段;理解「哪一步解、何時解」是這類漏洞的核心。
  • 範例程式碼僅用於安全測試與理解機制,請勿在生產環境或未授權系統操作。

文獻來源:Orange Tsai (Orange Tsai)