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

242 implement pivotssh exec function #243

Merged
merged 6 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
16 changes: 13 additions & 3 deletions docs/_docs/user-guide/eldritch.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,19 @@ The <b>pivot.port_forward</b> method is being proposed to provide socat like fun
The <b>pivot.smb_exec</b> method is being proposed to allow users a way to move between hosts running smb.

### pivot.ssh_exec
`pivot.ssh_exec(target: str, port: int, username: str, password: str, key: str, command: str, shell_path: str) -> List<str>`
`pivot.ssh_exec(target: str, port: int, command: str, username: str, password: Optional<str>, key: Optional<str>, key_password: Optional<str>, timeout: Optional<int>) -> List<Dict>`

The <b>pivot.ssh_exec</b> method is being proposed to allow users a way to move between hosts running ssh.
The <b>pivot.ssh_exec</b> method executes a command string on the remote host using the default shell. If no password or key is specified the function will error out with:
`Failed to run handle_ssh_exec: Failed to authenticate to host`
If the connection is successful but the command fails no output will be returned but the status code will be set.
Not returning stderr is a limitation of the way we're performing execution. Since it's not using the SSH shell directive we're limited on the return output we can capture.

```json
{
"stdout": "uid=1000(kali) gid=1000(kali) groups=1000(kali),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),118(bluetooth),128(lpadmin),132(scanner),143(docker)\n",
"status": 0
}
```

### pivot.ssh_password_spray
`pivot.ssh_password_spray(targets: List<str>, port: int, credentials: List<str>, keys: List<str>, command: str, shell_path: str) -> List<str>`
Expand Down Expand Up @@ -285,7 +295,7 @@ 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) -> Dict`
`sys.exec(path: str, args: List<str>, disown: Optional<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.
Expand Down
2 changes: 2 additions & 0 deletions implants/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ predicates = "2.1"
rand = "0.8.5"
regex = "1.5.5"
reqwest = "0.11.4"
russh = "0.37.1"
russh-keys = "0.37.1"
rust-embed = "6.6.0"
serde = "1.0"
serde_json = "1.0.87"
Expand Down
3 changes: 3 additions & 0 deletions implants/lib/eldritch/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ allocative = { workspace = true }
allocative_derive = { workspace = true }
anyhow = { workspace = true }
async-recursion = { workspace = true }
async-trait = { workspace = true }
derive_more = { workspace = true }
eval = { workspace = true }
flate2 = { workspace = true }
gazebo = { workspace = true }
nix = { workspace = true }
regex = { workspace = true }
reqwest = { workspace = true , features = ["blocking", "stream"] }
russh = { workspace = true }
russh-keys = { workspace = true }
rust-embed = { workspace = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = { workspace = true }
Expand Down
116 changes: 112 additions & 4 deletions implants/lib/eldritch/src/pivot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ mod port_forward_impl;
mod ncat_impl;
mod bind_proxy_impl;

use std::io::Write;
use std::sync::Arc;

use allocative::Allocative;
use async_trait::async_trait;
use derive_more::Display;

use russh::{client, Disconnect};
use russh_keys::{key, decode_secret_key};
use starlark::values::dict::Dict;
use starlark::environment::{Methods, MethodsBuilder, MethodsStatic};
use starlark::values::none::NoneType;
use starlark::values::{StarlarkValue, Value, UnpackValue, ValueLike, ProvidesStaticType, Heap};
use starlark::{starlark_type, starlark_simple_value, starlark_module};

use serde::{Serialize,Serializer};
use tokio::net::ToSocketAddrs;

#[derive(Copy, Clone, Debug, PartialEq, Display, ProvidesStaticType, Allocative)]
#[display(fmt = "PivotLibrary")]
Expand Down Expand Up @@ -54,9 +61,9 @@ impl<'v> UnpackValue<'v> for PivotLibrary {
// This is where all of the "file.X" impl methods are bound
#[starlark_module]
fn methods(builder: &mut MethodsBuilder) {
fn ssh_exec(this: PivotLibrary, target: String, port: i32, username: String, password: String, key: String, command: String, shell_path: String) -> anyhow::Result<String> {
if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); }
ssh_exec_impl::ssh_exec(target, port, username, password, key, command, shell_path)
fn ssh_exec<'v>(this: PivotLibrary, starlark_heap: &'v Heap, target: String, port: i32, command: String, username: String, password: Option<String>, key: Option<String>, key_password: Option<String>, timeout: Option<u32>) -> anyhow::Result<Dict<'v>> {
if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); }
ssh_exec_impl::ssh_exec(starlark_heap, target, port, command, username, password, key, key_password, timeout)
}
fn ssh_password_spray(this: PivotLibrary, targets: Vec<String>, port: i32, credentials: Vec<String>, keys: Vec<String>, command: String, shell_path: String) -> anyhow::Result<String> {
if false { println!("Ignore unused this var. _this isn't allowed by starlark. {:?}", this); }
Expand Down Expand Up @@ -96,4 +103,105 @@ fn methods(builder: &mut MethodsBuilder) {
// ssh_copy_impl::ssh_copy(target, port, username, password, key, command, shell_path, src, dst)?;
// Ok(NoneType{})
// }
}
}



// SSH Client utils
struct Client {}

#[async_trait]
impl client::Handler for Client {
type Error = russh::Error;

async fn check_server_key(
self,
_server_public_key: &key::PublicKey,
) -> Result<(Self, bool), Self::Error> {
Ok((self, true))
}
}

pub struct Session {
session: client::Handle<Client>,
}

impl Session {
async fn connect(
user: String,
password: Option<String>,
key: Option<String>,
key_password: Option<&str>,
addrs: String,
) -> anyhow::Result<Self> {
let config = client::Config {
..<_>::default()
hulto marked this conversation as resolved.
Show resolved Hide resolved
};
let config = Arc::new(config);
let sh = Client {};
let mut session = client::connect(config, addrs.clone(), sh).await?;

// Try key auth first
match key {
Some(local_key) => {
let key_pair = decode_secret_key(&local_key, key_password)?;
let _auth_res: bool = session
.authenticate_publickey(user, Arc::new(key_pair))
.await?;
return Ok(Self { session });
},
None => {},
}

// If key auth doesn't work try password auth
match password {
Some(local_pass) => {
let _auth_res: bool = session
.authenticate_password(user, local_pass)
.await?;
return Ok(Self { session });
},
None => {},
}
return Err(anyhow::anyhow!("Failed to authenticate to host {}@{}", user, addrs.clone()));
}

async fn call(&mut self, command: &str) -> anyhow::Result<CommandResult> {
let mut channel = self.session.channel_open_session().await?;
channel.exec(true, command).await?;
let mut output = Vec::new();
let mut code = None;
while let Some(msg) = channel.wait().await {
match msg {
russh::ChannelMsg::Data { ref data } => {
output.write_all(data).unwrap();
}
russh::ChannelMsg::ExitStatus { exit_status } => {
code = Some(exit_status);
}
_ => {
}
}
}
Ok(CommandResult { output, code })
}

async fn close(&mut self) -> anyhow::Result<()> {
self.session
.disconnect(Disconnect::ByApplication, "", "English")
.await?;
Ok(())
}
}

struct CommandResult {
output: Vec<u8>,
code: Option<u32>,
}

impl CommandResult {
fn output(&self) -> String {
String::from_utf8_lossy(&self.output).into()
}
}

Loading