bnbdr
WD My Cloud RCE
*slaps NAS* This baby fits so many CVEs
May 22, 2019
In this post I’ll explain how I discoverd several vulnerabilities in Western Digital NAS devices1 and used them together to execute code remotely, as root.
To take control of the NAS an attacker needs to be in the same network and know its IP address.
a smelly smell that smells… smelly
It all began after I decided to splurge and purchase the rather techy-oriented NAS option available by WD - the EX2 Ultra. Unlike other models this one gives the consumer the option to switch out the hard-drives, lets you use it without installing dedicated applications, and even works without internet connectivity.
While setting up the device I decided to open up the browser inspector and discovered an authentication bypass(by setting "isAdmin"
cookie to 1
). But alas, after updating my NAS I realized it had been fixed in a firmware update2.
I wanted to dig a little further because this bug had quite a stentch to it. After downloading the source code from WD’s website and foraging for the logic in charge of checking users’ credentials I located the coveted piece of code- It no longer relies on the user-supplied cookie:
/* fixed code in
firmware/module/crfs/web/pages/lib/login_checker.php
*/
function login_check()
{
$ret = 0;
if (!csrf_token_check()) /* this check can be bypassed easily as well */
return $ret;
if (isset($_SESSION['username']))
{
if (isset($_SESSION['username']) && $_SESSION['username'] != "")
$ret = 2; //login, normal user
if ($_SESSION['isAdmin'] == 1)
$ret = 1; //login, admin
}
return $ret;
}
Unauthenticated file upload (CVE-2019-9951)
By looking around all that “lovely” PHP code I stumbled upon one web-accessible file that failed to use the above login_check
function correctly, allowing unauthenticated file uploads to the device:
/* found in:
firmware/module/crfs/web/pages/jquery/uploader/uploadify.php
*/
include ("../../lib/login_checker.php");
if (login_check() != 1) /* i.e not-authenticated / admin */
{
/* user-controlled */
if ($_SERVER['HTTP_USER_AGENT'] == 'Shockwave Flash')
{
$headers = getallheaders();
/* also user-controlled */
if ($_GET['WD-CSRF-TOKEN'] !== $_POST['X-CSRF-Token'] ||
strrpos($headers['Content-Type'], 'multipart/form-data;'))
{
echo json_encode($r);
exit;
}
/* >> by reaching this line, you bypassed the authentication for this page*/
}
else
{
echo json_encode($r);
exit;
}
}
/* upload logic here */
However, this is not enough for exploitation since the aptly named uploadify.php
will perform a (somewhat buggy) check on the target path and will only allow writes to specific locations:
/* found in:
firmware/module/crfs/web/pages/lib/login_checker.php
*/
function check_path($path)
{
$file_path = realpath($path);
if (!$file_path) return false;
if (strncmp($file_path, "/mnt/HD", 7) != 0 &&
strncmp($file_path, "/mnt/USB", 8) != 0 &&
strncmp($file_path, "/mnt/isoMount", 13) != 0)
return false;
return true;
}
Authentication bypass (CVE-2019-9950)
The management of credentials is handled by firmware/module/crfs/cgi/login_mgr.cgi
in the cgiMain
exported function.
As it turns out the control-panel utilizes the built-in OS credentials management. This lets the control panel check the supplied username and password against the ones stored in /etc/shadow
:
Taking a look at said file shows that the nobody
account’s password is encrypted using the old CRYPT
algorithm:
nobody:pACwI1fCXYNw6:0:0:99999:7:::
One could use John the Ripper
or hashcat
to try and crack that hash, but nothing beats a good ol’ fashioned hunch:
Ironically, thanks to the fact that the user is configured on the NAS with an empty password by default, anybody could easily receive a session token:
POST http://WD/cgi-bin/login_mgr.cgi
{
"cmd": "wd_login",
"username": "nobody",
"pwd": "",
"port": "",
}
Root-RCE using low-privilege token (CVE-2019-9949)
Armed with a standard privileges token, one can now access a broader set of features attack vectors. One such feature is a file-manager (implemented mostly by webfile_mgr.cgi
), and whilst limited in functionality, it does support opening zip/tar archives.
Clicking the button showed in the picture above triggers an HTTP request to webfile_mgr.cgi
, with the sub-commnad cgi_unzip
:
Request URL: "http://wd/cgi-bin/webfile_mgr.cgi"
Request method: "POST"
cmd: "cgi_unzip"
path: "/mnt/HD/HD_a2/Public/test"
name: "myfile.zip"
After passing through the necessary routing and permission checks in firmware/module/crfs/web/pages/cgi_api.php
the cgi module forces some validation on the input by escaping the paths and executing unzip
, passing it the -t
command-line option:
/* ... in webfile_mgr.cgi */
fix_path_special_char_inline(archive_name); // user controlled
// target_dir is checked to exist in the PHP code
sprintf(cmdline, "cd %s;/usr/bin/unzip -t %s", target_dir, archive_name);
ret = popen(cmdline, "r");
/* ... */
The fix_path_special_char_inline
function loosely translates to:
def fix_path_special_char_inline(input_str):
out_str = ''
for c in input_str:
if c in '`$#%^&()+{};[]\=': # much secure
out_str += '\\'
out_str += c
return out_str
One character, egregiously missing from the above code, is the pipe(|
) character. For those less verse in the bash/batch world, When double pipe (||
) is used inbetween two commands the shell will execute the latter command if the former fails.
In this scenario, by using an archive “name” that would result in unzip
returning an error code (e.g. a non-existent path), an attacker could inject a shell command that would run as root. For example:
cd %s;/usr/bin/unzip -t 1||%MY_EVIL_COMMAND%
zip me baby one more time
As it turns out the file manager is susceptible to other attacks. Specifically, extracting an archive with symbolic links would create them with no further validation, allowing future filesystem operations to abuse them with impunity.
For example: if two consecutive cgi_untar
commands are performed on two specially crafted archives, an attacker can cause the file-manager to write anywhere(as root). This vulnerability also requires a normal user-session.
This happens because the code that extracts archives will simply untar the archive using cgi_untar
command, even if it contains symlinks. Then, when extracting a second archive, an attacker can use the previously created symlink to write into any arbitrary path.
Since the NAS uses squashfs
not all the paths are actually writeable, however, /bin/
is. For maximum amusement I decided to overwrite tar
itself, thus achieving RCE by performing the following:
- upload first archive, containing symlink
- send
cgi_untar
command: create symlink to/bin/
- upload second archive, containing payload
- send
cgi_untar
command: overwrite/bin/tar
- send
cgi_untar
command: execute my payload ๐
This second vulnerability was not deemed worthy of its own CVE-ID despite its severity.
Disclosure Timeline
- 2019-01-20 ๐ reported to psirt@wdc.com with 30-day deadline
- 2019-01-22
แดกแด
sent an automated(?) response - 2019-02-05 ๐ requested comfirmation of issues
- 2019-02-06
แดกแด
asked for 90 days to fix the issues - 2019-03-05 ๐ requested status update
- 2019-03-15
แดกแด
asked for additional 90-day extension - 2019-03-16 ๐ agreed on 30-day extension
- 2019-03-27
แดกแด
released first patch (CVE-2019-9950, CVE-2019-9951)3 - 2019-05-20
แดกแด
release of second patch (CVE-2019-9949)4 - 2019-05-22 ๐ public disclosure
- Tested on
My Cloud EX2 Ultra
version2.31.149_20181015
โ - and unbeknownst to me at the time, already discovered by the exploiteers โ
- https://community.wd.com/t/new-release-my-cloud-firmware-versions-2-31-174-3-26-19/235932 โ
- https://community.wd.com/t/new-release-my-cloud-firmware-versions-2-31-183-05-20-2019/237717 โ