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");
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