Page cover

Bugs in C2s

hehehaha

Motivation

I've been dabbling with c2s in the past year, using it mainly for various labs and certifications and happened to chance upon this article which talks about vulnerabilities in open-source C2sarrow-up-right.

With the increase in number of open-source C2s these days and new contenders such as Adaptixarrow-up-right, I decided to take a closer look at a few open-source implementations to better understand their design and see if any potential security issues stood out.

Focus Areas

With reference to IncludeSecurity's blogpostarrow-up-right, here are the bug classes I focused on while exploring open-source C2s.

Relationship
Example

Third Party -> Teamserver

An unauthenticated attacker is able to achieve arbitrary file write/read on the teamserver's host.

Agent -> Teamserver

An agent sends a untrusted input to the teamserver, allowing arbitrary file write on the teamserver's host.

Operator -> Teamserver

A low-privileged operator is able to send an untrusted input to the teamserver, resulting in remote code execution on the teamserver's host.

List of C2s I reviewed

Here are a list of the open-source C2 frameworks i explored. I mainly filtered them by seeing if they had any commit activity in the past year (2025).

All of the bugs were reported to the repository owners except for XieBroC2.

AdaptixC2

AdaptixC2 is the latest kid on the block with over 2.1k stars on GitHub as of January and in active development. It boasts a sleek UI similar to CobaltStrike and plenty of tools for customizing various components of the C2 such as AxScriptarrow-up-right and ExtensionKitarrow-up-right.

I found the documentation to be super detailed with each core feature explained clearly.

As if the documentation is not enough, there is plenty of videos and articles on AdaptixC2 herearrow-up-right if you are interested.

As for the code itself, I did not spot any obvious security issues with obvious segmentation between the agent and teamserver. However, i did two instance of unsafe sinks (E.g Exec.Command) in the gopher and beacon agent respectively with one of it being exploitable.

Authenticated Operator-To-Server RCE (<= v1.0)

Overview

As per Adaptix's documentationarrow-up-right, beacons can be generated with the following formats (EXE, DLL, Service Executable, Shellcode).

In the DLL path, the sideloading workflow mirrors export names into linker flags without sanitization, letting a crafted DLL name inject shell metacharacters and execute arbitrary commands on the teamserver during agent generation.

Analysis of Vulnerable Code

Looking at AdaptixServer/extenders/beacon_agent/pl_sideloading.go, the DLL name is extracted from PE export directory directly without sanitization as seen in (1).

This is then procesesed in getExports which saves this unsanitized string into dllName as seen in (2). Subsequently, this is used to create file path in defFileName in (3).

Finally, this value is appended to the linker flags and, via string formatting, is passed into the vulnerable exec.Command sink as seen in (4), (5) and (6).

POC

Assuming we have operator level access, we will need the following to gain RCE on the teamserver. We can take a look at Adaptix's Web APIarrow-up-right to gain a better understanding of what we need.

Requirements

  1. A listener of any sorts

    1. RegName and Name of the beacon

  2. A valid operator account

We can craft a simple python3 script to do the following:

  1. Login and get authorization token at /login

  2. Get listener config (optional as operators can just view it on GUI)

  3. Craft a request to /agent/generate with our malicious dll

Here is the associated poc script and video that demonstrates this vulnerability.

The vulnerable code was patched in this commitarrow-up-right which passes arguments as an array instead of the vulnerable sh -c.

Unauthenticated DoS via Unbounded Request Body Read (<= v1.0)

Overview

The HTTP beacon listener (AdaptixServer/extenders/beacon_listener_http/pl_http.go) reads the entire request body with io.ReadAll which has no limit. No authentication or session key is enforced at the HTTP layer. This allows an attacker tosend an arbitrarily large or endless body to exhaust memory/CPU, causing a DoS on the teamserver.

Analysis of Vulnerable Code

The vulnerable code lies in AdaptixServer/extenders/beacon_listener_http/pl_http.go, where after a series of operations, the teamserver uses io.ReadAll to read the request body.

POC

To exploit this, we need to spoof a minimal packet that the listener will accept. This can be extracted out from a compromised agent using a script such as thisarrow-up-right.

  • HTTP method must match listener HttpMethod (commonly POST).

  • Path must exactly equal configured Uri (e.g., /endpoint).

  • User-Agent must exactly match UserAgent in config.

  • Host must equal host_header if set; otherwise any Host passes.

  • Heartbeat header named as hb_header (default X-Beacon-Id) with base64 that decodes to at least 8 bytes; content is not otherwise validated before the body read

Below is the POC and a video demonstrating the exploit.

MerlinC2

MerlinC2 is a C2 framework that prioritizes HTTP/2 and HTTP/3 (QUIC) as its primary transport. It also offers server support on all three platforms (MacOS, Windows and Linux) which I thought was pretty cool.

It seem to be in active development with a commit in the last 9 months and having over 5.5k stars on Github.

Authenticated Operator-To-Server RCE (<= 2.1.4)

Overview

Merlin C2 has integrated SharpGen Supportarrow-up-right which accepts operator-controlled input for dotnetbin and sharpgenbin parameters, which are subsequently passed directly to exec.Command() without validation.

This allows any authenticated operator to obtain RCE with a few additional steps on the teamserver which usually is run with root privileges.

Analysis of Vulnerable Code

As an operator, we can control all the option values as seen in (1)

The operator controls the entire optionsMap including dotnetbin and sharpgenbin values.

However, it has flawed validation for the arguments dotnetbin and sharpgenbin as only validates the files exist not that they are the expected executables as seen in (1) and (2).

This allows any operators to substitute any executable on the system (E.g /bin/bash, /usr/bin/curl) they want.

This eventually leads to the vulnerable sink exec.Command, which despite using an argument array, is still vulnerable.

POC

Since the files have to exist on the system, we need to have the following pre-requsites

  1. Authenticated Operator

  2. At least one connect agent to satisfy the module execution requirements

    1. The host running on the agent has to be Windows as seen from the following code snippet as seen in (1) in module.go

  3. Ability to download files from agent to teamserver which is a standard privilege given to operators

Here my kali VM 192.168.125.128 is running the Merlin Teamserver with a spoofed agent (windows).

First, we download the test.sh, transfering the malicious file from the host that the agent is running on to the teamserver.

Next we set the following arguments to execute our malicious script.

  • Agent: valid agent-id that is on windows

  • sharpgenbin: set to where the malicious script is saved on the server.

  • dotnetbin: set to /bin/bash

This results in exec.Command executing the following command line

Upon execution, we recieve our reverse shell.

This remains unpatched as the author intended for "Merlin users have full control over the client and the server".

Emp3r0r

Path Traversal in File Download (<= v3.11.0)

Overview

The operator get --path command is built on GenerateGetFilePaths, which “sanitizes” the requested path with filepath.Clean and then concatenates it with the server download root (live.FileGetDir).

Because filepath.Clean preserves leading ../, a malicious operator (or compromised operator session) can write files outside the intended ~/.emp3r0r/file-get/ directory.

Analysis of Vulnerable Code

  • filepath.Clean only normalizes the string; it does not remove traversal.

  • Concatenation with live.FileGetDir means any ../... path (e.g., ../modules/pwn/config.json or ../../../etc/cron.d/evil) becomes ~/.emp3r0r/file-get/../..., letting an operator write outside file-get to arbitrary locations reachable by the user running the teamserver.]

POC

Based on config.go, default files gotten from get are stored based on SetupFilePaths().

Thus, we can do the following to achieve path traversal.

  1. On the operator, run cwd to learn the agent’s working directory (e.g., /home/kali/.BurpSuite/.../resources), so you know where to place a source file.

  2. Craft a traversal path with ../ to climb out of ~/.emp3r0r/file-get toward your target (e.g., ../../../../../tmp/ThisIsNotSupposedToBeCreated/pwned.txt).

  3. On the agent host, create the file at the matching relative path from its CWD (e.g., ../tmp/ThisIsNotSupposedToBeCreated/pwned.txt).

  4. In the operator console, run: get --path "../../../../../tmp/ThisIsNotSupposedToBeCreated/pwned.txt".

  5. Verify on the teamserver host: ~/.emp3r0r/file-get/../../../../../tmp/ThisIsNotSupposedToBeCreated/pwned.txt resolves to /tmp/ThisIsNotSupposedToBeCreated/pwned.txt and is written there.

The vulnerable code was patched in this commitarrow-up-right which uses filepath.Localize and then checking with filepath.isLocal.

XieBro C2

XieBroC2 is a C2 that has its teamserver written in .NET while the implant is written in Golang.

Interestingly enough, the teamserver is not open-source but there are compiled releases in the repository.

Using ilspy allows us to retrieve the source code by decompile .NET IL to C# which is much more readable.

As of Jan 2025, the author's biography states that the projects will no longer be maintained and updated.

Unauthenticated Arbitrary File Upload/Download (<= 3.1.8)

Overview

XieBroC2 exposes unauthenticated WebSocket endpoints /upload and /download on both the controller port (profile wsPort, default 8880) and every implant listener port. They accept raw MsgPack arrow-up-right(no AES, no password) and allow path traversal for arbitrary file write/read once ChunkSize is nonzero.

Analysis of Vulnerable Code

In Teamserver/TeamServer.Listener.WebSocket/WSlistener.cs, there is no authentication added to the /upload and /download routes for the teamserver.

Teamserver/FileUploadHandler.cs (raw MsgPack, traversal) - No AES Encryption/Decryption is being done on the msgPack data. Additionally, there is no normalization on filename, meaning we can simply use ../ sequences to traverse directories.

Teamserver/FileDownloadHandler.cs (raw MsgPack, traversal) - Similar to FileUploadHandler.cs, this is also present in FileDownloadHandler.cs.

However, one pre-requisite is that ChunkSize needs to be set first.

Teamserver/TeamServer/Connect.cs (only place ChunkSize is set):

ChunkSize defaults to 0 and is only set when Connect.ControlConnection runs during controller login. After any operator login (or a forged one), ChunkSize > 0 and the endpoints accept non-empty uploads, they are never re-protected on logout.

POC

Using python3 to craft our websocket payload, we are able to directly upload/download any file on the teamserver.

Over here, I am able to read /etc/passwd on the teamserver. However, this is limited to what privileges the teamserver is running as (usually root).

This can technically be escalated into an RCE with cron.

Reflection

Open-source C2s are usually pretty solid, but they’re not automatically “safe.” Like any complex security tool, they can still have weird edge cases or vulnerable sinks.

It is important to preface that many of the bugs found in this article have a low chance of being exploited in a properly hardened red-team setup as seen below.

Secure C2 Infrastructure taken from ZeroPointSecurity's CRTO2 coursearrow-up-right

Take the Denial of Service on Adaptix for example. In a secure C2 intrastructure, a malicious attacker should never be able to contact the teamserver directly due to the use of redirectors. The redirectors would also have rate-limiting and other guardrails in place.

It’s also worth noting that different C2s draw the trust boundary differently. For example, Sliver has previously stated in a past issuearrow-up-right that "there is a clear security boundary between the operator and server, an operator should not inherently be able to run commands or code on the server". Other frameworks may not enforce that same split, not necessarily as a flaw, but because of different design goals, constraints, or assumptions about how the C2 will be deployed and used.

C2 Glossary

Operator — Red team member who interacts with the C2 framework via a client/GUI to task implants and view results.

Teamserver — Central backend server that manages listeners, stores data, and coordinates all C2 operations. Operators connect to this.

Implant — Malicious code running on a compromised host that communicates with the teamserver. Also called agent, beacon, or payload.

Agent — Same as implant. Term varies by framework (Sliver uses "implant", Cobalt Strike uses "beacon", Mythic uses "agent").

Beacon — Implant that periodically "checks in" with the teamserver for new tasks rather than maintaining a constant connection.

Listener — Service on the teamserver that waits for incoming connections from implants. Configured per protocol (HTTP, DNS, SMB, etc).

Redirector — Proxy server between implants and teamserver. Hides the real teamserver IP and can be burned/replaced if discovered.

Stager — Small initial payload that downloads and executes the full implant. Keeps initial delivery small.

Last updated