From a73f99428b895696c84a2286a6834b9c2f0069da Mon Sep 17 00:00:00 2001 From: Hulto <7121375+hulto@users.noreply.github.com> Date: Fri, 19 Apr 2024 01:02:49 +0000 Subject: [PATCH 1/6] Seems to work --- implants/lib/eldritch/src/pivot/mod.rs | 4 +- implants/lib/eldritch/src/pivot/ncat_impl.rs | 52 ++++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/implants/lib/eldritch/src/pivot/mod.rs b/implants/lib/eldritch/src/pivot/mod.rs index 6cf790ff5..a2a8c9d70 100644 --- a/implants/lib/eldritch/src/pivot/mod.rs +++ b/implants/lib/eldritch/src/pivot/mod.rs @@ -85,8 +85,8 @@ fn methods(builder: &mut MethodsBuilder) { } #[allow(unused_variables)] - fn ncat(this: &PivotLibrary, address: String, port: i32, data: String, protocol: String) -> anyhow::Result { - ncat_impl::ncat(address, port, data, protocol) + fn ncat(this: &PivotLibrary, address: String, port: i32, data: String, protocol: String, timeout: Option) -> anyhow::Result { + ncat_impl::ncat(address, port, data, protocol, timeout) } #[allow(unused_variables)] diff --git a/implants/lib/eldritch/src/pivot/ncat_impl.rs b/implants/lib/eldritch/src/pivot/ncat_impl.rs index 0e7129836..0ccc431d7 100644 --- a/implants/lib/eldritch/src/pivot/ncat_impl.rs +++ b/implants/lib/eldritch/src/pivot/ncat_impl.rs @@ -4,8 +4,12 @@ use anyhow::Result; use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::net::{TcpStream, UdpSocket}; -// Since we cannot go from async (test) -> sync (ncat) `block_on` -> async (handle_ncat) without getting an error "cannot create runtime in current runtime since current thread is calling async code." -async fn handle_ncat(address: String, port: i32, data: String, protocol: String) -> Result { +async fn handle_ncat_timeout( + address: String, + port: i32, + data: String, + protocol: String, +) -> Result { // If the response is longer than 4096 bytes it will be truncated. let mut response_buffer: Vec = Vec::new(); let result_string: String; @@ -56,14 +60,52 @@ async fn handle_ncat(address: String, port: i32, data: String, protocol: String) } } +// Since we cannot go from async (test) -> sync (ncat) `block_on` -> async (handle_ncat) without getting an error "cannot create runtime in current runtime since current thread is calling async code." +async fn handle_ncat( + address: String, + port: i32, + data: String, + protocol: String, + timeout: u32, +) -> Result { + let res = match tokio::time::timeout( + std::time::Duration::from_secs(timeout as u64), + handle_ncat_timeout(address, port, data, protocol), + ) + .await? + { + Ok(local_res) => local_res, + Err(local_err) => { + return Err(anyhow::anyhow!( + "Failed to run handle_ncat_timeout: {}", + local_err.to_string() + )) + } + }; + + Ok(res) +} + // We do not want to make this async since it would require we make all of the starlark bindings async. // Instead we have a handle_ncat function that we call with block_on -pub fn ncat(address: String, port: i32, data: String, protocol: String) -> Result { +pub fn ncat( + address: String, + port: i32, + data: String, + protocol: String, + timeout: Option, +) -> Result { + let default_timeout = 2; + let timeout_u32 = match timeout { + Some(res) => res, + None => default_timeout, + }; + let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build()?; - let response = runtime.block_on(handle_ncat(address, port, data, protocol)); + let response = runtime.block_on(handle_ncat(address, port, data, protocol, timeout_u32)); match response { Ok(_) => Ok(response.unwrap()), @@ -163,6 +205,7 @@ mod tests { test_port, expected_response.clone(), String::from("tcp"), + 2, )); // Will this create a race condition where the sender sends before the listener starts? @@ -191,6 +234,7 @@ mod tests { test_port, expected_response.clone(), String::from("udp"), + 2, )); // Will this create a race condition where the sender sends before the listener starts? From 7f2f3c2c2991e6b57095414e504cfc142e8e35fa Mon Sep 17 00:00:00 2001 From: Hulto <7121375+hulto@users.noreply.github.com> Date: Fri, 19 Apr 2024 01:14:01 +0000 Subject: [PATCH 2/6] Adds test --- implants/lib/eldritch/src/pivot/ncat_impl.rs | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/implants/lib/eldritch/src/pivot/ncat_impl.rs b/implants/lib/eldritch/src/pivot/ncat_impl.rs index 0ccc431d7..97204a2af 100644 --- a/implants/lib/eldritch/src/pivot/ncat_impl.rs +++ b/implants/lib/eldritch/src/pivot/ncat_impl.rs @@ -245,6 +245,31 @@ mod tests { assert_eq!(expected_response, actual_response.unwrap().unwrap()); Ok(()) } + #[tokio::test] + async fn test_ncat_timeout_exceeded() -> anyhow::Result<()> { + let test_port = allocate_localhost_unused_ports(1, "udp".to_string()).await?[0]; + + // Setup a test echo server + let expected_response = String::from("Hello world!"); + + // Setup a sender + let send_task = task::spawn(handle_ncat( + String::from("127.0.0.1"), + test_port, + expected_response.clone(), + String::from("udp"), + 2, + )) + .await?; + + assert!(send_task.is_err()); + assert!(send_task + .unwrap_err() + .to_string() + .contains("deadline has elapsed")); + + Ok(()) + } // #[test] // fn test_ncat_not_handle() -> anyhow::Result<()> { // let runtime = tokio::runtime::Builder::new_current_thread() From 150f5a329ccd4b31bf01a5d0d291b1ef67dbfe0e Mon Sep 17 00:00:00 2001 From: Hulto <7121375+hulto@users.noreply.github.com> Date: Fri, 19 Apr 2024 01:16:05 +0000 Subject: [PATCH 3/6] Update docs. --- docs/_docs/user-guide/eldritch.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/_docs/user-guide/eldritch.md b/docs/_docs/user-guide/eldritch.md index b30d4a9ca..6aabd5cff 100644 --- a/docs/_docs/user-guide/eldritch.md +++ b/docs/_docs/user-guide/eldritch.md @@ -176,7 +176,7 @@ Key must be 16 Bytes (Characters) ### crypto.encode_b64 -`crypto.encode_b64(content: str, encode_type: Optional) -> str` +`crypto.encode_b64(content: str, encode_type: Option) -> str` The crypto.encode_b64 method encodes the given text using the given base64 encoding method. Valid methods include: @@ -187,7 +187,7 @@ The crypto.encode_b64 method encodes the given text using the given base6 ### crypto.decode_b64 -`crypto.decode_b64(content: str, decode_type: Optional) -> str` +`crypto.decode_b64(content: str, decode_type: Option) -> str` The crypto.decode_b64 method encodes the given text using the given base64 decoding method. Valid methods include: @@ -480,10 +480,12 @@ The pivot.bind_proxy method is being proposed to provide users another op ### pivot.ncat -`pivot.ncat(address: str, port: int, data: str, protocol: str ) -> str` +`pivot.ncat(address: str, port: int, data: str, protocol: str, timeout: Option) -> str` The pivot.ncat method allows a user to send arbitrary data over TCP/UDP to a host. If the server responds that response will be returned. +Timeout is the number of seconds to wait for the ncat connection to succeed before closing it. Defaults to 2 seconds if not specified. + `protocol` must be `tcp`, or `udp` anything else will return an error `Protocol not supported please use: udp or tcp.`. ### pivot.port_forward @@ -531,7 +533,7 @@ NOTE: Windows scans against `localhost`/`127.0.0.1` can behave unexpectedly or e ### pivot.reverse_shell_pty -`pivot.reverse_shell_pty(cmd: Optional) -> None` +`pivot.reverse_shell_pty(cmd: Option) -> None` The **pivot.reverse_shell_pty** method spawns the provided command in a cross-platform PTY and opens a reverse shell over the agent's current transport (e.g. gRPC). If no command is provided, Windows will use `cmd.exe`. On other platforms, `/bin/bash` is used as a default, but if it does not exist then `/bin/sh` is used. @@ -543,7 +545,7 @@ The pivot.smb_exec method is being proposed to allow users a way to move ### pivot.ssh_copy -`pivot.ssh_copy(target: str, port: int, src: str, dst: str, username: str, password: Optional, key: Optional, key_password: Optional, timeout: Optional) -> None` +`pivot.ssh_copy(target: str, port: int, src: str, dst: str, username: str, password: Option, key: Option, key_password: Option, timeout: Option) -> None` The pivot.ssh_copy method copies a local file to a remote system. If no password or key is specified the function will error out with: `Failed to run handle_ssh_exec: Failed to authenticate to host` @@ -554,7 +556,7 @@ The file directory the `dst` file exists in must exist in order for ssh_copy to ### pivot.ssh_exec -`pivot.ssh_exec(target: str, port: int, command: str, username: str, password: Optional, key: Optional, key_password: Optional, timeout: Optional) -> List` +`pivot.ssh_exec(target: str, port: int, command: str, username: str, password: Option, key: Option, key_password: Option, timeout: Option) -> List` The pivot.ssh_exec 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` @@ -580,7 +582,7 @@ The pivot.ssh_password_spray method is being proposed to allow users a wa ### process.info -`process.info(pid: Optional) -> Dict` +`process.info(pid: Option) -> Dict` The process.info method returns all information on a given process ID. Default is the current process. @@ -762,7 +764,7 @@ If your dll_bytes array contains a value greater than u8::MAX it will cause the ### sys.exec -`sys.exec(path: str, args: List, disown: Optional) -> Dict` +`sys.exec(path: str, args: List, disown: Option) -> Dict` The sys.exec 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. From d6acaf74758f7d7fe194097053001c7744034e24 Mon Sep 17 00:00:00 2001 From: Hulto <7121375+hulto@users.noreply.github.com> Date: Fri, 19 Apr 2024 23:29:35 +0000 Subject: [PATCH 4/6] Increase timeout --- implants/lib/eldritch/src/pivot/ncat_impl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/implants/lib/eldritch/src/pivot/ncat_impl.rs b/implants/lib/eldritch/src/pivot/ncat_impl.rs index 97204a2af..b35824100 100644 --- a/implants/lib/eldritch/src/pivot/ncat_impl.rs +++ b/implants/lib/eldritch/src/pivot/ncat_impl.rs @@ -205,7 +205,7 @@ mod tests { test_port, expected_response.clone(), String::from("tcp"), - 2, + 5, )); // Will this create a race condition where the sender sends before the listener starts? @@ -234,7 +234,7 @@ mod tests { test_port, expected_response.clone(), String::from("udp"), - 2, + 5, )); // Will this create a race condition where the sender sends before the listener starts? From a3466006fca5953055c75aeab50c24f79df10b6d Mon Sep 17 00:00:00 2001 From: Hulto <7121375+hulto@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:01:29 -0500 Subject: [PATCH 5/6] Lets try this (#841) --- implants/lib/eldritch/src/pivot/ncat_impl.rs | 36 +++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/implants/lib/eldritch/src/pivot/ncat_impl.rs b/implants/lib/eldritch/src/pivot/ncat_impl.rs index 37d3a0968..0ae2edfb6 100644 --- a/implants/lib/eldritch/src/pivot/ncat_impl.rs +++ b/implants/lib/eldritch/src/pivot/ncat_impl.rs @@ -1,31 +1,30 @@ -use std::net::Ipv4Addr; +use std::{io::{BufReader, Read, Write}, net::{Ipv4Addr, SocketAddr, TcpStream, UdpSocket}, time::Duration}; use anyhow::Result; -use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; -use tokio::net::{TcpStream, UdpSocket}; async fn handle_ncat_timeout( address: String, port: i32, data: String, protocol: String, + duration: Duration, ) -> Result { // If the response is longer than 4096 bytes it will be truncated. let mut response_buffer: Vec = Vec::new(); let result_string: String; - let address_and_port = format!("{}:{}", address, port); + let address_and_port = format!("{}:{}", address, port).parse::()?; if protocol == "tcp" { // Connect to remote host - let mut connection = TcpStream::connect(&address_and_port).await?; + let mut connection = TcpStream::connect_timeout(&address_and_port, duration)?; // Write our meessage - connection.write_all(data.as_bytes()).await?; + connection.write_all(data.as_bytes())?; // Read server response let mut read_stream = BufReader::new(connection); - read_stream.read_buf(&mut response_buffer).await?; + read_stream.read_to_end(&mut response_buffer)?; // We need to take a buffer of bytes, turn it into a String but that string has null bytes. // To remove the null bytes we're using trim_matches. @@ -40,16 +39,17 @@ async fn handle_ncat_timeout( // Setting the bind address to unspecified should leave it up to the OS to decide. // https://stackoverflow.com/a/67084977 - let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).await?; + let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?; + sock.set_read_timeout(Some(duration))?; + sock.set_write_timeout(Some(duration))?; // Send bytes to remote host let _bytes_sent = sock - .send_to(data.as_bytes(), address_and_port.clone()) - .await?; + .send_to(data.as_bytes(), address_and_port)?; // Recieve any response from remote host let mut response_buffer = [0; 1024]; - let (_bytes_copied, _addr) = sock.recv_from(&mut response_buffer).await?; + let (_bytes_copied, _addr) = sock.recv_from(&mut response_buffer)?; // We need to take a buffer of bytes, turn it into a String but that string has null bytes. // To remove the null bytes we're using trim_matches. @@ -71,9 +71,10 @@ async fn handle_ncat( protocol: String, timeout: u32, ) -> Result { + let duration = std::time::Duration::from_secs(timeout as u64); let res = match tokio::time::timeout( - std::time::Duration::from_secs(timeout as u64), - handle_ncat_timeout(address, port, data, protocol), + duration, + handle_ncat_timeout(address, port, data, protocol, duration), ) .await? { @@ -118,6 +119,7 @@ pub fn ncat( #[cfg(test)] mod tests { + use super::*; use anyhow::Context; use tokio::io::copy; @@ -266,10 +268,12 @@ mod tests { .await?; assert!(send_task.is_err()); - assert!(send_task + let err_string = send_task .unwrap_err() - .to_string() - .contains("deadline has elapsed")); + .to_string(); + println!("{}", err_string); + assert!(err_string.contains("deadline has elapsed") || + err_string.contains("not properly respond after a period of time")); Ok(()) } From 247668fe6511301e0d909ea2030cd6bfea507007 Mon Sep 17 00:00:00 2001 From: hulto <7121375+hulto@users.noreply.github.com> Date: Sun, 26 Jan 2025 22:12:11 +0000 Subject: [PATCH 6/6] Formatting --- implants/lib/eldritch/src/pivot/ncat_impl.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/implants/lib/eldritch/src/pivot/ncat_impl.rs b/implants/lib/eldritch/src/pivot/ncat_impl.rs index 0ae2edfb6..de0fe6ff3 100644 --- a/implants/lib/eldritch/src/pivot/ncat_impl.rs +++ b/implants/lib/eldritch/src/pivot/ncat_impl.rs @@ -1,4 +1,8 @@ -use std::{io::{BufReader, Read, Write}, net::{Ipv4Addr, SocketAddr, TcpStream, UdpSocket}, time::Duration}; +use std::{ + io::{BufReader, Read, Write}, + net::{Ipv4Addr, SocketAddr, TcpStream, UdpSocket}, + time::Duration, +}; use anyhow::Result; @@ -44,8 +48,7 @@ async fn handle_ncat_timeout( sock.set_write_timeout(Some(duration))?; // Send bytes to remote host - let _bytes_sent = sock - .send_to(data.as_bytes(), address_and_port)?; + let _bytes_sent = sock.send_to(data.as_bytes(), address_and_port)?; // Recieve any response from remote host let mut response_buffer = [0; 1024]; @@ -268,12 +271,12 @@ mod tests { .await?; assert!(send_task.is_err()); - let err_string = send_task - .unwrap_err() - .to_string(); + let err_string = send_task.unwrap_err().to_string(); println!("{}", err_string); - assert!(err_string.contains("deadline has elapsed") || - err_string.contains("not properly respond after a period of time")); + assert!( + err_string.contains("deadline has elapsed") + || err_string.contains("not properly respond after a period of time") + ); Ok(()) }