Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
 
242 implement pivotssh exec function (#243)

* Added russh dep.

* Add ssh client support.

* Implement ssh exec and test.

* Improved docs and errors.
  • Loading branch information
hulto authored Jul 22, 2023
1 parent effcc74 commit 4c7d9cf
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 9 deletions.
16 changes: 13 additions & 3 deletions docs/_docs/user-guide/eldritch.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,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 @@ -334,7 +344,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 @@ -32,6 +32,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,6 +8,7 @@ allocative = { workspace = true }
allocative_derive = { workspace = true }
anyhow = { workspace = true }
async-recursion = { workspace = true }
async-trait = { workspace = true }
chrono = { workspace = true }
derive_more = { workspace = true }
eval = { workspace = true }
Expand All @@ -17,6 +18,8 @@ network-interface = { 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()
};
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

0 comments on commit 4c7d9cf

Please sign in to comment.