Skip to content

Commit

Permalink
194 expand shell command to return exit code and stderr (#209)
Browse files Browse the repository at this point in the history
* sysshell now return dict with stdout stderr status

* Expanded sys exec output.

* Update tests.yml (#207)

* Update tests.yml

needed

* Update tests.yml

ugg windows

* Update tests.yml

fml

* 182 eldritch sleep blocks imix agent (#208)

* Couple tests.

* Resolve async errors.

* 193 resolve docs issues (#195)

* Updated docs.

* Added golem to toc.

* Fix typo start to sort by name.

* Add assets docs.

* Unified wording.

* Clean up spelling.

* Update tests.

* Removed double defined test.

---------

Co-authored-by: Nicholas O'Brien <ndo9903@rit.edu>
  • Loading branch information
hulto and Cictrone authored Jun 16, 2023
1 parent 26ff8be commit 10683d4
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 100 deletions.
37 changes: 33 additions & 4 deletions docs/_docs/user-guide/eldritch.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,26 @@ The <b>process.name</b> method is very cool, and will be even cooler when Nick d
The <b>sys.dll_inject</b> 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<str>, disown: bool) -> str`
`sys.exec(path: str, args: List<str>, disown: bool) -> Dict`

The <b>sys.exec</b> 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`
Expand All @@ -308,7 +322,22 @@ The <b>sys.is_macos</b> method returns `True` if on a mac os system and `False`
The <b>sys.is_windows</b> 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 <b>sys.shell</b> method takes a string and runs it in a native interpreter. On MacOS, Linux, and *nix/bsd systems this is `/bin/bash -c <your command>`. On Windows this is `cmd /C <your command>`. Stdout from the process will be returned. If your command errors the error will be ignored and not passed back to you.
The <b>sys.shell</b> Given a string run it in a native interpreter. On MacOS, Linux, and *nix/bsd systems this is `/bin/bash -c <your command>`. On Windows this is `cmd /C <your command>`. 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,
}
```

2 changes: 1 addition & 1 deletion implants/eldritch/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down
16 changes: 11 additions & 5 deletions implants/eldritch/src/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<String>, disown: Option<bool>) -> anyhow::Result<String> {
fn exec<'v>(this: SysLibrary, starlark_heap: &'v Heap, path: String, args: Vec<String>, disown: Option<bool>) -> anyhow::Result<Dict<'v>> {
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<NoneType> {
if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); }
Expand All @@ -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<String> {
fn shell<'v>(this: SysLibrary, starlark_heap: &'v Heap, cmd: String) -> anyhow::Result<Dict<'v>> {
if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); }
shell_impl::shell(cmd)
shell_impl::shell(starlark_heap, cmd)
}
}
66 changes: 47 additions & 19 deletions implants/eldritch/src/sys/exec_impl.rs
Original file line number Diff line number Diff line change
@@ -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<String>, disown: Option<bool>) -> Result<String> {
pub fn exec(starlark_heap: &Heap, path: String, args: Vec<String>, disown: Option<bool>) -> Result<Dict> {

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<String>, disown: Option<bool>) -> Result<CommandOutput> {
let should_disown = match disown {
Some(disown_option) => disown_option,
None => false,
Expand All @@ -20,8 +40,12 @@ pub fn exec(path: String, args: Vec<String>, disown: Option<bool>) -> Result<Str
.output()
.expect("failed to execute process");

let resstr = str::from_utf8(&res.stdout).unwrap();
return Ok(String::from(resstr));
let res = CommandOutput {
stdout: String::from_utf8(res.stdout)?,
stderr: String::from_utf8(res.stderr)?,
status: res.status.code().expect("Failed to retrive status code"),
};
return Ok(res);
}else{
#[cfg(target_os = "windows")]
return Err(anyhow::anyhow!("Windows is not supported for disowned processes."));
Expand All @@ -31,13 +55,17 @@ pub fn exec(path: String, args: Vec<String>, disown: Option<bool>) -> Result<Str
ForkResult::Parent { child } => {
// 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)
}

Expand All @@ -62,56 +90,56 @@ 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;
}
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);
}
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") ||
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("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(())
}

// 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") ||
Expand All @@ -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());
Expand All @@ -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(())
}
Expand Down
Loading

0 comments on commit 10683d4

Please sign in to comment.