Nmap Through SSH Pivot: Why Proxychains and sshuttle Fail


If you’ve tried pivoting nmap through an SSH jump host, you’ve probably hit one of two outcomes: every port comes back as filtered, or every port comes back as open. Both are wrong and in this post I will show you how I got to a satisifying result. (Spoiler: Using ligolo-ng)

Scenario: You’ve got an attack host, a pivot you can SSH into, and a target on an internal subnet that’s only reachable from the pivot:

Attack Host (10.10.14.12)  -->  Pivot (10.129.229.129)  -->  Target (172.16.5.35)

The target lives on 172.16.5.0/24. You want to scan it from your attack host and get real results.

Failed approach 1: SSH dynamic forward + proxychains

The textbook answer (or rather the one I learned in HTB Academy) Open a SOCKS proxy with SSH, point proxychains at it, run nmap.

> ssh -D 9050 -i ~/.ssh/id_rsa user@pivot-host

> proxychains nmap -sT -Pn -n 172.16.5.35

nmap result:

PORT     STATE    SERVICE
22/tcp   filtered ssh
135/tcp  filtered msrpc
445/tcp  filtered microsoft-ds
3389/tcp filtered ms-wbt-server

Checking with netcat I figured, the tunnel is fine, but the combination of the SOCKS proxy and Nmap is the problem.

Why does nmap show all ports as filtered through proxychains?

Three things compound. First, proxychains forces nmap’s non-blocking sockets into blocking mode. Nmap’s whole scan engine is built around firing off connect() in non-blocking mode and using select()/poll() to track dozens of probes in flight. Proxychains intercepts those calls and makes them blocking, so probes that should return instantly hang instead. Nmap’s reads the hang as “no response” and labels the port filtered.

I have tried -n, --max-parallelism 1, --max-rtt-timeout 5000ms, bumping tcp_read_time_out to 30000, or swapping socks4 for socks5… none of that fixes it. The socket-level mismatch between nmap and proxychains is just how those two tools interact.

What really made me wonder: Even Nmap’s own --proxies socks4://127.0.0.1:9050 flag has the same problem. Apparent the scan engine was never designed to run through a SOCKS proxy.

Failed approach 2: sshuttle

sshuttle uses iptables rules to redirect outbound TCP into a local Python process that forwards over SSH.

> sudo sshuttle -r user@pivot-host 172.16.5.0/24 --ssh-cmd "ssh -i ~/.ssh/id_rsa"

> nmap -sT -Pn -n 172.16.5.35

What you get: all 1000 default ports open. The first time I saw that I thought I’d found a goldmine :’D

Why does nmap show all ports as open through sshuttle?

sshuttle’s iptables REDIRECT rule funnels every outbound TCP connection into its local listener — and that listener accepts the connection immediately, before it has any idea whether the real target is reachable. From nmap’s perspective the three-way handshake completes on every port, because it’s completing against the local Python process, not the target. Nmap sees a successful connect() and dutifully reports “open.”

Approach 3: just run nmap on the pivot

You can skip the proxy problem entirely:

ssh -i ~/.ssh/id_rsa user@pivot-host "nmap -sT -Pn 172.16.5.35"

Accurate, fast, done — if nmap is on the pivot. On real engagements and most CTF labs, it isn’t, and uploading a static binary adds operational noise you might not want. Worth knowing about, though.

Approach 4: ligolo-ng – the fix

ligolo-ng creates a real tun interface on your attack host and tunnels raw IP packets through a small agent running on the pivot.

That’s the key detail. Your kernel sees the internal subnet as routable through a normal network interface. No SOCKS. No iptables redirect, everything works the way it does on a directly connected network, because as far as the OS is concerned, it is one. You also need to upload a static binary to the pivot, though. (So you might as well go with nmap on pivot, I prefer the ligolo-ng approach still)

Setup

Download proxy and agent binary from ligolo-ng release page: https://github.com/nicocha30/ligolo-ng/releases

On your attack host, build the tun interface and start the proxy:

sudo ip tuntap add user $(whoami) mode tun ligolo
sudo ip link set ligolo up
sudo ip route add 172.16.5.0/24 dev ligolo
sudo ./proxy -selfcert -laddr 0.0.0.0:11601

Push the agent to the pivot and run it:

scp -i ~/.ssh/id_rsa ./agent user@pivot-host:/tmp/agent
ssh -i ~/.ssh/id_rsa user@pivot-host "/tmp/agent -connect YOUR_ATTACK_IP:11601 -ignore-cert"

(There’s a Windows agent too, if your pivot is a compromised Windows host. Same idea, just agent.exe.)

In the ligolo proxy console (on your attack host):

ligolo-ng >> session
ligolo-ng >> start

Now you can run nmap like the pivot isn’t even there:

nmap 172.16.5.35

Results:

PORT     STATE SERVICE
22/tcp   open  ssh
135/tcp  open  msrpc
139/tcp  open  netbios-ssn
445/tcp  open  microsoft-ds
3389/tcp open  ms-wbt-server

Nmap done: 1 IP address (1 host up) scanned in 1.83 seconds

Five open, 995 correctly closed, 1.83 seconds. Compare that to the multi-minute proxychains scan that was faulty.

How the tools differ

ToolHow it tunnelsWhat breaks
proxychainsIntercepts connect(), proxies through SOCKSForces non-blocking sockets to blocking; nmap’s timing engine reads delays as “filtered”
sshuttleiptables REDIRECT to a local Python process over SSHLocal listener accepts every connection before forwarding, so nmap sees every port as “open”
ligolo-ngKernel tun interface, raw IP packetsNothing. There’s no proxy layer to break.

What about Chisel?

Chisel came up a lot in my research on this issue and it’s a legitimate option. It tunnels TCP over HTTP and can do SOCKS or reverse port forwards. The catch: if you use its SOCKS mode you’re back to proxychains territory and the same nmap problem. If you use reverse port forwards you have to set them up per-port, which doesn’t scale to a nmap scan, but might be relevant on other task during your pentest.

A note on SYN scans

If you’ve been wondering why nmap -sS fails through every SSH-based tunnel: SYN scans need raw sockets, which no userland proxy passes through. proxychains, sshuttle, Chisel SOCKS none of them can carry a half-open SYN. ligolo-ng can, because the tun interface lets nmap craft and send actual IP packets. UDP scans (-sU) work for the same reason. If you need anything beyond a connect scan over a pivot, ligolo-ng right now seems to me like the only option.

To never miss an article subscribe to my newsletter
No ads. One click unsubscribe.