程式碼審計Day16 - 深入理解$_REQUESTS陣列
大家好,我們是紅日安全-程式碼審計小組。最近我們小組正在做一個PHP程式碼審計的專案,供大家學習交流,我們給這個專案起了一個名字叫 ofollow,noindex"> PHP-Audit-Labs 。現在大家所看到的系列文章,屬於專案 第一階段 的內容,本階段的內容題目均來自 PHP SECURITY CALENDAR 2017 。對於每一道題目,我們均給出對應的分析,並結合實際CMS進行解說。在文章的最後,我們還會留一道CTF題目,供大家練習,希望大家喜歡。下面是 第16篇 程式碼審計文章:
Day 16 - Poem
題目叫做詩,程式碼如下:
漏洞解析 :
這道題目包含了兩個漏洞,利用這兩個漏洞,我們可以往FTP連線資源中注入惡意資料,執行FTP命令。首先看到 第7行 程式碼,可以發現程式使用 cleanInput 方法過濾 GET 、 POST 、 COOKIE 資料,將他們強制轉成整型資料。然而在 第8行 處,卻傳入了一個從 REQUEST 方式獲取的 mode 變數。我們都知道超全域性陣列 $_REQUEST 中的資料,是 $_GET 、 $_POST 、 $_COOKIE 的合集,而且資料是複製過去的,並不是引用。我們先來看一個例子,來驗證這一觀點:
可以發現 REQUEST 資料絲毫不受過濾函式的影響。回到本例題,例題中的程式過濾函式只對 GET 、 POST 、 COOKIE 資料進行操作,最後拿來用的卻是 REQUEST 資料,這顯然會存在安全隱患。想了解更多 $_REQUEST 資訊,大家自己上官網學習。第二個漏洞的話,在程式碼 第21行 ,這裡用了 == 弱比較。關於這個問題,我們在前面的文章中講的也很細緻了,大家可以參考: [紅日安全]PHP-Audit-Labs題解之Day1-4 (Day4)。
至於本次案例的攻擊payload,可以使用: ?mode=1%0a%0dDELETE%20test.file ,這個即可達到刪除FTP伺服器檔案的效果。
例項分析
本次例項分析,我們分析的是 WordPress 的 All In One WP Security & Firewall 外掛。該外掛在 4.1.4 - 4.1.9 版本中存在反射型XSS漏洞,漏洞原因和本次案例中的漏洞成因一致,官方也在 4.2.0 版本中修復了該漏洞。本次,我們將以 4.1.4 版本外掛作為案例講解。
將下載下來的外掛zip包,通過後臺外掛管理上傳壓縮包安裝即可。本次發生問題的檔案在於 wp-content\plugins\all-in-one-wp-security-and-firewall\admin\wp-security-dashboard-menu.php ,為了方便大家理解,我將問題程式碼抽取出來,簡化如下:
我們可以很清晰的看到,問題就出在 第25行 的 render_tab3 方法中,這裡直接將 REQUEST 方式獲取的 tab 變數拼接並輸出。而實際上,在 第20行 已經獲取了經過過濾處理的 $tab 變數。我們來看一下 get_current_tab 方法:
過濾函式的呼叫鏈如下圖 第1行 ,接著 $tab 變數就會經過 wp_check_invalid_utf8 方法的檢測。
漏洞利用
下面我們來看看攻擊 payload (向 http://website/wp-admin/admin.php?page=aiowpsec&tab=tab3 POST資料 tab="><script>alert(1)</script>
):
可以看到成功引發XSS攻擊。我們最後再根據 payload 對程式碼的呼叫過程進行分析。首先,我們的 payload 會傳入 wp-admin/admin.php 檔案中,最後進入 第14行 的 do_action(‘toplevel_page_aiowpsec’); 程式碼。
在 wp-includes/plugin.php 檔案中,程式又呼叫了 WP_Hook 類的 do_action 方法,該方法呼叫了自身的 apply_filters 方法。
然後 apply_filters 方法呼叫了 wp-content\plugins\all-in-one-wp-security-and-firewall\admin\wp-security-admin-init.php 檔案的 handle_dashboard_menu_rendering 方法,並例項化了一個 AIOWPSecurity_Dashboard_Menu 物件。
接下來就是開頭文章分析的部分,也就是下面這張圖片:
整個漏洞的攻擊鏈就如下圖所示:
這裡還有一個小知識點要提醒大家的是,案例中 $_REQUEST[“tab”] 最後取到的是 $_POST[“tab”] 的值,而不是 $_GET[“tab”] 變數的值。這其實和 php.ini 中的 request_order 對應的值有關。例如在我的環境中, request_order 配置如下:
這裡的 “GP” 表示的是 GET 和 POST ,且順序從左往右。例如我們同時以 GET 和 POST 方式傳輸 tab 變數,那麼最終用 $_REQUEST[‘tab’] 獲取到的就是 $_POST[‘tab’] 的值。更詳細的介紹可以看如下PHP手冊的定義:
request_order string This directive describes the order in which PHP registers GET, POST and Cookie variables into the _REQUEST array. Registration is done from left to right, newer values override older values. If this directive is not set, variables_order is used for $_REQUEST contents. Note that the default distribution php.ini files does not contain the 'C' for cookies, due to security concerns.
修復建議
對於這個漏洞的修復方案,我們只要使用過濾後的 $tab 變數即可,且變數最好經過HTML實體編碼後再輸出,例如使用 htmlentities 函式等。
結語
看完了上述分析,不知道大家是否對 $_REQUEST 陣列有了更加深入的理解,文中用到的 CMS 可以從這裡( All In One WP Security & Firewall )下載,當然文中若有不當之處,還望各位斧正。如果你對我們的專案感興趣,歡迎傳送郵件到 **[email protected] 聯絡我們。 Day16** 的分析文章就到這裡,我們最後留了一道CTF題目給大家練手,題目如下:
// index.php <?php function check_inner_ip($url) { $match_result=preg_match('/^(http|https)?:\/\/.*(\/)?.*$/',$url); if (!$match_result){ die('url fomat error1'); } try{ $url_parse=parse_url($url); } catch(Exception $e){ die('url fomat error2'); } $hostname=$url_parse['host']; $ip=gethostbyname($hostname); $int_ip=ip2long($ip); return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16 || ip2long('0.0.0.0')>>24 == $int_ip>>24; } function safe_request_url($url) { if (check_inner_ip($url)){ echo $url.' is inner ip'; } else{ $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 0); $output = curl_exec($ch); $result_info = curl_getinfo($ch); if ($result_info['redirect_url']){ safe_request_url($result_info['redirect_url']); } curl_close($ch); var_dump($output); } } $url = $_POST['url']; if(!empty($url)){ safe_request_url($url); } else{ highlight_file(__file__); } //flag in flag.php ?>
// flag.php <?php if (! function_exists('real_ip') ) { function real_ip() { $ip = $_SERVER['REMOTE_ADDR']; if (is_null($ip) && isset($_SERVER['HTTP_X_FORWARDED_FOR']) && preg_match_all('#\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#s', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) { foreach ($matches[0] AS $xip) { if (!preg_match('#^(10|172\.16|192\.168)\.#', $xip)) { $ip = $xip; break; } } } elseif (is_null($ip) && isset($_SERVER['HTTP_CLIENT_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } elseif (is_null($ip) && isset($_SERVER['HTTP_CF_CONNECTING_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_CF_CONNECTING_IP'])) { $ip = $_SERVER['HTTP_CF_CONNECTING_IP']; } elseif (is_null($ip) && isset($_SERVER['HTTP_X_REAL_IP']) && preg_match('/^([0-9]{1,3}\.){3}[0-9]{1,3}$/', $_SERVER['HTTP_X_REAL_IP'])) { $ip = $_SERVER['HTTP_X_REAL_IP']; } return $ip; } } $rip = real_ip(); if($rip === "127.0.0.1") die("HRCTF{SSRF_can_give_you_flag}"); else die("You IP is {$rip} not 127.0.0.1"); ?>
題解我們會階段性放出,如果大家有什麼好的解法,可以在文章底下留言,祝大家玩的愉快!