Rhino Security Labs

Unitrends Vulnerability Hunting: Remote Code Execution (CVE-2017-7280) – Chapter 2

This is chapter two of a two part series on Remote Code Execution (RCE) vulnerability hunting in Unitrends. Fixes to these bugs are available in the latest Unitrends update.

The exploits for the Unitrends vulnerabilities mentioned in this security research series can be found on the Rhino Security GitHub page.

In chapter one of this blog series, we reviewed two simple examples of remote code execution in Unitrends Enterprise Backup appliances. In this blog we’ll detail another vulnerable function that required more analysis in finding how to reach the vulnerable function. With an application as large as Unitrends, the interface will not always show you how to get to your vulnerable function call; however, with the simple tools of find and grep we’ll be able to trace every call along the way.


In the last blog, heavy back-tracing of code was not required. Navigating to a change password page is relatively intuitive compared to the example we will look at now. From our first find/grep we saw the /api/includes/restore.php file made several calls to exec and shell_exec. Digging through the file by searching for shell_exec, we come upon the function downloadFiles which shows:

The function control flow is roughly as follows:

  1. Look for the ‘filenames’ parameter in $data. Return 500 if not found.
  2. Set $filenames to $data[‘filenames’] and create a string from this array based on each filename in $filenames.
  3. Get the size of the files by feeding filenames into another php file.

The problem here is that if anyone filename in $data[‘filenames’] is tainted, you can achieve code execution through the shell_exec in step 3 from above. So now that we’ve located the vulnerable code, we need to find a way to reach it from the web interface. To do so, I used the grep command line tool (and will walk through it in this example), but there are undoubtedly much better ways to do this and automate such a task.

~/scratch/html$ grep -r "downloadFiles(" . | tr -s [:space:]
./ui/app/recover/filerecovery/fileLevelRecoveryController.js: $scope.downloadFiles();
./api/includes/restore.php: return $this->downloadFiles($data, $sid);
./api/includes/restore.php: // which therefore invokes 'download-files' to take the downloadFiles() path. What a nested mess!
./api/includes/restore.php: function downloadFiles($data, $sid) {
./api/includes/restore.php: // get the source's max size limit and let the target verify it in downloadFiles() once it adds up all the file sizes.

We see that the downloadFiles function called from the same file. Reopening the restore.php file, we note that:

The downloadFiles function is called in the ‘download-files’ switch/case statement from a function post, which reads as follows:

public function post($which, $data, $sid)
    global $Log;

    $sid = $sid !== false ? $sid : $this->BP->get_local_system_id();
    $result = array();
    if (is_string($which[0])) {
        switch ($which[0]) {
        ... other cases ...

        case 'download-files':
            if (isset($data['id']) && $data['id'] !== false) {
                return $this->downloadFilesTargetDir($data, $sid);
            } else {
                return $this->downloadFiles($data, $sid);

The function post is a part of the Restores class. To find an instance of the Restores class, we do another grep to search for each declaration using the “new” keyword.

~/scratch/html$ grep -rH "new Restores" . | tr -s [:space:]
./api/includes/appliance.php: $restores = new Restores($this->BP);
./api/includes/appliance.php: $restores = new Restores($this->BP);

Inside appliance.php, we see the class declaration inside a short function called post_restore which reads:

public function post_restore($which, $data, $sid)
    $restores = new Restores($this->BP);
    return $restores->post($which, $data, $sid);

Grepping for post_restore, we see it’s in ./api/index.php, and executed by the function execute_post. This function is responsible for dispatching POST requests based on the API endpoint hit, which is specified by the following switch/case snippet:

// Check access scheme
$method = $request[0];

... code dealing with request validation and var inits ...

switch ($controller->getMethod()) {
    case 'get':
        $status = 200;
        $Log->enterMethod("GET", $request[0], $data, $sid);
        $body = execute_get($request, $data, $sid, $systems);
    case 'post':
        // adding item returns Created (201)
        $status = 201;
        $Log->enterMethod("POST", $request[0], $data, $sid);
        $body = execute_post($request, $data, $sid, $systems);


function execute_post($request, $data, $sid, $systems)

    ... variable declarations ...

    $method = $request[0];
    switch($method) {
        case 'restore':
            $which = -1;
            if (isset($request[1])) {
                $which = array_splice($request, 1);
            if (is_array($which) and $which[0] == "archive") {
                $which = array_splice($which, 1);
                $body = $archive->restore($which, $data, $sid);
            } else {
                $body = $appliance->post_restore($which, $data, $sid);

To recap, we found a vulnerable function in restore.php, then back-traced this function call to see it was a part of a larger custom Restores class object, which is instantiated and called in appliance.php. This is called from the main api/index.php file which dispatches POST requests through execute_post. If we hit the endpoint /api/restore/download-files then we will successfully call our function. All that’s left is to pass it the data (in this case ‘filenames’) as a JSON array of filenames.

One caveat for this exploit is that the data we’re populating in the command string is encapsulated by string literals (or single quotes) which are used as pseudo sanitization for our input. An example of this is that the command echo ‘`sleep 10`’ will print the line `sleep 10` to the terminal, but echo “`sleep 10`” will cause the terminal to sleep ten seconds. To circumvent this protection we use the newline character to execute our commands directly. Surrounding our malicious file name with “\n” will cause the terminal to interpret it as:

sudo rflr_manage.php –-get-size –-filenames ‘(new line returns this command) $FILENAME (new line submits our malicious filename)‘

Below is an example of the payload in action captured from Burp’s interceptor.

This resulted in the file /tmp/pwnd in being created. A python wrapper was created around this web request, and the exploit shown below.

LFI IN UNITRENDS < 9.1.1 (CVE-2017-7282)

Similarly to the downloadFiles vulnerability from above, the function downloadFile has no sanitization performed on the filename. It simply opens up the variable $filename, which is passed in the POST request, reads the contents of the file, and returns it to the user. We reach it in a similar fashion as above, except instead of the ‘download-files’ case we must hit the ‘download’ case from the switch statement in restore.php.

Again, a simple wrapper was made in python to automate this process.


I emailed Unitrends support with the above six issues and had a phone call to determine where they should be submitted. After several days of not hearing back, I decided to make an account on support.unitrends.com to manage each of the six tickets I opened. After registering a new account and navigating to “My Profile and Cases,” I found that I was able to see other customers support tickets, emails, conversations with staff and attachments without any extra overhead.

List of support cases in various degrees of progression (these were not my cases, but other Unitrends customers).

When viewing a support ticket, I could chat and impersonate staff and even close the case.

The attachment of the ticket even revealed the root password of the appliance the customer was using.


I was told that these vulnerabilities were resolved in Unitrends 9.1.2, which at the time of writing is not available for free trial on their website. I requested to be issued a copy (or at least a trial) of 9.1.2 to verify the patches but was denied. Therefore, I had no way to check if the issues were resolved without paying for a full copy.

After adequate disclosure time passed and plenty of internal meetings, we decided to move forward and publish the research to the Rhino Security Labs website.

Fast forward to when Chapter 1 is published. Rhino Security Labs is contacted by Unitrends. It is determined that there was a breakdown of communication between support and the security team which caused the issue not to be escalated to the proper department. Within a matter of days, Unitrends remediates the issue, and we verify the patch.


3/6/2017: Initial contact with support requesting information on vulnerability disclosures. Told I’d receive more information the following day. No information or follow up came.

3/8/2017: Called Unitrends support line, told to forward information to support@unitrends.com. Unitrends Support Case #00429561-00429566 opened.

3/10/2017: No information from vendor. Called support line again, was notified all my cases were closed. Support is investigating as to why they were closed without contacting me.

3/10/2017: Called again, was told all the bugs had been fixed. Wanted to confirm with the development team, told I couldn’t reach them and had to go through sales and buy an appliance. Called the NW Rep from 3/6/2017 to determine if bug bounty program was available, said the director was on time off. I notified him my tickets were listed as resolved and wanted to confirm. Told I’d have answers/contact info on Monday.

3/13/2017: Support confirms this was fixed in 9.1.2 but to verify we must buy a new appliance. Opened another ticket showing how the support website of Unitrends is leaking confidential client information. Told it was a known issue and that it was specific to my account and ticket was closed. (This bug is not unique to one account, but affects all accounts.) Was told this is specific to my account only (which it is not) and my support case was closed. Details will follow upon expiry of the 45-day disclosure policy.

Following is the correspondence from Unitrends Support regarding the six bugs.

Mr. Hohnstein,

We are looking into this issue for you now. Your unitrends area representative, [REMOVED] should be back in contact with you soon to confirm we have addressed these issues. I can tell you that it looks like we have addressed these issues in our latest releases 9.1.2 and 9.2. Please allow [REMOVED] to confirm these for you.

Thank you for using Unitrends Customer Support,

(My response)

Hey all,

I downloaded the latest trial version on the site and it doesn’t seem to be up to date with the version you mentioned (9.1.2). Could I get a .ova file to confirm the changes, or know when the latest will be pushed out?


Mr. Hohnstein,

We will momentarily be denying and deactivating your community account request. If you wish you may activate and update your trial for 30 days of gold support. 9.1.2 release is only available for 2016 users and 9.2 is available currently(See KB 4078 if you have trouble updating) and replaces 9.1.2.

Thank you for using Unitrends Customer Support.

4/10/2017: Research published to Rhino Security Lab’s website.

4/13/2017: Unitrends Sr. Dev contacts Rhino Security Labs. We agree to work together to test the remediation.

4/14/2017: Confirm patches and update. Unitrends response to CVEs security update.


This interaction exemplifies the need for organizational policies and technical controls to be tested and verified. It is important for employees to be trained and know when to escalate an issue. In the end, a couple of fascinating bugs were found, and a few more CVEs issued. The Unitrends security team was quick to communicate with us to patch the issues, and their customers are a little better off for it.