在 Exploitee.rs,有时我们寻找有趣的设备进行攻击,而有时设备会找到我们。今天,我们将谈谈最近一段时间(我们发现自己处于后一种情况)我们在攻击一系列 Western Digital 的网络附加存储设备方面的经验。
去年年中,我开始寻找一个能借助 Plex(一款我最近很喜欢用的媒体播放器)提供硬件解码功能的网络附加存储设备。经过一番研究,我订购了一个 Western Digital“MyCloud”PR4100。这个设备满足了我的所有需求,并得到一个朋友强烈推荐。在将 NAS 添加到我的网络并首次访问设备的管理页面之后,我对在网络中不经适当审核而添加新设备感到厌烦。所以,我登录该设备,启用 SSH 访问,并查看该设备的 Web 服务器功能如何工作。
💎 登录绕过
我很快发现第一个令人震惊的 bug,这个 bug 来自于一段执行用户登录检查功能(使用 cookie 或 PHP 会话变量)的代码。使用 cookie 进行身份验证不一定是一件坏事,但问题是西部数据 MyCloud 的登录界面使用它们的方式。检查下面的代码。
/lib/login_checker.php
function login_check()
{
$ret = 0;
if (isset($_SESSION['username']))
{
if (isset($_SESSION['username']) && $_SESSION['username'] != "")
$ret = 2; //login, normal user
if ($_SESSION['isAdmin'] == 1)
$ret = 1; //login, admin
}
else if (isset($_COOKIE['username']))
{
if (isset($_COOKIE['username']) && $_COOKIE['username'] != "")
$ret = 2; //login, normal user
if ($_COOKIE['isAdmin'] == 1)
$ret = 1; //login, admin
}
return $ret;
}
上述代码包含一个名为“login_check”的函数,此函数由所有后端 PHP 脚本使用,用于验证预认证的用户。上面的代码可以通过两条途径完成检查,第一是通过检查“username”和“isAdmin”的会话值,第二是(如果先前失败)尝试使用 cookie 完成相同的过程。因为 cookie 是由用户提供的,所以脚本寻找的要求可以被攻击者满足。上述会话和 cookie 的检查过程总结如下。
这意味着在任何时候使用该 PHP 脚本进行登录检查时,攻击者能够通过提供 2个特制的 cookie 值绕过检查。
在写下我发现的过程中,一个新的固件被推出来修补上述 bug。然而,这个补丁引入了一个新的漏洞,它具有与上述 bug(更新前)相同的后果。下面是包括固定代码的当前版本。
/var/www/web/lib/login_checker.php
20 function login_check()
21 {
22 $ret = 0;
23
24 if (isset($_SESSION['username']))
25 {
26 if (isset($_SESSION['username']) && $_SESSION['username'] != "")
27 $ret = 2; //login, normal user
28
29 if ($_SESSION['isAdmin'] == 1)
30 $ret = 1; //login, admin
31 }
32 else if (isset($_COOKIE['username']))
33 {
34 if (isset($_COOKIE['username']) && $_COOKIE['username'] != "")
35 $ret = 2; //login, normal user
36
37 if ($_COOKIE['isAdmin'] == 1)
38 $ret = 1; //login, admin
39
40 if (wto_check($_COOKIE['username']) === 0) //wto check fail
41 $ret = 0;
42 }
43
44 return $ret;
45 }
46 ?>
在代码的更新版本中,调用了新函数“wto_check()”(40行)。此函数使用客户端提供的用户名以及用户的 IP 地址作为二进制参数在设备上运行。如果用户当前已经登录并且没有超时,则返回值 1,否则返回 0(指示用户未登录)。“wto_check()”方法的代码如下。
/var/www/web/lib/login_checker.php
3 /*
4 return value: 1: Login, 0: No login
5 */
6 function wto_check($username)
7 {
8 if (empty($username))
9 return 0;
10
11 exec(sprintf("wto -n \"%s\" -i '%s' -c", escapeshellcmd($username), $_SERVER["REMOTE_ADDR"]), $login_status);
12 if ($login_status[0] === "WTO CHECK OK")
13 return 1;
14 else
15 return 0;
16 }
17
18 /* ret: 0: no login, 1: login, admin, 2: login, normal user */
19
在上面的代码中你可以看到,为了将用户名和 IP 地址引入作为“wto”二进制的参数,在第 11 行进行了转义处理。上面代码的问题是 PHP 方法“escapeshellcmd()”的不正确使用,“escapeshellcmd()”的正确用途是处理整个命令字符串,而不仅仅是一个参数。这是因为“escapeshellcmd()”函数不转义引号,因此攻击者可以突破引号的封装(在我们的案例中为“-n”参数),允许引入新的参数执行。因此,我们可以添加新的参数实现自我登录,而绕过对用户是否已经登录的检查。尽管我们认为仅仅通过检查IP地址和是否登录超时来验证用户是否已经登录是远远不够的。编写这段代码的程序员应该使用“escapeshellarg()”,它用于过滤独立的二进制参数,并过滤掉引号。使用“escapeshellarg()”而不是目前使用的“escapeshellcmd()”至少可阻止上述提到的攻击。
💎
命令注入错误
WDCloud Web 界面的大多数功能实际上由设备上的 CGI 脚本处理。大多数二进制文件使用相同的模式,它们从请求中获取 post / get / cookie 值,然后使用 PHP 调用中的值来执行shell命令。在大多数情况下,这些命令将使用用户提供的数据(几乎不进行清理)。例如,考虑一下以下代码(来自于该设备)。
php/users.php
15 $username = $_COOKIE['username'];
16 exec("wto -n \"$username\" -g", $ret);
上面的代码从 COOKIE 的超全局变量(包含从请求提交来的 cookie 数组索引)分配一个值给本地变量“$ username”。然后,该值作为本地方法“wto”的二进制参数立即在 PHP 的“
exec()
”调用中被使用。因为没有数据清理,使用用户名值
username=$(touch /tmp/1)
将现有的 exec 命令转换为
wto -n "$(touch /tmp/1)" -g
并在其中执行用户提供的命令。
因为参数用双引号封装且我们使用“$(COMMANDHERE)”这样的语法,命令“touch / tmp / 1”先于“wto”方法被执行,其返回值被用作“wto”方法的“-n”参数。这种导致命令注入漏洞的基本模式在Web界面使用的许多脚本中被多次使用。虽然上述的一些漏洞可能被常规的身份认证所阻止,但是通过上面提到的绕过登陆可以克服该限制。另外,值得注意的是,所有通过 Web 界面执行的命令都是完成的,因此本案例中 Web 服务器是以 root 用户的身份在运行。
💎
其他错误
可能你认为上述错误已经很严重了,但在 Web 界面中仍然存在了大量的错误(像被注释掉的正常验证一样简单的):
addons/ftp_download.php
6 //include ("../lib/login_checker.php");
7 //
8 ///* login_check() return 0: no login, 1: login, admin, 2: login, normal user */
9 //if (login_check() == 0)
10 //{
11 // echo json_encode($r);
12 // exit;
13 //}
还有其他更具特定功能性的错误,如下面的例子,允许未经验证的用户能够将文件上传到 myCloud 设备。
addons/upload.php
2 //if(!isset($_REQUEST['name'])) throw new Exception('Name required');
3 //if(!preg_match('/^[-a-z0-9_][-a-z0-9_.]*$/i', $_REQUEST['name'])) throw new Exception('Name error');
4 //
5 //if(!isset($_REQUEST['index'])) throw new Exception('Index required');
6 //if(!preg_match('/^[0-9]+$/', $_REQUEST['index'])) throw new Exception('Index error');
7 //
8 //if(!isset($_FILES['file'])) throw new Exception('Upload required');
9 //if($_FILES['file']['error'] != 0) throw new Exception('Upload error');
10
11 $path = str_replace('//','/',$_REQUEST['folder']);
12 $filename = str_replace('\\','',$_REQUEST['name']);
13 $target = $path . $filename . '-' . $_REQUEST['index'];
14
15 //$target = $_REQUEST['folder'] . $_REQUEST['name'] . '-' . $_REQUEST['index'];
16
17 move_uploaded_file($_FILES['file']['tmp_name'], $target);
18
19
20 //$handle = fopen("/tmp/debug.txt", "w+");
21 //fwrite($handle, $_FILES['file']['tmp_name']);
22 //fwrite($handle, "\n");
23 //fwrite($handle, $target);
24 //fclose($handle);
25
26 // Might execute too quickly.
27 sleep(1);
上面的代码由未经检查的身份验证组成,该段代码被调用时将简单地检索上传的文件内容,并根据用户提供的路径来决定在哪里放置新文件。
除了本博客中列出的错误,我们将在 MyCloud Web 界面中发现的大量错误放在我们的 wiki 中。尽快修复错误是我们 Exploitee.rs 的普遍追求。然而,大量的严重错误的发现意味着,我们可能需要在供应商正确修复已发布的漏洞后重新评估产品。
💎
责任披露
Exploitee.rs 通常致力于同供应商合作,确保漏洞被正确地发布。然而,上一次在维加斯的
BlackHat
大会上取得了
Pwnie奖
后,我们了解了供应商在圈子里的声誉。特别地,某供应商在忽略了向他们所提交的一组错误的严重性后,仍然赢得了“Lamest Vendor Response” Pwnie奖。厂商的忽略使得易受攻击的设备更长时间地在线(尽管已经做过了责任披露)。因此,我们转而尝试向圈内人士做出有关这些缺陷的预警,并希望用户将其设备从接入公网的网络中转移走,或是尽可能限制访问。通过这个过程,我们完全公开了我们的所有研究,并希望这会加快给用户设备打补丁的速度。
找到的 Bug 统计信息
-
1 x绕过登录
-
1 x任意文件写入
-
13 x未经身份验证的远程命令执行Bug
-
70 x需要验证命令执行Bug*
*绕过登录Bug 可以取得和“需要验证”Bug一样的效果。
💎
范围
即便不是全部也可以说是大多数,本项研究可以应用于西部数据的整个 MyCloud 系列产品。包括以下设备:
-
My Cloud
-
My Cloud Gen 2
-
My Cloud Mirror
-
My Cloud PR2100
-
My Cloud PR4100
-
My Cloud EX2 Ultra
-
My Cloud EX2
-
My Cloud EX4
-
My Cloud EX2100