Empire Fails


Everyone makes mistakes, and we’re certainly no exception. Empire has suffered from a few security issues since its original release at BSides LV in 2015, and for a while, I’ve wanted to give some technical details on the specific mistakes we’ve made along the way for the sake of transparency. Thanks to a recent second disclosure by Spencer McIntyre (@zeroSteiner) several weeks ago, it seemed to be an appropriate time to own up to our transgressions. This post will cover the crypto issue disclosed right after release by Jon Cave (@joncave), as well as the two separate RCE issues disclosed by Spencer.

Crypto is Hard

One of the earlier Empire issues disclosed after the project was released was pull request #3 “Use authenticated encryption” by Jon Cave. As Jon described, “Even though the agent to server communications are encrypted they are still malleable and vulnerable to attack“. So what does this mean, and what’s the fix?

Essentially, because we didn’t originally include any kind of message integrity/validation in our communications, it was possible to modify sniffed Empire ciphertext in a way that modified the type of the packet. In Jon’s example, he modified the first byte of the ciphertext (the IV) in order to change the first part of the plaintext (the packet type). Since there are only 256 packet types, one of them being TYPE_EXIT, with a relatively small number of packets sent to the control server it was possible to force the exit of agents if the sessionID was known, effectively creating a DoS on the control server. Here was Jon’s simple PoC:

import requests
import sys

ciphertext = sys.argv[3].decode("hex")
cookies = {"SESSIONID": sys.argv[2]}
for i in range(0, 256):
    dos = chr(i) + ciphertext[1:]
    requests.post(sys.argv[1], cookies=cookies, data=dos)

Other attacks were theoretically possible, including the chance for encrypted information disclosure (though this was complicated by a lack of padding on Empire’s part).

Jon’s fix was to implement MD5 HMAC into message communications that occur after staging, along with double HMAC verification on the server side in order to prevent timing attacks. I bought Jon a round of beers at 44con, where he explained that he actually found the issue within 1-2 days of Empire being released. He also helped proof the Empire 2.0 protocol redesign and offered several optimizations.

RCE Is Bad Mmmmkay

The next mistake was much, much worse. Can you spot the mistake in lib/common/agents.py ?

        # see if we were passed a name instead of an ID
        nameid = self.get_agent_name(sessionID)
        if nameid : sessionID = nameid

        parts = path.split("\\")

        # construct the appropriate save path
        savePath =  self.installPath + "/downloads/"+str(sessionID)+"/" + "/".join(parts[0:-1])
        filename = parts[-1]

        # make the recursive directory structure if it doesn't already exist
        if not os.path.exists(savePath):

        # overwrite an existing file
        if not append:
           f = open(savePath+"/"+filename, 'wb')
            # otherwise append
            f = open(savePath+"/"+filename, 'ab')


A few months after release, Spencer McIntyre sent us a disclosure that allowed for remote compromise of an Empire control server. We worked to get a fix out quickly and Spencer chose not to release the PoC he titled ‘Skywalker’, but he recently released a updated PoC and Metasploit module that is compatible with the v2 version of the bug (described below). Issue #52 shows the patch for the original exploit.

One of the packet types for Empire is TASK_DOWNLOAD, which is the chunked response to a file download. The control server takes the packets comprising a file download and reconstructs the original file in ./downloads/SESSIONID/<original_path>/ where the original path is cloned on the server side to preserve the downloaded file path. What Spencer realized is that we weren’t doing proper path sanitization, leaving the path save mechanism open to a path traversal.

Spencer’s exploit will first ‘fake’ an agent checking into the control server, executing the normal key exchange process. A TASK_DOWNLOAD packet response is then sent to the control server, with the original file name compromising a ../path/traversal and the payload composing a malicious crontab entry. The server receives the packet and dutifully saves the malicious crontab to /etc/crontab, which will provide remote execution if the control server is running as root.

We fixed this with PR #52, which uses os.path.abspath to compare a normalized absolutized version of the save path to the ‘safe’ downloads folder.

Skywalker v2, “Oh no, not again”

About a month ago Spencer contacted us again with another disclosure notice (nothing makes your heart sink like getting a gpg message from Spencer with ‘skywalker.v2’ in the title : ). The second version of this exploit abused a malicious client SESSIONID to execute a similar type of path traversal. Spencer also included a patch that we verified and integrated into both the development and master branches with the 1.6 release.

Spencer graciously waited to release the exploit proof of concept and Metasploit module for a month after the path was integrated. The pull request (#7450) containing the exploit module was submitted this morning and is located here if you want to check it out.

Security is Hard

We wanted to extend another big thank you to both Jon and Spencer for the disclosures. We take it as a big compliment that anyone would look at our code as closely as they have, and we owe a lot to both of these researchers for providing fixes. If anyone else finds additional issues, please let us know, and we’ll buy you some rounds at the next con we all end up at!

Leave a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.