From ed4791fb4e14519bb95aa12a8acbc4aeacfc1610 Mon Sep 17 00:00:00 2001 From: "Mariusz B. / mgeeky" Date: Wed, 24 Mar 2021 04:21:01 +0100 Subject: [PATCH] Added C3 Client --- red-teaming/C3-Client/README.md | 180 +++++ red-teaming/C3-Client/c3-client.py | 1183 ++++++++++++++++++++++++++++ red-teaming/C3-Client/speak.ps1 | 7 + red-teaming/README.md | 2 + 4 files changed, 1372 insertions(+) create mode 100644 red-teaming/C3-Client/README.md create mode 100644 red-teaming/C3-Client/c3-client.py create mode 100644 red-teaming/C3-Client/speak.ps1 diff --git a/red-teaming/C3-Client/README.md b/red-teaming/C3-Client/README.md new file mode 100644 index 0000000..a9297b7 --- /dev/null +++ b/red-teaming/C3-Client/README.md @@ -0,0 +1,180 @@ +## F-Secure's C3 Client script + +This is a simple [F-Secure C3](https://github.com/FSecureLABS/C3) client Python script offering a few functions to interact with C3 framework in an automated manner. + +It connects to the C3 WebController (typically the one that's listening on port _52935_) and allows to issue API requests automating few things for us. + +### Usage: + +The script offers subcommands-kind of CLI interface, so after every command one can issue `--help` to get subcommand's help message. + + +**General help**: + +``` +PS D:\> py c3-client.py --help + + :: C3 Client - a lightweight automated companion with C3 voyages + Mariusz B. / mgeeky, + +usage: +Usage: ./c3-client.py [options] [...] + +positional arguments: + host C3 Web API host:port + {alarm,list,get,ping,channel} + command help + alarm Alarm options + list List options + get Get options + ping Ping Relays + channel Send Channel-specific command + +optional arguments: + -h, --help show this help message and exit + -v, --verbose Display verbose output. + -d, --debug Display debug output. + -f {json,text}, --format {json,text} + Output format. Can be JSON or text (default). + -A user:pass, --httpauth user:pass + HTTP Basic Authentication (user:pass) +``` + +**Example of a sub-help**: + +``` +PS D:\> py c3-client.py -f text http://192.168.0.200:52935 alarm relay --help + + :: C3 Client - a lightweight automated companion with C3 voyages + Mariusz B. / mgeeky, + +usage: Usage: ./c3-client.py [options] [...] alarm relay [-h] [-e EXECUTE] [-x WEBHOOK] [-g gateway_id] + +optional arguments: + -h, --help show this help message and exit + -e EXECUTE, --execute EXECUTE + 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 + /@')" + -x WEBHOOK, --webhook WEBHOOK + 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. + -g gateway_id, --gateway-id gateway_id + ID (or Name) of the Gateway which Relays should be returned. If not given, will result all relays from all gateways. +``` + +Currently, following commands are supported: + +- `list` + - `gateways` - list gateways in either JSON or text format + - `relays` - list relays in either JSON or text format + +- `get` + - `gateway` - get gateway details in text or JSON format + - `relay` - get relay details in text or JSON format + +- `alarm` + - `relay` - trigger an alarm whenever a new Relay checks-in on a gateway + +- `ping` - ping selected Relays + +- `channel` - channel-specific commands + - `mattermost` + - `clear` - Clear Mattermost's channel messages to improve bandwidth + - `ldap` + - `clear` - Clear LDAP attribute to improve bandwidth + - `mssql` + - `clear` - Clear DB Table entries to improve bandwidth + - `uncsharefile` + - `clear` - Remove all message files to improve bandwidth + - `dropbox` + - `clear` - Remove All Files to improve bandwidth + - `github` + - `clear` - Remove All Files to improve bandwidth + - `googledrive` + - `clear` - Remove All Files to improve bandwidth + + +### Example Usage + +**Example 1** +This example shows how to keep all of your Relays pinged every 45 seconds: + +``` +PS D:\> py c3-client.py http://192.168.0.200:52935 ping -k 45 + + :: C3 Client - a lightweight automated companion with C3 voyages + Mariusz B. / mgeeky, + +[.] Sending a ping every 45 seconds. +[.] Pinged relay: matter4 from gateway gate4 +[.] Pinged relay: mssql1 from gateway gate4 +[.] Pinged relay: ldap9 from gateway gate4 +[.] Pinged relay: mssql1 from gateway gate4 +[+] Pinged 4 active relays. + +[.] Sending a ping every 45 seconds. +[.] Pinged relay: matter4 from gateway gate4 +[.] Pinged relay: mssql1 from gateway gate4 +[.] Pinged relay: ldap9 from gateway gate4 +[.] Pinged relay: mssql1 from gateway gate4 +[+] Pinged 4 active relays. + +``` + +**Example 2** + +In this example setup an alarm that triggers upon new Relay checking-in. Whenever that happens, a command is executed with placeholders that will be substituted with values extracted from Relay's metadata: + +``` +PS D:\> py c3-client.py -f text http://192.168.0.200:52935 alarm relay -g gate4 --execute "powershell -file speak.ps1 -message \`"New C3 Relay Inbound: /, computer: \`"" + + :: C3 Client - a lightweight automated companion with C3 voyages + Mariusz B. / mgeeky, + +[.] Entering infinite-loop awaiting for new Relays... +[+] New Relay checked-in! + Relay 5: matter4 + Relay ID: 70a6f7c456f049c8 + Build ID: 795f + Is active: True (+) + Timestamp: 2021-03-24 04:14:34 + Host Info: + Computer: JUMPBOX + Domain: CONTOSO + User Name: alice + Is elevated: False + OS Version: Windows 10.0 Server SP: 0.0 Build 14393 + Process ID: 4092 + + Channels: + Gateway Return Channel (GRC) 1: + Jitter: 3.5 ... 6.5 + Properties: + Name: Output ID + Value: 3UM2G2TW + + Name: Input ID + Value: fftuO5py + + Name: Mattermost Server URL + Value: http://192.168.0.210:8080 + + Name: Mattermost Team Name + Value: foobar + + Name: Mattermost Access Token + Value: c3g7sokucbgidgxxxxxxxxxx + + Name: Channel name + Value: x26vg0 + + Name: User-Agent Header + Value: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) + +[.] Executing command: powershell -file speak.ps1 -message "New C3 Relay Inbound: CONTOSO/alice, computer: JUMPBOX" + +``` + diff --git a/red-teaming/C3-Client/c3-client.py b/red-teaming/C3-Client/c3-client.py new file mode 100644 index 0000000..89a27ce --- /dev/null +++ b/red-teaming/C3-Client/c3-client.py @@ -0,0 +1,1183 @@ +#!/usr/bin/python3 + +import os +import sys +import re +import time +import json +import requests +import subprocess +import argparse +import random +import string +from datetime import datetime + + +config = { + 'verbose' : False, + 'debug' : False, + 'host' : '', + 'command' : '', + 'format' : 'text', + 'httpauth' : '', +} + +commands = { + 'list' : [ + 'gateways', + 'relays' + ], + 'get' : [ + 'gateway', + 'relay' + ] +} + +# 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): + 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}') + + resp = requests.get(fullurl, headers=headers, auth=auth) + 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 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'] + break + elif d['type'] == 'uint16': + port = d['value'] + break + + 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} Gateway 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): + relays = [] + gateways = getRequest('/api/gateway') + + gateway_id = None + if hasattr(args, 'gateway_id') and args.gateway_id != None: + gateway_id = args.gateway_id + + relay_id = None + if hasattr(args, 'relay_id') and args.relay_id != None: + relay_id = args.relay_id + + if gateway_id != None: + gatewayId = '' + + for g in gateways: + if gateway_id.lower() == g['name'].lower(): + gatewayId = g['agentId'] + break + elif gateway_id.lower() == g['agentId'].lower(): + gatewayId = g['agentId'] + break + + if gatewayId == '': + Logger.err('Gateway with given Name/ID could not be found.') + if config['format'] == 'json': print('{}') + sys.exit(1) + + gateway = getRequest(f'/api/gateway/{gatewayId}') + + if 'relays' not in gateway.keys(): + Logger.err('Specified Gateway did not have any Relay.') + if config['format'] == 'json': print('{}') + sys.exit(1) + + if relay_id != None: + for r in gateway['relays']: + if relay_id.lower() == r['name'].lower(): + relays.append((gateway, r)) + elif relay_id.lower() == r['agentId'].lower(): + relays.append((gateway, r)) + else: + for r in gateway['relays']: + relays.append((gateway, r)) + else: + for g in gateways: + gr = getRequest(f'/api/gateway/{g["agentId"]}') + if 'relays' in gr.keys(): + for r in gr['relays']: + if relay_id != None: + if relay_id.lower() == r['name'].lower(): + relays.append((g, r)) + elif relay_id.lower() == r['agentId'].lower(): + relays.append((g, r)) + else: + relays.append((g, r)) + + return relays + +def onPing(args): + 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) + +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"]:10s} 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(gateway, secondOrder = True): + lastId = 0 + commands = getRequest(f'/api/gateway/{gateway["agentId"]}/command') + for comm in commands: + if secondOrder: + if 'data' in comm.keys(): + if 'id' in comm['data'].keys(): + if comm['data']['id'] > lastId: + lastId = comm['data']['id'] + else: + if comm['id'] > lastId: + lastId = comm['id'] + + return lastId + +def onMattermostPurge(args): + data = { + 'data' : { + 'arguments' : [], + 'command' : 'Clear all channel messages', + 'id' : 0, + 'name' : 'Mattermost' + }, + 'name' : 'ChannelCommandGroup' + } + + channels = collectChannelsToSendCommand(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 onLDAPClear(args): + data = { + 'data' : { + 'arguments' : [], + 'command' : 'Clear attribute values', + 'id' : 0, + 'name' : 'LDAP' + }, + 'name' : 'ChannelCommandGroup' + } + + channels = collectChannelsToSendCommand(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 = collectChannelsToSendCommand(args, 'table name') + + 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 = collectChannelsToSendCommand(args, 'filesystem path') + + 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 = collectChannelsToSendCommand(args, 'dropbox token') + + 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 = collectChannelsToSendCommand(args, 'github token') + + 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 = collectChannelsToSendCommand(args, 'github token') + + 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 collectChannelsToSendCommand(args, channelKeyword): + relays = collectRelays(args) + gateways = getRequest('/api/gateway') + + channel_id = None + if hasattr(args, 'channel_id') and args.channel_id != None: + channel_id = args.channel_id + + channels = [] + + for gateway, relay in relays: + if 'channels' in relay.keys(): + channel_num = 0 + + for c in relay['channels']: + channel_num += 1 + Logger.dbg(f'Iterating over channel {c["iid"]} on Relay ...') + if channel_id != None: + if c['iid'] == channel_id: + Logger.dbg(f'Adding channel {c["iid"]} in Relay {relay["name"]}.') + channels.append({ + 'url' : f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/channel/{c["iid"]}/command', + 'gateway' : gateway, + 'relay' : relay, + 'channelId' : c['iid'], + }) + continue + else: + for arg in c['propertiesText']['arguments']: + if type(arg) == dict: + if channelKeyword in arg['name'].lower() or ("description" in arg.keys() and channelKeyword in arg['description'].lower()): + Logger.dbg(f'Adding channel {c["iid"]} in Relay {relay["name"]}.') + channels.append({ + 'url' : f'/api/gateway/{gateway["agentId"]}/relay/{relay["agentId"]}/channel/{c["iid"]}/command', + 'gateway' : gateway, + 'relay' : relay, + 'channelId' : c['iid'], + }) + break + + for _gateway in gateways: + gateway = getRequest(f'/api/gateway/{_gateway["agentId"]}') + + if type(gateway) != dict: + continue + + if 'channels' in gateway.keys(): + channel_num = 0 + hadGatewayId = False + + if hasattr(args, 'gateway_id') and args.gateway_id != None: + hadGatewayId = True + if (args.gateway_id == gateway['agentId'].lower()) or (args.gateway_id == gateway['name'].lower()): + pass + else: + continue + + Logger.dbg(f'Checking channels bound to Gateway {gateway["name"]} / {gateway["agentId"]}') + + for c in gateway['channels']: + channel_num += 1 + Logger.dbg(f'Iterating over channel {c["iid"]} in Gateway...') + if channel_id != None: + if c['iid'] == channel_id: + Logger.dbg(f'Adding channel {c["iid"]} in gateway {gateway["name"]}.') + channels.append({ + 'url' : f'/api/gateway/{gateway["agentId"]}/channel/{c["iid"]}/command', + 'gateway' : gateway, + 'channelId' : c['iid'], + }) + break + else: + for arg in c['propertiesText']['arguments']: + if type(arg) == dict: + if channelKeyword in arg['name'].lower() or ("description" in arg.keys() and channelKeyword in arg['description'].lower()): + Logger.dbg(f'Adding channel {c["iid"]} in gateway {gateway["name"]}.') + channels.append({ + 'url' : f'/api/gateway/{gateway["agentId"]}/channel/{c["iid"]}/command', + 'gateway' : gateway, + 'channelId' : c['iid'], + }) + break + 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() + + if len(errs) > 0 and not surpressStderr: + error = ''' +Running shell command ({}) failed: + +--------------------------------------------- +{} +--------------------------------------------- +'''.format(cmd, errs.decode(errors='ignore')) + + if stdErrToStdout: + return error + + return status + +def onAlarmRelay(args): + origRelays = collectRelays(args) + 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: + currRelays = collectRelays(args) + currRelayIds = set() + currLastTimestamp = 0 + newestRelay = None + + for gateway, relay in currRelays: + currRelayIds.add(relay['agentId']) + if relay['timestamp'] > currLastTimestamp: + currLastTimestamp = relay['timestamp'] + newestRelay = relay + + if currLastTimestamp > lastTimestamp and len(currRelayIds) > len(origRelayIds) and newestRelay['agentId'] not in origRelayIds: + lastTimestamp = currLastTimestamp + origRelayIds = currRelayIds + + print('[+] New Relay checked-in!') + printFullRelay(newestRelay, len(currRelays)) + + try: + if args.execute != None and len(args.execute) > 0: + cmd = args.execute + 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("", newestRelay['name']) + + print(f'[.] Executing command: {cmd}') + shell(cmd) + + if args.webhook != None and len(args.webhook) > 0: + 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']), + "", newestRelay['name'], + } + + print(f'[.] Triggering a webhook: {args.webhook}') + requests.post(args.webhook, data = data, headers = headears) + + 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 + + return None + +def getValueOrRandom(val, N = 6): + if val == 'random': + return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(N)) + + return val + +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 = getLastGatewayCommandID(gateway) + 1 + commandId = getLastGatewayCommandID(gateway, False) + 1 + 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", + }, + 'id' : commandId, + 'name' : 'GatewayCommandGroup' + } + + Logger.info('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.text}') + + +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('-A', '--httpauth', metavar = 'user:pass', 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', 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', 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', help = 'ID (or Name) of the Gateway which Relays should be returned. If not given, will result all relays from all gateways.') + alarm_relay.set_defaults(func = onAlarmRelay) + + # + # 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', 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', 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', help = 'Specifies which Relay should be pinged. Can be its ID or name.') + parser_ping.add_argument('-g', '--gateway-id', 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) + + # + # Channel + # + parser_channel = subparsers.add_parser('channel', help = 'Send Channel-specific command') + parser_channel.add_argument('-c', '--channel-id', 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', help = 'Specifies Relay that runs target channel. Can be its ID or name.') + parser_channel.add_argument('-g', '--gateway-id', 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) + + ## 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 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) + + ## 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) + + ## 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) + + ## 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(''' + :: C3 Client - a lightweight automated companion with C3 voyages + Mariusz B. / mgeeky, +''') + parseArgs(argv) + + if config['format'] == 'text': + print() + +if __name__ == '__main__': + main(sys.argv) diff --git a/red-teaming/C3-Client/speak.ps1 b/red-teaming/C3-Client/speak.ps1 new file mode 100644 index 0000000..c97b027 --- /dev/null +++ b/red-teaming/C3-Client/speak.ps1 @@ -0,0 +1,7 @@ +param ( + [string]$message +) + +Add-Type -AssemblyName System.Speech +$synth = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer +$synth.Speak($message) diff --git a/red-teaming/README.md b/red-teaming/README.md index 818fc6d..4112540 100755 --- a/red-teaming/README.md +++ b/red-teaming/README.md @@ -42,6 +42,8 @@ PS > $ExecutionContext.SessionState.LanguageMode FullLanguage ``` +- [**`C3-Client`**](https://github.com/mgeeky/Penetration-Testing-Tools/tree/master/red-teaming/C3-Client) - A lightweight [F-Secure's C3](https://github.com/FSecureLABS/C3) client script letting you setup an alarm on incoming Relay, continuously ping your Relays, Clear commands queues in various channels, and others. Might be useful while working with the framework. + - **`clickOnceSharpPickTemplate.cs`** - This is a template for **C# Console Project** containing [SharpPick](https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerPick) technique of loading Powershell code from within C# application. The ClickOnce concept is to generate a windows self-updating Application that is specially privileged ([ClickOnce](https://www.slideshare.net/NetSPI/all-you-need-is-one-a-click-once-love-story-secure360-2015)) - **`cmstp-template.inf`** - INF file being a smallest possible template for **CMSTP** code execution technique, as described by [LOLBAS project](https://lolbas-project.github.io/lolbas/Binaries/Cmstp/). Sample usage: