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

194 expand shell command to return exit code and stderr #209

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
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