Hacking the Western Digital MyCloud NAS

Posted: March 4th, 2017 | Author: | Filed under: NAS, Western Digital | 16 Comments »

Sometimes at Exploitee.rs, we look for fun devices to hack and sometimes the devices find us. Today we’re going to talk about a recent time where we found ourselves in the latter situation and our experience with the Western Digital series of Networked Attached Storage devices.

In the middle of last year I (Zenofex) began looking for a NAS that provided hardware decoding through my currently prefered media player, Plex. After a bit of research I ordered a Western Digital “MyCloud” PR4100. This device met all the requirements of what I was looking for and came highly recommended by a friend. After adding the NAS to my network and visiting the device’s admin page for the first time, I grew weary of adding a new device to my network without giving it a proper audit. So, I logged in, enabled SSH access, and looked at how the web server functionality of the device worked.

Login Bypass

I quickly found the first bug that shocked me, this bug was based on code that performed a user login check but did so using cookies or PHP session variables. Using cookies for authentication isn’t necessarily a bad thing, but the way that the Western Digital MyCloud interface uses them is the problem. Examine the code below.

/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;
}

The above code contains a function called “login_check”, this function is used by all of the backend PHP scripts and is used to verify pre-authenticated users. The above code has two paths, one which involves checking the session values for “username” and “isAdmin” and another (if the prior fails) attempts to complete the same process but with cookies. Because cookies are supplied by the user, the requirements that the scripts are looking for can be met by the attacker. The above process for sessions and cookies is summed up as follows.

    “username” variable is set and is not empty – User is logged in as a normal privileged user.
    “isAdmin” variable is set to 1 – User is logged in as an administrator.

This means that any time there is a login check within the PHP scripts, an attacker is able to bypass the check by supplying 2 specially crafted cookie values.

During the process of writing up my findings a new firmware was rolled out patching the above bug. However, this patch introduced a new vulnerability which had the same consequences as the original (prior to the update). Below is the current version including the fixed code.

/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 ?>

In the updated version of the code, a call to the new method “wto_check()” is made (line 40). This function runs a binary on the device with the client supplied username as an argument along with the user’s IP address. If the user is currently logged in and hasn’t timed out the value 1 is returned, otherwise 0 is returned (indicating the user isn’t logged in). The code for the “wto_check()” method can be found below.

/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 

In the above you can see that on line 11 the command is formatted to include the username and IP address as arguments to the “wto” binary. The problem with the above is the incorrect use of the PHP method “escapeshellcmd()” which, in its intended usage, handles an entire command string, and not just an argument. This is because the “escapeshellcmd()” function does not escape quotes and therefore allows an attacker the ability to break out of the encapsulating quotes (in our case for the “-n” argument), allowing for new arguments to be supplied to the binary. Because of this, instead of actually checking if the user is logged in, we can add new arguments and log the user in ourselves. Although we do not believe simply verifying that the user is already logged in by checking an IP address and login timeout is sufficient. The programmer who wrote this code should have used “escapeshellarg()”, which is intended to filter independent binary arguments and which does filter out quotes. Using “escapeshellarg()” as opposed to the currently used “escapeshellcmd()” would have at least prevented this attack from working.

Command Injection Bugs

A majority of the functionality of the WDCloud web interface is actually handled by CGI scripts on the device. Most of the binaries use the same pattern, they obtain post/get/cookie values from the request, and then use the values within PHP calls to execute shell commands. In most cases, these commands will use the user supplied data with little or no sanitization. For example, consider the following code from the device.

php/users.php

 15 $username = $_COOKIE['username'];
 16 exec("wto -n \"$username\" -g", $ret);

The code above assigns a value from the COOKIE superglobal variable, which contains array indexes for cookies submitted from the request, to the local variable “$username”. This value is then immediately used in a PHP “exec()” call as an argument to the local “wto” binary. Since there is no sanitization, using a username value like

username=$(touch /tmp/1)

turns the existing exec command into

wto -n "$(touch /tmp/1)" -g

and executes the user supplied command within.

Because the argument is encapsulated with double quotes and we use the “$(COMMANDHERE)” syntax, the command “touch /tmp/1” is executed prior to the execution of the “wto” binary and the return value of which is used as its “-n” argument. This basic pattern resulting in a command injection vulnerability is used multiple times within the many scripts used by the web interface. While some may have normally been prevented by authentication being required, that restriction is overcome by the authentication bypass mentioned above. Also, it is important to note that all commands executed through the web interface are done so as the user the web-server is running as, which, in this case is root.

Other Errata

While you may think that the above bugs are severe, there are a number of other errors within the web interface with some being as simple as the normal authentication being commented out:

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 //}

And others being more functionality specific, like the following example of a bug allowing a non-authenticated user the ability to upload files onto the myCloud device.

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

The above code consists of no checks for authentication and, when called will simply retrieve the uploaded file contents and use the user supplied path to determine where to place the new file.

Beyond the bugs listed in this blog post, our wiki is full of bugs we’ve found within the MyCloud web interface. Our general goal at Exploitee.rs is to get bugs fixed as quickly as possible. However, the large number of severe findings means that we may need to re-evaluate the product after the vendor has properly fixed the released vulnerabilities.

Responsible Disclosure

At Exploitee.rs, we normally attempt to work with vendors to ensure that vulnerabilities are properly released. However, after visiting the Pwnie Awards at the last BlackHat Vegas, we learned of the vendor’s reputation within the community. In particular, this vendor won a “Pwnie for Lamest Vendor Response” in a situation where the vendor ignored the severity of a set of bugs reported to them. Ignoring these bugs would leave the vulnerable devices online for longer periods while responsible disclosure is worked out. Instead we’re attempting to alert the community of the flaws and hoping that users remove their devices from any public facing portions of their networks, limiting access wherever possible. Through this process, we’re fully disclosing all of our research and hoping that this expedites the patches to users’ devices.

 

Bugs Found Statistics

1 x Login Bypass
1 x Arbitrary File Write
13 x Unauthenticated Remote Command Execution Bugs
70 x Authentication Required Command Execution Bugs*

*”Authentication Required” bugs can be reached with the login bypass bug.

Scope

Most, if not all, of the research can be applied to the entire series of Western Digital MyCloud products. This includes the following devices:

  • 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
  • My Cloud EX4100
  • My Cloud DL2100
  • My Cloud DL4100

More Info

For the complete listing and a small write-up on each of the bugs found during our Western Digital MyCloud research, visit the Exploitee.rs Wiki.

For updates on Western Digital’s response or alerts when new content is added to our wiki or blog follow us on twitter @Exploiteers

Video Demo

-Exploitee.rs