Chaining vulnerabilities in Druva inSync for local privilege escalation

Backing up important data is fundamental to cyber hygiene. When disaster strikes, backups are a key protection against data loss. Oftentimes, 3rd party software is used to manage this process. Since companies depend on these solutions, I thought it made sense to bug hunt in a popular endpoint backup product, Druva inSync.

What I found is an Electron-based front end combined with a compiled Python backend service. The OS X keychain is used to store backend authentication keys. And this is where it gets interesting. Together we will slither our way to root by chaining vulnerabilities and abusing keychain permissions.

Let’s talk about the backend first.

Feeling out the attack surface

One of the first things I encountered was the fact that the ‘inSyncDecommission’ and ‘inSyncUpgradeDaemon’ processes run with root privileges.

And inSyncDecommission (pid 5709) is listening on TCP port 6059 — only over the loopback interface though.

After some more digging, I found that inSyncUpgradeDaemon listens on a UNIX socket.

There are also some juicy log files. Notice the top two in particular: inSyncUpgradeDaemon.log and inSyncDecommission.log.

Now we have some useful info. Two processes are running as root, and there are some sockets open. There are also log files being written to, which should help with debugging.

Communications

I fired up Wireshark, and listened on the loopback interface. There wasn’t much going on, so I decided to play around with the client user interface.

Lo and behold! When I changed the CPU Priority, some traffic was generated on TCP port 6059. Here is the captured TCP stream:

The inSync client sends two HTTP POST requests (red), and the server responds (blue) with HTTP 200. Weird though… there’s a custom header, ‘X-Drv-Encoding’ and the request/response body is kind of strange looking.

These are marshalled Python objects. As a side note, if you were to look in the “/Applications/Druva inSync.app/Contents/MacOS/” directory, you’ll see a whole bunch of Python junk in there, though it’s all compiled.

I won’t get into analyzing the message format, but essentially a message contains a length field, a marshalled Python object, and a trailing ADLER32 checksum. I used a hex editor, iHex, to figure out the checksum piece.

With this in mind, the response object can be unmarshalled. Here is a response.

Doing the same for the requests, we can see there is a daemon.authentication request followed by a daemon.renice.

daemon.authenticate

daemon.renice

To recap, we now have an idea of what a message looks like. We know that there’s a header with a length field, a marshalled Python object, and an ADLER32 checksum. The inSyncDecommission service (running as root) listening on localhost TCP port 6059 will process these messages, and there’s clearly an authentication requirement. The inSyncUpgradeDaemon uses the same protocol.

Creating a Client

Knowing what the protocol looked like, I created a Python client to talk to the service. It took a bit of trial and error to get things right, but here’s a successful renice log entry:

In order to get this to work, I opened a TCP socket, and sent two requests: daemon.authenticate followed by daemon.renice.

For reference, this is how you would marshal a “requests” object calling daemon.renice:

Once I got the client working, I wondered if there were any more request types that could be invoked (daemon.<method>).

Enumerating the Service

This part was extremely challenging, and I learned so much. My first thought was to perform some static analysis on inSyncDecommission to find any other methods that could be triggered. Eventually, what I found was a function that inspected the value of the environment variable “_MEIPASS2”.

A simple Google search revealed that this was related to PyInstaller. Turns out, this service was built with PyInstaller, which “bundles a Python application and all its dependencies into a single package. The user can run the packaged app without installing a Python interpreter or any modules.” Nifty! The takeaway for me was that this binary was loading Python modules from somewhere.

I actually struggled to determine how to extract any PYC files from the OS X install, so I installed the inSync client on a Windows box. This was a little easier to work with. There was a library.zip file containing any/all PYC files I needed (this was built with py2exe).

Next I transferred library.zip back to my Mac research machine, unzipped it, and searched for ‘daemon.renice’ to find the Python module containing that.

I tried to decompile the PYC, but to my surprise, uncompyle6 (and other decompilers) choked on the class file. The magic number was not recognized.

Even when I changed the magic number to something common like 62211, no dice.

I went down this rabbit hole for a while with no luck. Pydisasm gave me something to work with, but still not a complete disassembly. There’s probably a trick that I missed, but I’m pretty sure the bytecode was tweaked on purpose because I could decompile PyInstaller binaries that I generated myself.

If you figure that out, let me know! Anyway, I went down a different path. A quick search in the strings revealed quite a few daemon functions.

I created a list of all these method names, and iteratively called each method via an authenticated “rpc” request. Since I had the authentication token from previous requests, I could reuse it. However the token isn’t consistent across installations.

Notice how I passed a bunch of lists as arguments. I did this with the hope that an error would throw in the log, since it’s unlikely every argument is a list. The results were lovely. I received some helpful errors telling me how many arguments were expected.

After playing with the arguments a bit, I was able to generate the following error. Not good. The daemon.set_file_acl method calls eval() on untrusted input.

The eval() function will execute a Python expression, and since the service is running as root, we can run arbitrary Python with root privileges. At this point, it was trivial to pop a root shell. But we have to keep in mind that an authentication token is required. Long story short, the token required for this service (along with others) is stored in the OS X Keychain.

And only a handful of processes could access the token value.

I needed to figure out a way to programmatically access the token value without a password. The question in my mind was, “can I abuse any of these processes to give me a token?”

The Electron Application

In case you’re not familiar with the Electron framework, it is designed to allow you to create cross-platform native applications using JavaScript, HTML, CSS. Chromium and Node.JS are combined into a single runtime. So it wasn’t surprising when I saw this in my process list after launching the inSync UI:

Basically, the command form is: inSync <url> main no_rfs.

The kicker here is that any JavaScript contained in the HTML input file is executed by NodeJS. The impact of running NodeJS code is more severe than just running client-side JavaScript in the browser. For example, we could run the “id” shell command using the “child_process” module. We’ll call this file test.html.

Here is the command to open the file with inSync:

open /Applications/Druva\ inSync.app/Contents/Resources/inSync.app — args “file:///Users/scooby/test.html” main no_rfs

And the result:

We can see here that the command executed successfully.

Note: Code could be loaded from a remote address as well using an http:// URL.

Remember that the inSync app can access the keychain. Now that we can execute NodeJS code within the inSync app context, we can use the KeyTar module to pull tokens out.

Boom! Below you can see the value of the upgrade token.

As a side note, I should mention that the KeyTar module had to be rebuilt for Electron. The problem was that I needed the Electron application to load modules outside of the inSync installation. If you’re interested in this process take a look at this tutorial on using native node modules.

Putting it all together

Let’s take a quick moment to zoom out and ensure we understand all of the moving parts.

We can execute arbitrary NodeJS code at a low privilege level because of the misconfiguration in the Electron app (CVE-2019–4001). This doesn’t really get us much; however, we can query the OS X keychain to pull the value of the INSYNC_UPG_SHARED_KEY.

With the upgrade key, we can authenticate to the inSyncUpgradeDaemon over a UNIX socket. This service exposes a method named “set_secrets” that can be used to modify the value of INSYNC_SHARED_KEY. This key is required to authenticate to inSyncDecommission. Again, both of these services use a custom protocol requiring marshalled Python objects.

Since we can set the value of INSYNC_SHARED_KEY, we can authenticate to inSyncDecommission (localhost TCP port 6059). inSyncDecommission has a Python code injection vulnerability (CVE-2019–4000) in the “set_file_acl” method, and since the service runs with root privileges, we can execute code at this privilege level.

If we put it all together, this diagram models the logic flow required for exploitation.

Final Thoughts

The take-home here for me is that vulnerabilities don’t always lend themselves to exploitation. We as researchers must take time to understand how various components interact with each other. And we must familiarize ourselves with the privilege boundaries between these components.

Also, I would like to mention that I did manage to decompile the Python code, but I was only able to do so after exploiting the code injection vulnerability. After getting code execution, I was then able to import the ‘dis’ module and decompile by hand. Take a look at our research advisory for more details, and our GitHub has a full exploit.

After writing this blog, I dug a bit deeper into why the PYC files wouldn’t decompile. In an upcoming article, I will discuss how I managed to solve this problem. Thanks for reading.