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(())
}
-
-
}