Covenant Tasks 101

Covenant is a .NET Command and Control framework that boasts a number of exciting features for red teamers. The Covenant implants are called Grunts, which are capable of executing post-exploitation “tasks” on a compromised machine. Covenant v0.1 released with a number of useful tasks, but the repository has really grown from contributions from the Covenant community.

Tasks can extend the functionality and versatility of a Grunt, such as providing new lateral movement, persistence or privilege escalation techniques and more. Contributing a Task to Covenant is an excellent way to support the project.

This post will provide an introduction for those wishing to create and contribute new Tasks.

Hello World

To create a new Task, there are at least two changes needed within Covenant. The first is the creation of a new *.task file within Covenant\Covenant\Data\Tasks\CSharp\ (e.g. HelloWorld.task) - this contains the actual source code of the task that the Grunt will execute. The second is within Covenant\Covenant\Core\DbInitializer.cs - this tells Covenant about the new Task so it will appear in the UI etc.

Tip: I like to create my Task file with a *.cs extension so that IntelliSense works properly. Just remember to rename it later.

There are a few rules for a Task:

It’s the responsibility of the Task author (you) to ensure input and return values are appropriately “translated”.

The reason for this is apparent if we look at the source code of the Grunt.

A bare minimum example could be:

public static class Task
{
    public static string Execute()
    {
        return "Hello World";
    }
}

DbInitializer.cs is monstrously large, but we’re looking for the section that starts with public async static Task InitializeTasks(CovenantContext context). Scroll down and you should see var GruntTasks = new List<GruntTask>, which is where the Task definitions start. At the time of writing, the first GruntTask is the Shell command. It’s generally easier to blast to the very bottom of the GruntTasks list so we can add our new task at the bottom.

So to wire-in our Task, we can add the following:

new GruntTask
{
    Name = "HelloWorld",
    AlternateNames = new List<string>{ "hello" },
    Description = "An example Hello World Task.",
    Code = File.ReadAllText(Path.Combine(Common.CovenantTaskCSharpDirectory, "HelloWorld" + ".task")),
}

Finally, we need to add some ReferenceAssemblies for the Task - these will differ depending on the functionality of the Task. Some Tasks use SharpSploit, others use Powerkatz (Mimikatz) and so on (we’ll create an example of this below). For a Task this simple, we only need basic core functionality.

Find the section just below the GruntTask list that looks like:

var getcurrentdir = await context.GetGruntTaskByName("GetCurrentDirectory");

await context.AddRangeAsync(
    new GruntTaskReferenceAssembly { GruntTask = upload, ReferenceAssembly = await context.GetReferenceAssemblyByName("mscorlib.dll", Common.DotNetVersion.Net35) },

First define the new GruntTask:

var helloworld = await context.GetGruntTaskByName("HelloWorld");

Then add the new reference assemblies:

new GruntTaskReferenceAssembly { GruntTask = helloworld, ReferenceAssembly = await context.GetReferenceAssemblyByName("mscorlib.dll", Common.DotNetVersion.Net35) },
new GruntTaskReferenceAssembly { GruntTask = helloworld, ReferenceAssembly = await context.GetReferenceAssemblyByName("mscorlib.dll", Common.DotNetVersion.Net40) },
new GruntTaskReferenceAssembly { GruntTask = helloworld, ReferenceAssembly = await context.GetReferenceAssemblyByName("System.dll", Common.DotNetVersion.Net35) },
new GruntTaskReferenceAssembly { GruntTask = helloworld, ReferenceAssembly = await context.GetReferenceAssemblyByName("System.dll", Common.DotNetVersion.Net40) },
new GruntTaskReferenceAssembly { GruntTask = helloworld, ReferenceAssembly = await context.GetReferenceAssemblyByName("System.Core.dll", Common.DotNetVersion.Net35) },
new GruntTaskReferenceAssembly { GruntTask = helloworld, ReferenceAssembly = await context.GetReferenceAssemblyByName("System.Core.dll", Common.DotNetVersion.Net40) },

Note how we need each reference for .NET 3.5 and 4.0.

Now if we start Covenant, our Task should appear in the Tasks menu and be available to the Grunts.

Tip: When you make changes to DbInitializer, you should remove Covenant\Covenant\Data\covenant.db before starting Covenant again.

Parameters

Some Tasks require additional input from the user to act as a parameter. Some may be mandatory and others optional - here we’ve modified our task to accept some string inputs, and conditional output depending on if that optional parameter is used.

public static class Task
{
    public static string Execute(string mandatoryInput, string optionalInput = "")
    {
        string output = "You said: " + mandatoryInput;

        if (!string.IsNullOrEmpty(optionalInput) || optionalInput != "")
        {
            output += " and " + optionalInput;
        }

        return output;
    }
}

Generally, all inputs will be strings since that’s all the user can type. For example, if you need to provide an int:

public static string Execute(string ProcessID)
{
    var pid = int.Parse(ProcessID);
    var handle = Process.GetProcessById(pid);
}

And if your code produces something other than a string…

public static string Execute(string ProcessID)
{
    var pid = int.Parse(ProcessID);
    var handle = Process.GetProcessById(pid);
    var sessionId = handle.SessionId;

    return sessionId.ToString();
}

To make Covenant aware that this Task can accept inputs, we need to add a new GruntTaskOption list to the GruntTask definition.:

new GruntTask
{
    Name = "HelloWorld",
    AlternateNames = new List<string>{ "hello" },
    Description = "An example Hello World Task.",
    Code = File.ReadAllText(Path.Combine(Common.CovenantTaskCSharpDirectory, "HelloWorld" + ".task")),
    Options = new List<GruntTaskOption>
    {
        new GruntTaskOption
        {
            Id = 123,
            Name = "InputOne",
            Description = "First Input",
            SuggestedValues = new List<string>{ "Hello World" },
            Optional = false,
            DefaultValue = "",
            DisplayInCommand = true
        },
        new GruntTaskOption
        {
            Id = 124,
            Name = "InputTwo",
            Description = "Second Input",
            SuggestedValues = new List<string>(),
            Optional = true,
            DefaultValue = "",
            DisplayInCommand = false
        }
    }
}

Executing this task via the Task UI, specifying both inputs produce the expected results.

Notice how InputTwo does not appear in the Interact UI, because we set DisplayInCommand to false. This is good for tasks that take long strings such as PowerShell Launchers, so as to not clutter the interface.

Reference Libraries

As mentioned above, some Tasks may need to leverage the functionality from an external project. Covenant already has a few built-in including SharpSploit, Rubeus, Seatbelt and SharpUp. Referencing these assemblies is relatively simple.

First, add the appropriate using statement in your task. As an example, let’s execute the WhoAmI method in SharpSploit.

using SharpSploit.Credentials;

public static class Task
{
    public static string Execute()
    {
        string result = string.Empty;

        using (var token = new Tokens())
        {
            result = token.WhoAmI();
        }

        return result;
    }
}

Then in DbInitializer, add a new GruntTaskReferenceSourceLibrary:

new GruntTaskReferenceSourceLibrary { ReferenceSourceLibrary = ss, GruntTask = await context.GetGruntTaskByName("HelloWorld") }

Adding an additional reference library to the Covenant back-end is more involved and probably doesn’t qualify as a “101”, so I’ll stop this post here. For all things Covenant, join the #Covenant channel in the BloodHound Slack.