Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[implant] receive encoded command #22

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 3 additions & 18 deletions src/api/manage_inject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use std::io::{BufWriter, Write};

use mailparse::{parse_content_disposition, parse_header};
use serde::{Deserialize, Serialize};
use ureq::serde_json::Value;

use crate::common::error_model::Error;

Expand Down Expand Up @@ -55,21 +54,6 @@ pub struct InjectorContractPayload {
pub payload_cleanup_command: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct InjectorContract {
pub injector_contract_id: String,
pub injector_contract_payload: InjectorContractPayload,
}

#[derive(Debug, Deserialize)]
pub struct InjectResponse {
pub inject_id: String,
pub inject_title: String,
pub inject_description: Option<String>,
pub inject_content: Value,
pub inject_injector_contract: InjectorContract,
}

#[derive(Debug, Deserialize)]
pub struct UpdateInjectResponse {
#[allow(dead_code)]
Expand All @@ -84,8 +68,9 @@ pub struct UpdateInput {
}

impl Client {
pub fn get_inject(&self, inject_id: String) -> Result<InjectResponse, Error> {
return match self.get(&format!("/api/injects/{}", inject_id)).call() {

pub fn get_executable_payload(&self, inject_id: String) -> Result<InjectorContractPayload, Error> {
return match self.get(&format!("/api/injects/{}/executable-payload", inject_id)).call() {
Ok(response) => Ok(response.into_json()?),
Err(ureq::Error::Status(_, response)) => {
Err(Error::Api(response.into_string().unwrap()))
Expand Down
62 changes: 5 additions & 57 deletions src/handle/handle_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,19 @@ use std::path::PathBuf;
use std::time::Instant;

use log::info;
use regex::Regex;

use crate::api::Client;
use crate::api::manage_inject::{InjectorContract, InjectorContractPayload, InjectResponse};
use crate::api::manage_inject::{InjectorContractPayload};
use crate::handle::handle_execution::handle_execution_result;
use crate::process::command_exec::command_execution;

pub fn compute_parameters(command: &String) -> Vec<&str> {
let re = Regex::new(r"#\{([^#{}]+)}").unwrap();
let mut command_parameters = vec![];
for (_, [id]) in re.captures_iter(command).map(|c| c.extract()) {
command_parameters.push(id);
}
return command_parameters;
}

fn compute_working_dir() -> PathBuf {
let current_exe_patch = env::current_exe().unwrap();
return current_exe_patch.parent().unwrap().to_path_buf();
}

pub fn compute_command(command: &String, inject_data: &InjectResponse) -> String {
let command_parameters = compute_parameters(&command);
let mut executable_command = command.clone();
if command_parameters.len() > 0 {
let config_map = inject_data.inject_content.as_object().unwrap();
for parameter in command_parameters {
let key = format!("#{{{}}}", parameter);
// Try to fill the values with user arguments
let param_value = config_map.get(parameter).unwrap().as_str();
if param_value.is_some() {
executable_command = executable_command.replace(key.as_str(), param_value.unwrap())
} else {
// Try to fill the values with default
let InjectResponse {
inject_injector_contract,
..
} = inject_data;
let InjectorContract {
injector_contract_payload,
..
} = inject_injector_contract;
let InjectorContractPayload {
payload_arguments, ..
} = injector_contract_payload;
let empty_arguments = vec![];
let arguments = match payload_arguments {
None => &empty_arguments,
Some(args) => args,
};
let arg = arguments.iter().find(|arg| arg.key == parameter);
if arg.is_some() {
let arg_value = arg.unwrap();
let default_value = arg_value.default_value.clone();
if default_value.is_some() {
executable_command = executable_command
.replace(key.as_str(), default_value.unwrap().as_str())
}
}
}
}
}
pub fn compute_command(command: &String) -> String {
let executable_command = command.clone();
let working_dir = compute_working_dir();
return executable_command.replace("#{location}", working_dir.to_str().unwrap());
}
Expand All @@ -84,13 +35,10 @@ pub fn handle_execution_command(
return handle_execution_result(semantic, api, inject_id, command_result, elapsed);
}

pub fn handle_command(inject_id: String, api: &Client, inject_data: &InjectResponse) {
let contract_payload = &inject_data
.inject_injector_contract
.injector_contract_payload;
pub fn handle_command(inject_id: String, api: &Client, contract_payload: &InjectorContractPayload) {
let command = contract_payload.command_content.clone().unwrap();
let executor = contract_payload.command_executor.clone().unwrap();
let executable_command = compute_command(&command, &inject_data);
let executable_command = compute_command(&command);
let _ = handle_execution_command(
"implant execution",
&api,
Expand Down
8 changes: 3 additions & 5 deletions src/handle/handle_dns_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ use std::net::{SocketAddr, ToSocketAddrs};
use log::info;

use crate::api::Client;
use crate::api::manage_inject::{InjectResponse, UpdateInput};
use crate::api::manage_inject::{InjectorContractPayload, UpdateInput};
use crate::handle::ExecutionOutput;

pub fn handle_dns_resolution(inject_id: String, api: &Client, inject_data: &InjectResponse) {
let hostname_raw = &inject_data
.inject_injector_contract
.injector_contract_payload
pub fn handle_dns_resolution(inject_id: String, api: &Client, contract_payload: &InjectorContractPayload) {
let hostname_raw = &contract_payload
.dns_resolution_hostname;
let data = hostname_raw.clone().unwrap();
let hostnames = data.split("\n");
Expand Down
14 changes: 3 additions & 11 deletions src/handle/handle_file_drop.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
use crate::api::Client;
use crate::api::manage_inject::{InjectorContract, InjectorContractPayload, InjectResponse};
use crate::api::manage_inject::{InjectorContractPayload};
use crate::handle::handle_file::handle_file;

pub fn handle_file_drop(inject_id: String, api: &Client, inject_data: &InjectResponse) {
let InjectResponse {
inject_injector_contract,
..
} = inject_data;
let InjectorContract {
injector_contract_payload,
..
} = inject_injector_contract;
let InjectorContractPayload { file_drop_file, .. } = injector_contract_payload;
pub fn handle_file_drop(inject_id: String, api: &Client, contract_payload: &InjectorContractPayload) {
let InjectorContractPayload { file_drop_file, .. } = contract_payload;
let _ = handle_file(inject_id, api, file_drop_file, false);
}
14 changes: 3 additions & 11 deletions src/handle/handle_file_execute.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
use crate::api::Client;
use crate::api::manage_inject::{InjectorContract, InjectorContractPayload, InjectResponse};
use crate::api::manage_inject::{InjectorContractPayload};
use crate::handle::handle_file::{handle_execution_file, handle_file};

pub fn handle_file_execute(inject_id: String, api: &Client, inject_data: &InjectResponse) {
let InjectResponse {
inject_injector_contract,
..
} = inject_data;
let InjectorContract {
injector_contract_payload,
..
} = inject_injector_contract;
pub fn handle_file_execute(inject_id: String, api: &Client, contract_payload: &InjectorContractPayload) {
let InjectorContractPayload {
executable_file, ..
} = injector_contract_payload;
} = contract_payload;
let handle_file = handle_file(inject_id.clone(), api, executable_file, false);
match handle_file {
Ok(filename) => {
Expand Down
30 changes: 14 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use log::info;
use rolling_file::{BasicRollingFileAppender, RollingConditionBasic};

use crate::api::Client;
use crate::api::manage_inject::{InjectResponse, UpdateInput};
use crate::api::manage_inject::{InjectorContractPayload, UpdateInput};
use crate::common::error_model::Error;
use crate::handle::handle_command::{compute_command, handle_command, handle_execution_command};
use crate::handle::handle_dns_resolution::handle_dns_resolution;
Expand Down Expand Up @@ -42,11 +42,8 @@ pub fn mode() -> String {
return env::var("env").unwrap_or_else(|_| ENV_PRODUCTION.into());
}

pub fn handle_payload(inject_id: String, api: &Client, inject_data: &InjectResponse) {
pub fn handle_payload(inject_id: String, api: &Client, contract_payload: &InjectorContractPayload) {
let mut prerequisites_code = 0;
let contract_payload = &inject_data
.inject_injector_contract
.injector_contract_payload;
// region prerequisite execution
let prerequisites_data = &contract_payload.payload_prerequisites;
let empty_prerequisites = vec![];
Expand All @@ -58,7 +55,7 @@ pub fn handle_payload(inject_id: String, api: &Client, inject_data: &InjectRespo
let mut check_status = 0;
let check_cmd = &prerequisite.check_command;
if check_cmd.is_some() {
let check_prerequisites = compute_command(check_cmd.as_ref().unwrap(), &inject_data);
let check_prerequisites = compute_command(check_cmd.as_ref().unwrap());
check_status = handle_execution_command(
"prerequisite check",
&api,
Expand All @@ -70,7 +67,7 @@ pub fn handle_payload(inject_id: String, api: &Client, inject_data: &InjectRespo
}
// If exit 0, prerequisite are already satisfied
if check_status != 0 {
let install_prerequisites = compute_command(&prerequisite.get_command, &inject_data);
let install_prerequisites = compute_command(&prerequisite.get_command);
prerequisites_code += handle_execution_command(
"prerequisite execution",
&api,
Expand All @@ -87,10 +84,10 @@ pub fn handle_payload(inject_id: String, api: &Client, inject_data: &InjectRespo
if prerequisites_code == 0 {
let payload_type = &contract_payload.payload_type;
match payload_type.as_str() {
"Command" => handle_command(inject_id.clone(), &api, &inject_data),
"DnsResolution" => handle_dns_resolution(inject_id.clone(), &api, &inject_data),
"Executable" => handle_file_execute(inject_id.clone(), &api, &inject_data),
"FileDrop" => handle_file_drop(inject_id.clone(), &api, &inject_data),
"Command" => handle_command(inject_id.clone(), &api, &contract_payload),
"DnsResolution" => handle_dns_resolution(inject_id.clone(), &api, &contract_payload),
"Executable" => handle_file_execute(inject_id.clone(), &api, &contract_payload),
"FileDrop" => handle_file_drop(inject_id.clone(), &api, &contract_payload),
// "NetworkTraffic" => {}, // Not implemented yet
_ => {
let _ = api.update_status(
Expand Down Expand Up @@ -120,10 +117,10 @@ pub fn handle_payload(inject_id: String, api: &Client, inject_data: &InjectRespo
// Cleanup command will be executed independently of the previous commands success.
let cleanup = contract_payload.payload_cleanup_command.clone();
if cleanup.is_some() && !cleanup.clone().unwrap().is_empty() {
let executable_cleanup = compute_command(&cleanup.unwrap(), &inject_data);
let executable_cleanup = compute_command(&cleanup.unwrap());
let executor = contract_payload.payload_cleanup_executor.clone().unwrap();
let _ = handle_execution_command(
"prerequisite cleanup",
"cleanup execution",
&api,
inject_id.clone(),
&executable_cleanup,
Expand All @@ -148,12 +145,13 @@ fn main() -> Result<(), Error> {
.init();
// endregion
// region Process execution

let args = Args::parse();
info!("Starting OpenBAS implant {} {}", VERSION, mode());
let api = Client::new(args.uri, args.token, args.unsecured_certificate == "true", args.with_proxy == "true");
let inject = api.get_inject(args.inject_id.clone());
let inject_data = inject.unwrap_or_else(|err| panic!("Fail getting inject {}", err));
handle_payload(args.inject_id.clone(), &api, &inject_data);
let payload = api.get_executable_payload(args.inject_id.clone());
let contract_payload = payload.unwrap_or_else(|err| panic!("Fail getting payload {}", err));
handle_payload(args.inject_id.clone(), &api, &contract_payload);
// endregion
return Ok(());
}
58 changes: 25 additions & 33 deletions src/process/command_exec.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use std::process::{Child, Command, Output, Stdio};

use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use serde::Deserialize;

use crate::common::error_model::Error;
Expand All @@ -25,7 +23,7 @@ pub fn invoke_command(echo_cmd: Child, executor: &str) -> std::io::Result<Output

pub fn invoke_powershell_command(command: &str, executor: &str, args: &[&str]) -> std::io::Result<Output> {
// For powershell complex command, we need to encode in base64 to manage escape caracters and multi lines commands
let invoke_expression = format!("$ErrorActionPreference = 'Stop'; Invoke-Expression ([System.Text.Encoding]::UTF8.GetString([convert]::FromBase64String(\"{}\")))", BASE64_STANDARD.encode(command));
let invoke_expression = format!("$ErrorActionPreference = 'Stop'; Invoke-Expression ([System.Text.Encoding]::UTF8.GetString([convert]::FromBase64String(\"{}\")))", command);
Command::new(executor)
.args(args)
.arg(invoke_expression)
Expand All @@ -37,34 +35,44 @@ pub fn invoke_powershell_command(command: &str, executor: &str, args: &[&str]) -

pub fn invoke_shell_command(command: &str, executor: &str) -> std::io::Result<Output> {
// For shell complex command, we need to encode in base64 to manage escape caracters and multi lines commands
let base64_child = Command::new(executor)
let base64_command = format!("echo {} | base64 -d", command);
let base64_child = Command::new(executor) // Use 'sh' to interpret the pipe correctly
MarineLeM marked this conversation as resolved.
Show resolved Hide resolved
.arg("-c")
.arg("echo ".to_owned() + &BASE64_STANDARD.encode(command) + " | base64 --d")
.arg(&base64_command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;

invoke_command(base64_child, executor)
}

pub fn invoke_windows_command(command: &str) -> std::io::Result<Output> {
// To manage multi lines (we need more than just base64 like the other executor), we replace break line (\n) by &
// \n can be found in Windows path (ex: C:\\newFile) but \n replaces only break line and not \\n in path
let new_command = format!(
"setlocal & {} & if errorlevel 1 exit /b 1",
command.trim().replace("\n", " & ") // trim "cleans" the start and the end of the command (see the trim doc)
);
let invoke_expression = format!("([System.Text.Encoding]::UTF8.GetString([convert]::FromBase64String(\"{}\")))", BASE64_STANDARD.encode(new_command));
let invoke_expression = format!("([System.Text.Encoding]::UTF8.GetString([convert]::FromBase64String(\"{}\")))", command);
let base64_child = Command::new("powershell.exe")
.arg("-Command")
.arg(&invoke_expression)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
invoke_command(base64_child, "cmd")
.output()?;

let decoded_command = String::from_utf8_lossy(&base64_child.stdout).trim().to_string();

let cmd_expression = format!(
"setlocal & {} & if errorlevel 1 exit /b 1",
decoded_command
);

Command::new("cmd.exe")
.arg("/C")
.arg(cmd_expression)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
.wait_with_output()
}

pub fn manage_result(invoke_output: Output, pre_check: bool) -> Result<ExecutionResult, Error> {
let invoke_result = invoke_output.clone();
// 0 success | other = maybe prevented
let exit_code = invoke_result.status.code().unwrap_or_else(|| -99);
let stdout = String::from_utf8_lossy(&invoke_result.stdout).to_string();
let stderr = String::from_utf8_lossy(&invoke_result.stderr).to_string();
Expand Down Expand Up @@ -110,31 +118,15 @@ pub fn command_execution(command: &str, executor: &str, pre_check: bool) -> Resu
manage_result(invoke_output?, pre_check)
}

pub fn invoke_unix_command(command: &str, executor: &str) -> std::io::Result<Output> {
// For unix shell complex command, we need to encode in base64 to manage escape caracters (pipes,...) and multi lines commands
let echo_child = Command::new("echo")
.arg(BASE64_STANDARD.encode(command))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let base64_child = Command::new("base64")
.arg("-d")
.stdin(Stdio::from(echo_child.stdout.unwrap()))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
invoke_command(base64_child, executor)
}

#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn command_execution(command: &str, executor: &str, pre_check: bool) -> Result<ExecutionResult, Error> {
let invoke_output;
if executor == "bash" {
invoke_output = invoke_unix_command(command, "bash");
invoke_output = invoke_shell_command(command, "bash");
} else if executor == "psh" {
invoke_output = invoke_powershell_command(command, "powershell", &["-c"]);
} else {
invoke_output = invoke_unix_command(command, "sh");
invoke_output = invoke_shell_command(command, "sh");
}
manage_result(invoke_output?, pre_check)
}