Why this guide?
SSH tunneling lets you move traffic through an encrypted channel to reach services you otherwise couldn’t. Whether you’re a pentester, sysadmin, SRE, or developer, you’ll eventually meet a database that only binds to 127.0.0.1
on a remote host, or you’ll need the remote machine to reach a service running on your machine, or you’ll want a general-purpose proxy to explore a network without re-writing tool flags every time.
This post is half tutorial, half lab manual. You’ll build three small labs – Local, Remote, and Dynamic – and you’ll finish with a practical, detailed walk-through of SOCKS5 + ProxyChains, including configuration options and chaining “anonymous” proxies responsibly.
Audience & prerequisites
- Comfortable with Linux CLI on two VMs (your “attacker” – e.g., Kali – and a target Ubuntu Server).
- SSH access to the target (password or key).
- Root/sudo on your machine; non-privileged access is fine on the target (that’s the point of tunneling).
- Basic networking (TCP, ports) and a database client (we’ll use
psql
locally in Lab 1).
Lab topology
- Attacker (Kali) – where you initiate tunnels and run clients.
- Target (Ubuntu) – runs services, some bound to
127.0.0.1
only (not exposed to the network). - Optional jump host – for multi-hop/ProxyJump demos (not required for core labs).
Tip: confirm what’s listening on the target with
ss
. Common flags:-l
(listening),-t
(TCP),-n
(no DNS lookups).So,
ss -tln
lists listening TCP sockets numerically.
Mental model: L–R–D (Local, Remote, Dynamic)
- Local port-forward
-L LPORT:HOST:TPORT
Open a listener on your box; traffic goes through SSH to a host/port reachable from the target.
Use it when: a service is only reachable on the target’s127.0.0.1
(e.g., Postgres on 5432).
- Remote port-forward
-R RPORT:HOST:LPORT
Open a listener on the target; traffic goes back through SSH to a host/port reachable from you.
Use it when: the target must reach a service or handler running on your machine (or another internal box).
- Dynamic port-forward
-D LPORT
(SOCKS5)
Start a SOCKS5 proxy on your box; tools choose destinations on the fly.
Use it when: you want a general-purpose, “pick any host:port” tunnel – great with ProxyChains.
Lab 1: Local Port Forward (-L
): Reaching a local-only database on the target
Goal: Database on the target listens on 127.0.0.1:5432
. You can’t install psql
there (no sudo), so install the client locally and forward TCP through SSH.
- Verify the target’s service is local-only (on the target):
ss -tln | grep 5432
# expect: 127.0.0.1:5432 listening (not 0.0.0.0)
- Create a local tunnel on Kali (listener on Kali → target’s Postgres):
ssh -L 1234:127.0.0.1:5432 user@<target-ip>
# Add -fN to background the tunnel without an interactive shell:
# ssh -fN -L 1234:127.0.0.1:5432 user@<target-ip>
- Why
-fN
?-f
backgrounds after authentication;-N
tells SSH not to run a remote command (tunnel-only).
- Confirm the listener on Kali:
ss -tlpn | grep 1234
# should show ssh listening on 127.0.0.1:1234 on YOUR machine
- Seeing the new local socket is expected: a local port (1234) now forwards into the target’s 5432.
- Install the psql client on Kali and connect through the tunnel:
sudo apt update && sudo apt install -y postgresql-client
psql -U <db_user> -h 127.0.0.1 -p 1234
- You install the client locally (Kali), then point
-h 127.0.0.1 -p 1234
at your tunnel.
- Enumerate databases & tables (inside
psql
):
\l -- list databases
\c secrets -- connect to the "secrets" DB
\dt -- list tables
SELECT * FROM flag;
- These are standard
psql
introspection commands used in the scenario.
Real-world context: in the exercise this was driven by credentials discovered via password spraying (user
/ password
) and a local-only Postgres. The pivot with -L
is the clean way to interact without installing tools on the target.
Why choose -L
? When the remote service isn’t exposed to the network (bound to 127.0.0.1
), -L
lets you “bring it home” so your local tools can talk to it securely.
Lab 2: Remote Port Forward (-R
) – Let the target reach a service on your machine
Goal: The target needs to reach something on your machine (or a handler). Classic uses: serving payloads to a host that can’t egress directly, or exposing your local web UI for a remote collaborator.
- Start a service on Kali:
python3 -m http.server 8000
- Create a remote tunnel (on Kali):
ssh -R 9000:127.0.0.1:8000 user@<target-ip>
- This opens a listener on the target (127.0.0.1:9000 by default), which forwards back through SSH to your
127.0.0.1:8000
.
- From the target, verify:
curl http://127.0.0.1:9000
Variations:
- Make it listen on all interfaces on the target:
ssh -R 0.0.0.0:9000:127.0.0.1:8000 user@<target-ip>
- Hop through a bastion:
ssh -J bastion user@<target-ip> -R 9000:127.0.0.1:8000
Why choose -R
? When the remote needs to consume a service that exists local to you (or on your LAN), or you need the remote side to pull a connection back through your SSH link.
Lab 3: Dynamic Port Forward (-D
) – SOCKS5 pivot + ProxyChains
Goal: Create a local SOCKS5 proxy that routes arbitrary TCP through the SSH session. Perfect when you’ll probe multiple ports/hosts without re-creating tunnels each time.
- Start a SOCKS5 proxy on Kali:
ssh -fN -D 1080 user@<target-ip>
- This sets one local port (1080) and dynamically assigns the remote destination per connection.
- Configure ProxyChains (Kali):
Create~/.proxychains.conf
(or edit/etc/proxychains4.conf
) with:
# one of these chain policies (pick ONE)
strict_chain # use proxies in the order listed (good for predictable paths)
#dynamic_chain # skip dead proxies, keep order
#random_chain # pick random proxies each connection
proxy_dns # send DNS over the proxy (avoids DNS leaks)
[ProxyList]
# Examples of “anonymous” or privacy-first endpoints:
# socks5 127.0.0.1 9050 # Tor (if running locally)
socks5 127.0.0.1 1080 # our SSH -D SOCKS proxy
- For this scenario, ensure
strict_chain
is enabled and put the SOCKS entry under[ProxyList]
.
- Use any TCP tool through the tunnel:
proxychains psql -h 127.0.0.1 -p 5432 -U <db_user>
proxychains curl http://127.0.0.1:8080
proxychains nmap -sT -p 22,80,5432 127.0.0.1
- ProxyChains will be verbose about which proxy a connection used – normal and helpful for troubleshooting.
Why choose -D
? You want a general, reusable path. Instead of rebuilding -L
for each new port or host, you keep one SOCKS listener and let each tool decide the destination at runtime.
Choosing Local vs. Remote vs. Dynamic (real-world scenarios)
- Local
-L
- Reach “localhost-only” services on the target (DBs, admin panels, metrics endpoints).
- Safely use your mature toolchain (psql, pg_dump, Burp, browser) without installing on the target.
- Great when you have creds but not sudo (exactly the Postgres scenario here).
- Remote
-R
- Expose your local web server, webhook receiver, or reverse shell handler to the target.
- Collaborate: let the remote host view a local Grafana or notebook via
target:9000
.
- Dynamic
-D
- General pivoting over SOCKS for many tools/ports without re-tunneling.
- Layers nicely with additional proxies (e.g., Tor, commercial SOCKS) for privacy, testing geo-routes, or simulating client connectivity.
Deep dive: SOCKS5 + ProxyChains
What is SOCKS5 here?
A simple, TCP-level proxy protocol. When you run ssh -D 1080 ...
, SSH creates a local SOCKS5 server. Any SOCKS-aware client can connect to 127.0.0.1:1080
and request “please connect me to host X on port Y,” and SSH carries that stream through your encrypted session to the remote end, then onward.
ProxyChains config—common options you’ll actually use
(Examples shown in a per-user config at ~/.proxychains.conf
though /etc/proxychains4.conf
works too.)
# --- Chain policy (choose one) ---
strict_chain # Use proxies strictly in the order listed.
#dynamic_chain # Skip dead proxies but keep the order of the rest.
#random_chain # Pick proxies randomly for each connection.
# --- DNS handling ---
proxy_dns # Send DNS over the first proxy to avoid DNS leaks.
# --- Timeouts (milliseconds) ---
tcp_connect_time_out 5000
tcp_read_time_out 15000
# --- Noise level ---
#quiet_mode # Silence ProxyChains’ connect banners.
# --- Local networks that should bypass proxies (optional) ---
#localnet 127.0.0.0/255.0.0.0
#localnet 10.0.0.0/255.0.0.0
#localnet 192.168.0.0/255.255.0.0
[ProxyList]
# Stack proxies in order for strict_chain; first is used first.
# Some responsible “anonymous” options you might use:
# 1) Tor (if the Tor service runs locally)
# socks5 127.0.0.1 9050
# 2) Your SSH -D dynamic tunnel (this lab)
socks5 127.0.0.1 1080
# 3) A commercial SOCKS provider endpoint (if you subscribe)
# socks5 <provider-host> <provider-port> username password
Notes on “anonymous proxies”:
- Tor is easy and widely used for privacy testing; remember many sites block Tor exit nodes.
- Commercial SOCKS (paid) can offer stable egress IPs and geos; treat providers like you would a VPN—read privacy policies, test for DNS leaks, and avoid violating ToS/laws.
- Random “free proxy lists” are generally a bad idea: they’re unstable, untrustworthy, and may MITM you. Use only for harmless, throwaway experiments—never for credentials.
How chaining works: With strict_chain
, ProxyChains will try proxies top-to-bottom. With dynamic_chain
, it skips dead ones but preserves order. With random_chain
, it picks randomly per connection. In this lab, we keep it simple: enable strict_chain and put your SSH -D SOCKS at the bottom of the file under [ProxyList]
.
When to use ProxyChains vs. direct SOCKS support:
- Use ProxyChains with tools that don’t natively support SOCKS (e.g.,
psql
, somenmap
modes—prefer-sT
instead of SYN scans when proxied). - Use native SOCKS (
ALL_PROXY=socks5://127.0.0.1:1080
) when tools support it; it’s often cleaner.
Troubleshooting checklist
- Tunnel not listening where you think it is?
Runss -tlpn | grep <port>
on the machine where you expect the listener (Kali for-L
and-D
; target for-R
).
psql: command not found
on the target?
That’s expected; installpsql
locally and talk through the tunnel.
Connection refused
to the forwarded port?
The tunnel didn’t come up, or you ran-L
on the wrong side. Re-run with-vvv
to see SSH’s tunnel logs.
- ProxyChains spew too verbose?
It’s normal – useful to see which proxy succeeded. You can enablequiet_mode
if you prefer silence.
Quick reference (copy/paste)
Local (-L
) – reach target’s local-only service:
ssh -fN -L 1234:127.0.0.1:5432 user@<target>
psql -h 127.0.0.1 -p 1234 -U <dbuser>
- (Use
-fN
to background and avoid a remote shell.)
Remote (-R
) – expose your local service to target:
python3 -m http.server 8000
ssh -R 9000:127.0.0.1:8000 user@<target>
# then on target: curl http://127.0.0.1:9000
Dynamic (-D
) – SOCKS5 for general pivoting:
ssh -fN -D 1080 user@<target>
# ~/.proxychains.conf
strict_chain
proxy_dns
[ProxyList]
socks5 127.0.0.1 1080
# then:
proxychains curl http://127.0.0.1:8080
- (One local port; destinations chosen per connection.)
Final thoughts
-L
is your go-to when a service hides behindlocalhost
on the target.-R
is your bridge when the target must pull from your machine.-D
turns SSH into a flexible SOCKS proxy – pair it with ProxyChains for a powerful, tool-agnostic pivot.
If you want, I can wrap this into a printable PDF or add simple ASCII diagrams throughout – and we can expand the dynamic lab to include a Tor hop for a controlled “anonymous proxy” chain.