Skip to content
Open
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
441 changes: 435 additions & 6 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rustcat"
version = "3.0.0"
version = "3.1.0"
authors = ["robiot"]
description = "The Modern Port Listener and Reverse Shell"
license = "GPL-3.0-only"
Expand All @@ -18,6 +18,11 @@ rustyline = "14.0.0"
log = "0.4.22"
fern = { version = "0.6.2", features = ["colored"] }

# TLS/DTLS support
rustls = { version = "0.23", features = ["std"] }
webpki-roots = "0.26"
udp-dtls = "0.1"

[target.'cfg(unix)'.dependencies]
termios = "0.3"
signal-hook = "0.3.17"
32 changes: 32 additions & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ pub struct Opts {
// verbose: bool,
}

#[derive(Debug, Clone, Copy)]
pub enum Protocol {
Tcp,
Tls,
Udp,
Dtls,
}

#[derive(Subcommand, Debug)]
pub enum Command {
/// Start a listener for incoming connections
Expand Down Expand Up @@ -38,6 +46,18 @@ pub enum Command {
// Host:ip, IP if only 1 value provided
#[clap(num_args = ..=2)]
host: Vec<String>,

/// Protocol: tcp, tls, udp, dtls
#[clap(long, default_value = "tcp")]
protocol: String,

/// Path to certificate file (PEM)
#[clap(long)]
cert: Option<String>,

/// Path to private key file (PEM)
#[clap(long)]
key: Option<String>,
},

/// Connect to the controlling host
Expand All @@ -50,5 +70,17 @@ pub enum Command {
// Host:ip, IP if only 1 value provided
#[clap(num_args = ..=2)]
host: Vec<String>,

/// Protocol: tcp, tls, udp, dtls
#[clap(long, default_value = "tcp")]
protocol: String,

/// Path to certificate file (PEM)
#[clap(long)]
cert: Option<String>,

/// Path to private key file (PEM)
#[clap(long)]
key: Option<String>,
},
}
161 changes: 125 additions & 36 deletions src/listener/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod tls;
use colored::Colorize;
use rustyline::error::ReadlineError;
use std::io::{stdin, stdout, Read, Result, Write};
Expand All @@ -17,8 +18,12 @@ pub struct Opts {
pub exec: Option<String>,
pub block_signals: bool,
pub mode: Mode,
pub protocol: crate::input::Protocol,
pub cert: Option<String>,
pub key: Option<String>,
}

#[derive(Debug, Clone, Copy)]
pub enum Mode {
Normal,
Interactive,
Expand Down Expand Up @@ -108,51 +113,135 @@ fn block_signals(should_block: bool) -> Result<()> {
}
// Listen on given host and port
pub fn listen(opts: &Opts) -> rustyline::Result<()> {
let listener = TcpListener::bind(format!("{}:{}", opts.host, opts.port))?;
match opts.protocol {
crate::input::Protocol::Tcp => {
let listener = TcpListener::bind(format!("{}:{}", opts.host, opts.port))?;

#[cfg(not(unix))]
{
if let Mode::Interactive = opts.mode {
print_feature_not_supported();

exit(1);
}
}

log::info!("Listening on {}:{}", opts.host.green(), opts.port.cyan());

let (mut stream, _) = listener.accept()?;
#[cfg(not(unix))]
{
if let Mode::Interactive = opts.mode {
print_feature_not_supported();
exit(1);
}
}

match &opts.mode {
Mode::Interactive => {
// It exists it if isn't unix above
block_signals(opts.block_signals)?;
log::info!("Listening on {}:{}", opts.host.green(), opts.port.cyan());
let (mut stream, _) = listener.accept()?;

#[cfg(unix)]
{
termios_handler::setup_fd()?;
listen_tcp_normal(stream, opts)?;
match &opts.mode {
Mode::Interactive => {
block_signals(opts.block_signals)?;
#[cfg(unix)]
{
termios_handler::setup_fd()?;
listen_tcp_normal(stream, opts)?;
}
}
Mode::LocalInteractive => {
let t = pipe_thread(stream.try_clone()?, stdout());
print_connection_received();
readline_decorator(|command| {
stream
.write_all((command + "\n").as_bytes())
.expect("Failed to send TCP.");
})?;
t.join().unwrap();
}
Mode::Normal => {
block_signals(opts.block_signals)?;
listen_tcp_normal(stream, opts)?;
}
}
}
Mode::LocalInteractive => {
let t = pipe_thread(stream.try_clone()?, stdout());

crate::input::Protocol::Tls => {
use std::fs;
use std::sync::Arc;
use rustls::ServerConfig;
use rustls::pki_types::CertificateDer;
use crate::listener::tls::accept_tls;
let cert_path = opts.cert.as_ref().expect("TLS listener requires --cert");
let key_path = opts.key.as_ref().expect("TLS listener requires --key");
let cert_data = fs::read(cert_path).expect("Failed to read cert file");
let key_data = fs::read(key_path).expect("Failed to read key file");
Comment on lines +162 to +165
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion:

Establish a standard location for these files (analogous to of $HOME/.ssh/), preferably in a path like $XDG_CONFIG_DIR/rustcat1. The files .../server-cert.der2 and .../server-key.der, stored in that directory, could be loaded automatically if --protocol tls is used but no --cert and/or no --key argument (respectively) is given. To save the user having to type them every time.

(Another command line flag could disable automatic file loading even in the absence of --cert or --key, for the paranoid or in scripts. Something like --no-user-defaults, --ignore-user-keys, etc.)

Notes

  1. Where the XDG spec defines $XDG_CONFIG_DIR as either the contents of that envvar, or $HOME/.config/ if no such environment variable is set.
  2. (Or default-cert.der / default-key.der, you get the idea.)

Comment on lines +164 to +165
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second suggestion: Take a page from SSH et al, and check the permissions on any files being read, whether from a default location or one passed in via --cert / --key. Refuse to load encryption files if they're world-readable or (worse!) world-writable.

(SSH recommends, but doesn't enforce, that $HOME/.ssh/ have 0700 permissions, and will refuse to use any private key that's accessible by others, i.e. their private keyfile permissions mask is 0077.)

let certs = vec![CertificateDer::from(cert_data)];
use rustls::pki_types::PrivatePkcs8KeyDer;
let key = PrivatePkcs8KeyDer::from(key_data).into();
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.expect("bad cert/key");
let config = Arc::new(config);
let listener = TcpListener::bind(format!("{}:{}", opts.host, opts.port))?;
log::info!("Listening (TLS) on {}:{}", opts.host.green(), opts.port.cyan());
let (stream, _) = listener.accept()?;
let mut tls_stream = accept_tls(stream, config)?;
let (stdin_thread, stdout_thread) = (
pipe_thread(stdin(), tls_stream.get_mut().try_clone()?),
pipe_thread(tls_stream, stdout()),
);
print_connection_received();

readline_decorator(|command| {
stream
.write_all((command + "\n").as_bytes())
.expect("Failed to send TCP.");
})?;

t.join().unwrap();
stdin_thread.join().unwrap();
stdout_thread.join().unwrap();
}
Mode::Normal => {
block_signals(opts.block_signals)?;
listen_tcp_normal(stream, opts)?;
crate::input::Protocol::Udp => {
log::error!("UDP listener not implemented");
exit(1);
}
crate::input::Protocol::Dtls => {
use std::fs;
use udp_dtls::Identity;
use crate::listener::tls::accept_dtls;
let cert_path = opts.cert.as_ref().expect("DTLS listener requires --cert (PKCS12)");
let pkcs12_data = fs::read(cert_path).expect("Failed to read PKCS12 file");
let identity = Identity::from_pkcs12(&pkcs12_data, "").expect("Invalid PKCS12");
let socket = std::net::UdpSocket::bind(format!("{}:{}", opts.host, opts.port))?;
log::info!("Listening (DTLS) on {}:{}", opts.host.green(), opts.port.cyan());
use std::sync::{Arc, Mutex};
let dtls_stream = accept_dtls(socket, identity)?;
let stream = Arc::new(Mutex::new(dtls_stream));
let stream_writer = Arc::clone(&stream);
let stdin_thread = std::thread::spawn(move || {
let mut stdin = stdin();
let mut buf = [0u8; 4096];
loop {
let n = match stdin.read(&mut buf) {
Ok(0) => break,
Ok(n) => n,
Err(_) => break,
};
if let Ok(mut s) = stream_writer.lock() {
if s.write_all(&buf[..n]).is_err() {
break;
}
} else {
break;
}
}
});
let stream_reader = Arc::clone(&stream);
let stdout_thread = std::thread::spawn(move || {
let mut stdout = stdout();
let mut buf = [0u8; 4096];
loop {
let n = match stream_reader.lock() {
Ok(mut s) => match s.read(&mut buf) {
Ok(0) => break,
Ok(n) => n,
Err(_) => break,
},
Err(_) => break,
};
if stdout.write_all(&buf[..n]).is_err() {
break;
}
let _ = stdout.flush();
}
});
print_connection_received();
stdin_thread.join().unwrap();
stdout_thread.join().unwrap();
}
}

Ok(())
}

Expand Down
61 changes: 61 additions & 0 deletions src/listener/tls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use std::io::{Read, Write, Result};
use std::net::{TcpStream, UdpSocket, SocketAddr};
use std::sync::Arc;

use rustls::{ClientConfig, ServerConfig, StreamOwned, ServerConnection, ClientConnection};
use rustls::pki_types::ServerName;
use udp_dtls::{DtlsAcceptor, DtlsConnector, Identity, Certificate, DtlsStream};

// Minimal wrapper to adapt UdpSocket to Read/Write for udp-dtls
#[derive(Debug)]
pub struct UdpSocketChannel(pub UdpSocket);

impl Read for UdpSocketChannel {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.recv(buf)
}
}

impl Write for UdpSocketChannel {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.send(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

pub fn accept_tls(stream: TcpStream, config: Arc<ServerConfig>) -> Result<StreamOwned<ServerConnection, TcpStream>> {
let conn = ServerConnection::new(config).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(StreamOwned::new(conn, stream))
}

pub fn connect_tls(stream: TcpStream, config: Arc<ClientConfig>, server_name: &str) -> Result<StreamOwned<ClientConnection, TcpStream>> {
let server_name = ServerName::try_from(server_name.to_owned())
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid server name"))?;
let conn = ClientConnection::new(config, server_name)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(StreamOwned::new(conn, stream))
}

pub fn accept_dtls(socket: UdpSocket, identity: Identity) -> Result<DtlsStream<UdpSocketChannel>> {
let acceptor = DtlsAcceptor::builder(identity)
.build()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let channel = UdpSocketChannel(socket);
let stream = acceptor.accept(channel)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", e)))?;
Ok(stream)
}

pub fn connect_dtls(socket: UdpSocket, addr: SocketAddr, identity: Identity, peer_cert: Certificate) -> Result<DtlsStream<UdpSocketChannel>> {
let connector = DtlsConnector::builder()
.identity(identity)
.add_root_certificate(peer_cert)
.build()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
let channel = UdpSocketChannel(socket);
let stream = connector.connect(&addr.to_string(), channel)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", e)))?;
Ok(stream)
}
Loading