ACE

RESPONDER

Attack Animator

Blog

Learn

Challenges

Sign in

Exploiting Empire C2 Framework

14 Feb 2024

Share on Twitter
Share on LinkedIn

This writeup covers the process of exploiting Empire C2 Framework <5.9.3 and concludes with recommendations for offensive and defensive teams. You can find the exploit PoC here.

As a company specializing in realistic defensive training, we are highly interested in the inner workings of Command and Control (C2) frameworks. Empire is a modernized, open-source framework equipped with a wide array of capabilities. Examining its inner workings offers two opportunities: first, to improve adversary simulation, and second, to develop novel defensive techniques. While scrutinizing the staging and tasking processes, we pinpointed several vulnerabilities that an attacker could exploit to gain root access to a C2 server.

On Monday, February 5th, 2024, we reported these vulnerabilities to BC Security. The development team promptly initiated actions to address the issue, pushing a first draft patch in a blistering 24 hours. By Friday, February 9th, BC Security released Empire v5.9.3, incorporating a well-tested patch that fully mitigates the identified vulnerabilities.

Empire Lore

The root cause of these issues first appeared back in 2016 when @zeroSteiner disclosed a directory traversal vulnerability to the original developers. His exploit, dubbed Skywalker, took advantage of the way Empire agents exfiltrate files from compromised hosts. By impersonating an agent, he could “trick” the C2 server into thinking a valid agent responded to a ‘download’ task. Since the server didn’t properly handle file names, zeroSteiner could traverse up the file system and overwrite critical system files resulting in remote code execution (RCE).

The Empire developers adequately addressed these problems in a subsequent release. However, path traversal vulnerabilities would reemerge as a result of fundamental changes and upgrades.

Stagers

Empire offers a variety of pre-generated stagers in multiple languages and formats.

There are two key pieces of information we can extract from stagers: the staging key and the C2 server URL. The listener on the C2 server has a single staging key associated with it. Any agent that wants to register with a listener must “authenticate” with this key. If we have one, we are effectively an agent to the C2 server.

The Staging Process

Empire’s stagers bootstrap an agent in three transactions.

STAGE0

The purpose of the first stage is to cut down on stager size. It exists to load packet handling, encryption and bootstrap classes that are required for the second stage. This stage is unnecessary for our purposes.

STAGE1

We request the second stage with a valid staging key. The second stage is used to establish a new session key via a Diffie-Hellman key exchange process. This is also where we tell the C2 server what we want our session ID to be.

STAGE2

A STAGE2 request involves sending system information to the C2 server. When everything is working as intended, the agent gathers user and operating system information and encrypts it with the new session key. The C2 server responds with the main agent class.

C2 Tasks

Empire, like most C2 frameworks, has a tasking process. When an operator interacts with an agent, the C2 server queues their commands in a database. Agents periodically call out to the C2 server and request new taskings. If they have a task waiting, they perform the specified action and respond to the C2 server with the result. How an agent reacts to a task, and how the server processes a response, is determined by the task type. Some examples of task types:

  • TASK_SYSINFO - Update the host information for the agent
  • TASK_SHELL - Execute a shell command
  • TASK_UPLOAD - Upload a file to the victim’s system
  • TASK_DOWNLOAD - Download a file from the victim’s system

There is no requirement that a tasking exists for the server to process a response. In other words, we can force the server to process task data by sending unprompted “responses.”

Directory Traversal

After investigating how the server processes different task types, we noticed an ominous comment in the TASK_DOWNLOAD path:

At this point we found the first exploitation candidate. If you remember from STAGE1, the agent picks its own session ID. Session IDs are 8 bytes and unsanitized. This means we can tell the server our session ID should be ./../../ and it will append it to the download_dir: /<install path>/empire/server/downloads/.

With this information alone we can overwrite any file one or two directories above the downloads directory. The main candidates are server.py and config.yml in empire/server/. We could overwrite server.py with a backdoor, or we could change the download directory to root in config.yml and send another TASK_DOWNLOAD to write to /etc/cron.d/. However, both of these files are loaded on startup. So we either need to crash the C2 server or wait for the operators to reboot it. We could accomplish this with social engineering, perhaps we could send fake errors to the operators via another task type, but this is not exactly a satisfying exploit path.

Bypassing the Skywalker Detection

The whole reason we can create files outside of the downloads directory comes down to this condition:

safe_path = download_dir.absolute()
if not str(save_file.absolute()).startswith(str(safe_path)):

The condition checks to see if the save_file path starts with the safe_path (/<install dir>/empire/server/downloads/). If it doesn’t, the server assumes the response is a directory traversal attack, emits an ‘attempted skywalker exploit!’ detection and returns.

The trouble is, absolute() doesn’t normalize paths. It only resolves relative paths. This means it converts a path like dir/file.txt to /home/kali/dir/file.txt. But it will also convert a path like ../../dir/file.txt to /home/kali/../../dir/file.txt. Confusingly, path normalization in Python’s pathlib is accomplished with the abspath() method instead of absolute().

Our first attempt at a bypass involved guessing the Empire install location. Our path traversals overwrote the entire save_path. So, guessing the correct install path would meet the startswith() condition.

But there’s a simpler way to do this. Our path traversal overwrote the save_path because of a nuance in how pathlib appends child directories. If you append a directory with a leading / it replaces the entire path with the child. This quirk works in Empire’s favor. However, it illustrates why stripping .. from input is not a substitute for path normalization.

We just need to make sure our traversal doesn’t start with \\ and we have a reliable exploit.

Another Path Traversal

There is another function that saves files from agent responses: save_module_file(). The logic is more or less the same as save_file(), Skywalker detection and all. The save_module_file() function is called by two task types: TASK_CMD_WAIT_SAVE and TASK_CMD_JOB_SAVE.

We looked into TASK_CMD_JOB_SAVE first since nobody likes to wait. But it didn’t encode the input properly and inserted b’’ into the file extension. Luckily, TASK_CMD_WAIT_SAVE is exactly the same, but encodes our inputs before creating the file name.

The save_path for this task is interesting. It creates a directory with 15 bytes for the name and a file with:

  • The compromised system’s hostname
  • The current date and time
  • A 5 byte extension

So, a “legitimate” file would be named something like: prefix/alice-pc_2024-02-09_16-25-32.txt. We happen to control the 15 byte prefix and the 5 byte extension with our response data. But, these two inputs only allow for 5 directory traversals. Even if we find a good place to write despite this restriction, we’re stuck with the date string in the file name.

If you remember from the staging process, we supply operating system information when we request the third stage, STAGE2. We can set the hostname, which has no length restriction or input validation, to our desired traversal. All we need to do is put a reverse shell in a crontab snippet and upload our file with the very ugly name: _2024-02-09_16-25-32.aaaaa to /etc/cron.d/.

It doesn’t work. It turns out Cron is very picky about file names, and it doesn’t like the special characters in our ugly file. But, we still have a convenient 5 bytes in the file extension. We can set these to /../a and perform one more traversal. This would create an empty directory and a one-character file in /etc/cron.d/. Our save_path will end up looking something like this:

There is some logic that complicates this approach. A condition in save_module_file() checks to see if the save path exists and creates the necessary directories if it doesn’t.

Since we have one additional path traversal in the extension, os.makedirs() throws an error. It attempts to create the _2024-02-09_16-25-32. directory twice: once for the name and once for the final traversal ../. No big deal. We just need to send another request within the same second. This will cause the if not save_path.exists() condition to return true and we can write the file a to /etc/cron.d/.

Takeaways

The risk from a vulnerability of this nature can be reduced greatly, and, failing that, exploitation can be detected with minimal effort.

Offensive Teams

You can greatly reduce the risk of C2 server takeover by implementing a defensible C2 architecture. A defensible architecture prevents an attacker from leveraging your infrastructure against you and limits the impact of a successful breach.

The following are based on threats exploiting a C2 server through a C2 channel. Additional considerations should be made for other vectors.

  1. Block access to the listener from out-of-scope IPs.
  2. Spin up dedicated infrastructure for each client. If two targets have different security boundaries, they should have unique, segregated infrastructure.
  3. Implement monitoring and detection for C2 infrastructure.
  4. Completely destroy the C2 server and exfiltrated data after each engagement.
  5. Rotate agent/implant keys between engagements. For Empire these are the staging and session keys.
  6. Have a plan to quickly isolate compromised C2 servers.

Logs should be stored separately from the C2 server and monitored throughout the engagement. Basic log sources include:

  • C2 server (teamserver) logs
  • Auditd
  • Authentication logs (auth.log)
  • Web server or file server logs (if you’re serving tools/stages)

The broader threat is the possibility that an attacker will recover a staging key. Limiting access to the listener prevents an attacker with a valid key from registering an agent/implant. If all else fails, we can detect directory traversal attacks like this with basic detections.

It’s also well worth the effort to build additional detections depending on your C2 setup. We could only traverse two directories in the first path we explored. So, Cron detections don’t apply to all scenarios. To cover this vector we can monitor changes to our C2 framework’s files since they shouldn’t change often.

Defenders

Red teams and their derivatives (assumed breach, adversary simulation, etc.) are some of, if not the, highest ROI security services. Discovered vulnerabilities and recommendations aside, they offer an opportunity to observe a realistic attack path. Whatever the outcome, you should fully scope their presence. Treat it like a real incident and keep the detections/indicators in place after they leave.

The same path traversal considerations apply to systems we defend. Just like Empire, an attacker constrained by size or character limits may overwrite a file within the service’s working directory. It’s a good idea to identify which scripts and executables these services run regularly and monitor them for modifications. It’s also important to retain web access and service logs which, like Empire, could reveal path traversal strings in unlikely places.