Skip to content

Commit

Permalink
Expanded sys exec output.
Browse files Browse the repository at this point in the history
  • Loading branch information
hulto committed Jun 13, 2023
1 parent 7992a2d commit c8fabb5
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 28 deletions.
26 changes: 22 additions & 4 deletions docs/_docs/user-guide/eldritch.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,28 @@ 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"`.
When disown is true the returned dictionary will be `{"stdout":"","stderr":"","status":0}`.
If disown is not used stdout from the process will be returned stdout, stderr, and the status code will be returned to you as a dictionary with keys: `stdout`, `stderr`, `status`. For example:

```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 @@ -163,20 +179,22 @@ The <b>sys.is_windows</b> method returns true if on a windows system and fales o
The <b>sys.is_macos</b> method returns true if on a mac os system and false on everything else.

### sys.shell
`sys.shell(cmd: str) -> str`
`sys.shell(cmd: str) -> Dict`

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 and stderr will be returned to you as a dictionary with keys: `stdout`, `stderr`. For example:
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,
}
```

Expand Down
10 changes: 8 additions & 2 deletions implants/eldritch/src/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ 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 Down
62 changes: 45 additions & 17 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,26 +90,26 @@ 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") {
bool_res = true;
Expand All @@ -91,27 +119,27 @@ 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") ||
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,9 +162,9 @@ 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))?;
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_eq!(res.contains("runneradmin") || res.contains("Administrator"), true);
}
Ok(())
Expand Down
8 changes: 3 additions & 5 deletions implants/eldritch/src/sys/shell_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use starlark::values::dict::Dict;
use std::process::{Command};
use std::str;

use super::CommandOutput;

pub fn shell(starlark_heap: &Heap, cmd: String) -> Result<Dict> {

let cmd_res = handle_shell(cmd)?;
Expand All @@ -23,11 +25,7 @@ pub fn shell(starlark_heap: &Heap, cmd: String) -> Result<Dict> {

Ok(dict_res)
}
struct CommandOutput {
stdout: String,
stderr: String,
status: i32,
}

fn handle_shell(cmd: String) -> Result<CommandOutput> {
let command_string: &str;
let command_args: Vec<&str>;
Expand Down

0 comments on commit c8fabb5

Please sign in to comment.