diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index b7c905f74..72117616a 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -285,12 +285,26 @@ The process.name method is very cool, and will be even cooler when Nick d The sys.dll_inject method will attempt to inject a dll on disk into a remote process by using the `CreateRemoteThread` function call. ### sys.exec -`sys.exec(path: str, args: List, disown: bool) -> str` +`sys.exec(path: str, args: List, disown: bool) -> Dict` The sys.exec method executes a program specified with `path` and passes the `args` list. Disown will run the process in the background disowned from the agent. This is done through double forking and only works on *nix systems. -If disown is not used stdout from the process will be returned. When disown is `True` the return string will be `"No output"`. + +```python +sys.execute("/bin/bash",["-c", "whoami"]) +{ + "stdout":"root\n", + "stderr":"", + "status":0, +} +sys.execute("/bin/bash",["-c", "ls /nofile"]) +{ + "stdout":"", + "stderr":"ls: cannot access '/nofile': No such file or directory\n", + "status":2, +} +``` ### sys.is_linux `sys.is_linux() -> bool` @@ -308,7 +322,22 @@ The sys.is_macos method returns `True` if on a mac os system and `False` The sys.is_windows method returns `True` if on a windows system and `False` on everything else. ### sys.shell -`sys.shell(cmd: str) -> str` +`sys.shell(cmd: str) -> Dict` -The sys.shell method takes a string and runs it in a native interpreter. On MacOS, Linux, and *nix/bsd systems this is `/bin/bash -c `. On Windows this is `cmd /C `. Stdout from the process will be returned. If your command errors the error will be ignored and not passed back to you. +The sys.shell Given a string run it in a native interpreter. On MacOS, Linux, and *nix/bsd systems this is `/bin/bash -c `. On Windows this is `cmd /C `. Stdout, stderr, and the status code will be returned to you as a dictionary with keys: `stdout`, `stderr`, `status`. For example: + +```python +sys.shell("whoami") +{ + "stdout":"root\n", + "stderr":"", + "status":0, +} +sys.shell("ls /nofile") +{ + "stdout":"", + "stderr":"ls: cannot access '/nofile': No such file or directory\n", + "status":2, +} +``` diff --git a/implants/eldritch/src/lib.rs b/implants/eldritch/src/lib.rs index 0fde69dc6..852047347 100644 --- a/implants/eldritch/src/lib.rs +++ b/implants/eldritch/src/lib.rs @@ -175,7 +175,7 @@ sys.shell(input_params['cmd2']) "#); let param_string = r#"{"cmd":"id","cmd2":"echo hello_world","cmd3":"ls -lah /tmp/"}"#.to_string(); let test_res = eldritch_run("test.tome".to_string(), test_content, Some(param_string), &StdPrintHandler{}); - assert_eq!(test_res.unwrap().trim(), "hello_world".to_string()); + assert!(test_res?.contains("hello_world")); Ok(()) } diff --git a/implants/eldritch/src/sys.rs b/implants/eldritch/src/sys.rs index 82e855e7a..5fea69b1b 100644 --- a/implants/eldritch/src/sys.rs +++ b/implants/eldritch/src/sys.rs @@ -10,11 +10,17 @@ use derive_more::Display; use starlark::environment::{Methods, MethodsBuilder, MethodsStatic}; use starlark::values::none::NoneType; -use starlark::values::{StarlarkValue, Value, UnpackValue, ValueLike, ProvidesStaticType}; +use starlark::values::{StarlarkValue, Value, Heap, dict::Dict, UnpackValue, ValueLike, ProvidesStaticType}; use starlark::{starlark_type, starlark_simple_value, starlark_module}; use serde::{Serialize,Serializer}; +struct CommandOutput { + stdout: String, + stderr: String, + status: i32, +} + #[derive(Copy, Clone, Debug, PartialEq, Display, ProvidesStaticType, Allocative)] #[display(fmt = "SysLibrary")] pub struct SysLibrary(); @@ -51,9 +57,9 @@ impl<'v> UnpackValue<'v> for SysLibrary { // This is where all of the "sys.X" impl methods are bound #[starlark_module] fn methods(builder: &mut MethodsBuilder) { - fn exec(this: SysLibrary, path: String, args: Vec, disown: Option) -> anyhow::Result { + fn exec<'v>(this: SysLibrary, starlark_heap: &'v Heap, path: String, args: Vec, disown: Option) -> anyhow::Result> { if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); } - exec_impl::exec(path, args, disown) + exec_impl::exec(starlark_heap, path, args, disown) } fn dll_inject(this: SysLibrary, dll_path: String, pid: u32) -> anyhow::Result { if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); } @@ -71,8 +77,8 @@ fn methods(builder: &mut MethodsBuilder) { if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); } is_macos_impl::is_macos() } - fn shell(this: SysLibrary, cmd: String) -> anyhow::Result { + fn shell<'v>(this: SysLibrary, starlark_heap: &'v Heap, cmd: String) -> anyhow::Result> { if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); } - shell_impl::shell(cmd) + shell_impl::shell(starlark_heap, cmd) } } \ No newline at end of file diff --git a/implants/eldritch/src/sys/exec_impl.rs b/implants/eldritch/src/sys/exec_impl.rs index db937151b..5f7da2a98 100644 --- a/implants/eldritch/src/sys/exec_impl.rs +++ b/implants/eldritch/src/sys/exec_impl.rs @@ -1,14 +1,34 @@ use anyhow::Result; +use starlark::{values::{Heap, dict::Dict, Value}, collections::SmallMap, const_frozen_string}; use std::process::Command; -use std::str; #[cfg(any(target_os = "linux", target_os = "macos"))] use nix::{sys::wait::waitpid, unistd::{fork, ForkResult}}; #[cfg(any(target_os = "linux", target_os = "macos"))] use std::process::exit; +use super::CommandOutput; + // https://stackoverflow.com/questions/62978157/rust-how-to-spawn-child-process-that-continues-to-live-after-parent-receives-si#:~:text=You%20need%20to%20double%2Dfork,is%20not%20related%20to%20rust.&text=You%20must%20not%20forget%20to,will%20become%20a%20zombie%20process. -pub fn exec(path: String, args: Vec, disown: Option) -> Result { +pub fn exec(starlark_heap: &Heap, path: String, args: Vec, disown: Option) -> Result { + + let cmd_res = handle_exec(path, args, disown)?; + + let res = SmallMap::new(); + let mut dict_res = Dict::new(res); + let stdout_value = starlark_heap.alloc_str(cmd_res.stdout.as_str()); + dict_res.insert_hashed(const_frozen_string!("stdout").to_value().get_hashed().unwrap(), stdout_value.to_value()); + + let stderr_value = starlark_heap.alloc_str(cmd_res.stderr.as_str()); + dict_res.insert_hashed(const_frozen_string!("stderr").to_value().get_hashed().unwrap(), stderr_value.to_value()); + + let status_value = Value::new_int(cmd_res.status); + dict_res.insert_hashed(const_frozen_string!("status").to_value().get_hashed().unwrap(), status_value); + + Ok(dict_res) +} + +fn handle_exec(path: String, args: Vec, disown: Option) -> Result { let should_disown = match disown { Some(disown_option) => disown_option, None => false, @@ -20,8 +40,12 @@ pub fn exec(path: String, args: Vec, disown: Option) -> Result, disown: Option) -> Result { // Wait for intermediate process to exit. waitpid(Some(child), None).unwrap(); - return Ok("No output".to_string()); + return Ok(CommandOutput{ + stdout: "".to_string(), + stderr: "".to_string(), + status: 0, + }); } ForkResult::Child => { match unsafe{fork().expect("Failed to fork process")} { ForkResult::Parent { child } => { - if child.as_raw() < 0 { return Ok("Pid was negative. ERR".to_string()) } + if child.as_raw() < 0 { return Err(anyhow::anyhow!("Pid was negative. ERR".to_string())) } exit(0) } @@ -62,14 +90,14 @@ mod tests { use super::*; #[test] - fn test_process_exec_current_user() -> anyhow::Result<()>{ + fn test_sys_exec_current_user() -> anyhow::Result<()>{ if cfg!(target_os = "linux") || cfg!(target_os = "ios") || cfg!(target_os = "android") || cfg!(target_os = "freebsd") || cfg!(target_os = "openbsd") || cfg!(target_os = "netbsd") { - let res = exec(String::from("/bin/sh"),vec![String::from("-c"), String::from("id -u")], Some(false))?; + let res = handle_exec(String::from("/bin/sh"),vec![String::from("-c"), String::from("id -u")], Some(false))?.stdout; let mut bool_res = false; if res == "1001\n" || res == "0\n" { bool_res = true; @@ -77,13 +105,13 @@ mod tests { assert_eq!(bool_res, true); } else if cfg!(target_os = "macos") { - let res = exec(String::from("/bin/echo"),vec![String::from("hello")], Some(false))?; + let res = handle_exec(String::from("/bin/echo"),vec![String::from("hello")], Some(false))?.stdout; assert_eq!(res, "hello\n"); } else if cfg!(target_os = "windows") { - let res = exec(String::from("C:\\Windows\\System32\\cmd.exe"), vec![String::from("/c"), String::from("whoami")], Some(false))?; + let res = handle_exec(String::from("C:\\Windows\\System32\\cmd.exe"), vec![String::from("/c"), String::from("whoami")], Some(false))?.stdout; let mut bool_res = false; - if res.contains("runneradmin") || res.contains("Administrator") { + if res.contains("runneradmin") || res.contains("Administrator") || res.contains("user") { bool_res = true; } assert_eq!(bool_res, true); @@ -91,7 +119,7 @@ mod tests { Ok(()) } #[test] - fn test_process_exec_complex_linux() -> anyhow::Result<()>{ + fn test_sys_exec_complex_linux() -> anyhow::Result<()>{ if cfg!(target_os = "linux") || cfg!(target_os = "ios") || cfg!(target_os = "macos") || @@ -99,7 +127,7 @@ mod tests { cfg!(target_os = "freebsd") || cfg!(target_os = "openbsd") || cfg!(target_os = "netbsd") { - let res = exec(String::from("/bin/sh"), vec![String::from("-c"), String::from("cat /etc/passwd | awk '{print $1}' | grep -E '^root:' | awk -F \":\" '{print $3}'")], Some(false))?; + let res = handle_exec(String::from("/bin/sh"), vec![String::from("-c"), String::from("cat /etc/passwd | awk '{print $1}' | grep -E '^root:' | awk -F \":\" '{print $3}'")], Some(false))?.stdout; assert_eq!(res, "0\n"); } Ok(()) @@ -107,11 +135,11 @@ mod tests { // This is a manual test: // Example results: - // 42284 pts/0 S 0:00 /workspaces/realm/implants/target/debug/deps/eldritch-a23fc08ee1443dc3 test_process_exec_disown_linux --nocapture + // 42284 pts/0 S 0:00 /workspaces/realm/implants/target/debug/deps/eldritch-a23fc08ee1443dc3 test_sys_exec_disown_linux --nocapture // 42285 pts/0 S 0:00 \_ /bin/sh -c sleep 600 // 42286 pts/0 S 0:00 \_ sleep 600 #[test] - fn test_process_exec_disown_linux() -> anyhow::Result<()>{ + fn test_sys_exec_disown_linux() -> anyhow::Result<()>{ if cfg!(target_os = "linux") || cfg!(target_os = "ios") || cfg!(target_os = "macos") || @@ -123,7 +151,7 @@ mod tests { let path = String::from(tmp_file.path().to_str().unwrap()); tmp_file.close()?; - let _res = exec(String::from("/bin/sh"), vec![String::from("-c"), String::from(format!("touch {}", path.clone()))], Some(true))?; + let _res = handle_exec(String::from("/bin/sh"), vec![String::from("-c"), String::from(format!("touch {}", path.clone()))], Some(true))?; thread::sleep(time::Duration::from_secs(2)); println!("{:?}", path.clone().as_str()); @@ -134,10 +162,10 @@ mod tests { Ok(()) } #[test] - fn test_process_exec_complex_windows() -> anyhow::Result<()>{ + fn test_sys_exec_complex_windows() -> anyhow::Result<()>{ if cfg!(target_os = "windows") { - let res = exec(String::from("C:\\Windows\\System32\\cmd.exe"), vec![String::from("/c"), String::from("wmic useraccount get name | findstr /i admin")], Some(false))?; - assert_eq!(res.contains("runneradmin") || res.contains("Administrator"), true); + let res = handle_exec(String::from("C:\\Windows\\System32\\cmd.exe"), vec![String::from("/c"), String::from("wmic useraccount get name | findstr /i admin")], Some(false))?.stdout; + assert!(res.contains("runner") || res.contains("Administrator") || res.contains("user")); } Ok(()) } diff --git a/implants/eldritch/src/sys/shell_impl.rs b/implants/eldritch/src/sys/shell_impl.rs index b04e8bf88..2dd8a2d42 100644 --- a/implants/eldritch/src/sys/shell_impl.rs +++ b/implants/eldritch/src/sys/shell_impl.rs @@ -1,79 +1,76 @@ use anyhow::Result; -use std::process::Command; +use starlark::collections::SmallMap; +use starlark::const_frozen_string; +use starlark::values::{Heap, Value}; +use starlark::values::dict::Dict; +use std::process::{Command}; use std::str; -pub fn shell(cmd: String) -> Result { - if cfg!(target_os = "linux") || - cfg!(target_os = "ios") || - cfg!(target_os = "android") || - cfg!(target_os = "freebsd") || - cfg!(target_os = "openbsd") || - cfg!(target_os = "netbsd") { - - let res = Command::new("bash") - .args(["-c", cmd.as_str()]) - .output() - .expect("failed to execute process"); - let resstr = str::from_utf8(&res.stdout).unwrap(); - return Ok(String::from(resstr)); - } - else if cfg!(target_os = "macos") { - let res = Command::new("bash") - .args(["-c", cmd.as_str()]) - .output() - .expect("failed to execute process"); - let resstr = str::from_utf8(&res.stdout).unwrap(); - return Ok(String::from(resstr)); - } - else if cfg!(target_os = "windows") { - let res = Command::new("cmd") - .args(["/C", cmd.as_str()]) - .output() - .expect("failed to execute process"); - let resstr = str::from_utf8(&res.stdout).unwrap(); - return Ok(String::from(resstr)); - }else{ - return Err(anyhow::anyhow!("This OS isn't supported by sys.shell.\n\n")); +use super::CommandOutput; + +pub fn shell(starlark_heap: &Heap, cmd: String) -> Result { + + let cmd_res = handle_shell(cmd)?; + + let res = SmallMap::new(); + let mut dict_res = Dict::new(res); + let stdout_value = starlark_heap.alloc_str(cmd_res.stdout.as_str()); + dict_res.insert_hashed(const_frozen_string!("stdout").to_value().get_hashed().unwrap(), stdout_value.to_value()); + + let stderr_value = starlark_heap.alloc_str(cmd_res.stderr.as_str()); + dict_res.insert_hashed(const_frozen_string!("stderr").to_value().get_hashed().unwrap(), stderr_value.to_value()); + + let status_value = Value::new_int(cmd_res.status); + dict_res.insert_hashed(const_frozen_string!("status").to_value().get_hashed().unwrap(), status_value); + + Ok(dict_res) +} + +fn handle_shell(cmd: String) -> Result { + let command_string: &str; + let command_args: Vec<&str>; + + if cfg!(target_os = "macos") { + command_string = "bash"; + command_args = ["-c", cmd.as_str()].to_vec(); + } else if cfg!(target_os = "windows") { + command_string = "cmd"; + command_args = ["/c", cmd.as_str()].to_vec(); + } else if cfg!(target_os = "linux") { + command_string = "bash"; + command_args = ["-c", cmd.as_str()].to_vec(); + } else { // linux and such + command_string = "bash"; + command_args = ["-c", cmd.as_str()].to_vec(); } + + let tmp_res = Command::new(command_string) + .args(command_args) + .output() + .expect("failed to execute process"); + + return Ok(CommandOutput{ + stdout: String::from_utf8(tmp_res.stdout)?, + stderr: String::from_utf8(tmp_res.stderr)?, + status: tmp_res.status.code().expect("Failed to retrieve error code"), + }); } #[cfg(test)] mod tests { + use starlark::{syntax::{AstModule, Dialect}, starlark_module, environment::{GlobalsBuilder, Module}, eval::Evaluator, values::Value}; + use super::*; #[test] - fn test_process_shell_current_user() -> anyhow::Result<()>{ - let res = shell(String::from("whoami"))?; - println!("{:?}", res); - if cfg!(target_os = "linux") || - cfg!(target_os = "ios") || - cfg!(target_os = "android") || - cfg!(target_os = "freebsd") || - cfg!(target_os = "openbsd") || - cfg!(target_os = "netbsd") { - let mut bool_res = false; - if res == "runner\n" || res == "root\n" { - bool_res = true; - } - assert_eq!(bool_res, true); - } - else if cfg!(target_os = "macos") { - let mut bool_res = false; - if res == "runner\n" || res == "root\n" { - bool_res = true; - } - assert_eq!(bool_res, true); - } - else if cfg!(target_os = "windows") { - let mut bool_res = false; - if res.contains("runneradmin") || res.contains("Administrator") { - bool_res = true; - } - assert_eq!(bool_res, true); - } + fn test_sys_shell_current_user() -> anyhow::Result<()>{ + let res = handle_shell(String::from("whoami"))?.stdout; + println!("{}",res); + assert!(res.contains("runner") || res.contains("Administrator") || res.contains("root") || res.contains("user")); Ok(()) } + #[test] - fn test_process_shell_complex_linux() -> anyhow::Result<()>{ + fn test_sys_shell_complex_linux() -> anyhow::Result<()>{ if cfg!(target_os = "linux") || cfg!(target_os = "ios") || cfg!(target_os = "macos") || @@ -81,17 +78,52 @@ mod tests { cfg!(target_os = "freebsd") || cfg!(target_os = "openbsd") || cfg!(target_os = "netbsd") { - let res = shell(String::from("cat /etc/passwd | awk '{print $1}' | grep -E '^root:' | awk -F \":\" '{print $3}'"))?; + let res = handle_shell(String::from("cat /etc/passwd | awk '{print $1}' | grep -E '^root:' | awk -F \":\" '{print $3}'"))?.stdout; assert_eq!(res, "0\n"); } Ok(()) } #[test] - fn test_process_shell_complex_windows() -> anyhow::Result<()>{ + fn test_sys_shell_complex_windows() -> anyhow::Result<()>{ if cfg!(target_os = "windows") { - let res = shell(String::from("wmic useraccount get name | findstr /i admin"))?; - assert_eq!(res.contains("runneradmin") || res.contains("Administrator"), true); + let res = handle_shell(String::from("wmic useraccount get name | findstr /i admin"))?.stdout; + assert!(res.contains("runner") || res.contains("Administrator") || res.contains("user")); } Ok(()) } + + #[test] + fn test_sys_shell_from_interpreter() -> anyhow::Result<()>{ + // Create test script + let test_content = format!(r#" +func_shell("whoami") +"#); + + // Setup starlark interpreter with handle to our function + let ast: AstModule; + match AstModule::parse( + "test.eldritch", + test_content.to_owned(), + &Dialect::Standard + ) { + Ok(res) => ast = res, + Err(err) => return Err(err), + } + + #[starlark_module] + fn func_shell(builder: &mut GlobalsBuilder) { + fn func_shell<'v>(starlark_heap: &'v Heap, cmd: String) -> anyhow::Result> { + shell(starlark_heap, cmd) + } + } + + let globals = GlobalsBuilder::extended().with(func_shell).build(); + let module: Module = Module::new(); + + let mut eval: Evaluator = Evaluator::new(&module); + let res: Value = eval.eval_module(ast, &globals).unwrap(); + let res_string = res.to_string(); + assert!(res_string.contains("runner") || res_string.contains("Administrator") || res_string.contains("root") || res_string.contains("user")); + Ok(()) + } } \ No newline at end of file diff --git a/implants/imix/src/main.rs b/implants/imix/src/main.rs index 258fa80a2..33931c676 100644 --- a/implants/imix/src/main.rs +++ b/implants/imix/src/main.rs @@ -449,7 +449,7 @@ mod tests { description: "Execute a command in the default system shell".to_string(), eldritch: r#" print("custom_print_handler_test") -sys.shell(input_params["cmd"]) +sys.shell(input_params["cmd"])["stdout"] "#.to_string(), files: None, param_defs: Some(r#"{"params":[{"name":"cmd","type":"string"}]}"#.to_string()), @@ -492,7 +492,6 @@ sys.shell(input_params["cmd"]) } - #[test] fn imix_test_main_loop_sleep_twice_short() -> Result<()> { // Response expectations are poped in reverse order. @@ -685,7 +684,5 @@ print("main_loop_test_success")"#.to_string(), assert!(true); Ok(()) } - - }