Why pivoting is harder than it looks
The first time I tried to reach an internal service from a compromised host, I made it work, but I did not really understand why. I copied a command from a blog post, changed a few values, and hoped for the best. The tunnel worked, but I could not explain the difference between local forwarding, dynamic forwarding, and reverse tunneling. That lack of understanding caught up with me later when a tunnel broke and I had no idea how to fix it.
This post is my personal study note on internal port forwarding and proxy chains. I am writing it because pivoting is one of those topics that seems simple until the network does not behave the way the diagram says it should. The notes are organized around the question of which side can reach which network.
Start with a connectivity map
Before I choose a pivoting technique, I draw a connectivity map. The map shows my host, the compromised host, and the target service. It also shows which connections are possible. The key question is always: who can reach whom?
If my host can reach the compromised host, and the compromised host can reach the target service, I can use local forwarding. If the target service is only reachable through several hosts, I need to chain tunnels or use a SOCKS proxy. If the compromised host cannot reach me at all, I need a reverse tunnel that starts from the target.
The map also shows me the ports and protocols. Some services use TCP, some use UDP, and some use a combination. The tunnel must support the protocol that the service needs. I record the source address, the destination address, and the protocol for every connection.
SSH local port forwarding
SSH local forwarding is the first technique I try. The syntax maps a local port to a destination that is reachable from the SSH server. The command looks like this:
ssh -L 9000:10.0.0.5:3306 user@jump-host
The local port 9000 on my machine is connected to 10.0.0.5:3306 through the jump host. When I connect to 127.0.0.1:9000, the SSH client sends the traffic through the encrypted channel to the jump host, and the jump host forwards it to the database.
Local forwarding works well when I need to reach a small number of fixed services. It does not require any software on the target, and it leaves almost no trace on the target host. The main limitation is that I have to create a new forward for every service I want to reach.
The command can also bind to a specific interface. By default, the local port binds to the loopback interface, which is safer. If I need another host to use the tunnel, I can bind to a different address, but that increases the exposure.
Dynamic SOCKS tunnels
When I need to reach many hosts or many ports, a SOCKS tunnel is more flexible. The command looks like this:
ssh -D 1080 user@jump-host
The local port 1080 becomes a SOCKS proxy. I can point tools and browsers at the proxy, and they can reach any host that the jump host can reach. The proxy handles the routing, so I do not need to create a new forward for every service.
Tools like proxychains let me route individual commands through the SOCKS proxy. I add the proxy address to the configuration file, and then prefix commands with proxychains. This is useful for command-line tools that do not have built-in proxy support.
The dynamic tunnel is more flexible than local forwarding, but it can introduce DNS issues. Some tools resolve hostnames on the local machine instead of through the proxy. If the hostname only resolves inside the internal network, I need to make sure the DNS resolution happens on the jump host.
Reverse tunnels
Local and dynamic forwarding both require the compromised host to be reachable from my host. That is not always the case. A firewall may block incoming connections, or the compromised host may be behind NAT. In those situations, I need a reverse tunnel.
A reverse tunnel starts on the target and connects out to my relay server. The relay server accepts the connection and makes the target's network reachable. Tools like frp, nps, and chisel implement this pattern. The target initiates the connection, so it does not need to accept any incoming traffic.
The relay server is a piece of infrastructure that I control. It needs to be reachable from the target and from my testing machine. The relay can expose a local port that forwards into the target's network, or it can provide a SOCKS proxy.
Reverse tunnels are powerful, but they require uploading or installing a client on the target. The client leaves a footprint, and the relay server becomes a critical piece of infrastructure. I prefer SSH when it is available, because it uses an existing protocol and requires less tooling.
When to use a dedicated tool
Dedicated tunneling tools become necessary when SSH is blocked, when the target is on an unusual operating system, or when I need features such as UDP support, HTTP multiplexing, or authentication. Tools like frp and nps can tunnel multiple protocols over a single connection and are easier to configure for complex scenarios.
The downside is the footprint. A binary uploaded to the target is a piece of evidence. It may be detected by antivirus, endpoint detection, or a defender reviewing the filesystem. I try to use the target's existing tools whenever possible, and I only upload a dedicated client when the native options are not available.
Before uploading, I check the target architecture, the operating system, and the available libraries. A binary built for the wrong architecture will not run. I also check whether the target has network access to my relay server. If it does not, the reverse tunnel will not work.
Chaining tunnels
Some networks require more than one hop. A database may only be reachable from a host that is only reachable from another host. In that case, I chain tunnels. The chain can be built with multiple SSH commands or with a SOCKS proxy that routes through several hosts.
Chaining is fragile. Each hop adds latency and a new point of failure. I keep the chain as short as possible and document every hop. If a connection breaks, I can identify which hop failed by testing each link.
The commands for a chain can be complex. I write them down in my notes before I run them, because it is easy to mix up the ports and the destinations. I also use separate terminal sessions for each hop so that a broken connection does not affect the others.
Footprint and detection
Every tunnel leaves traces. The target host may have logs from the SSH client, the relay server may have connection logs, and the network may show the traffic pattern. I try to reduce the footprint by using encrypted channels, avoiding persistent listeners when possible, and removing temporary files at the end.
The relay server is a bigger footprint than a direct SSH connection. It is infrastructure that can be linked to me, and it may be monitored. I use throwaway relay servers for high-risk operations and rotate them if necessary.
The traffic pattern is also detectable. A tunnel that sends a lot of data at regular intervals looks different from normal traffic. I try to match the traffic pattern to the expected behavior of the network.
Stability and cleanup
Tunnels are not permanent. They can break when a connection drops, when the target reboots, or when the network changes. I use keepalive settings when the protocol supports them, and I monitor the tunnel during long operations.
At the end of the test, I remove every tunnel. I close the local ports, delete the reverse client, and remove any temporary configuration files. I also check the target for listeners that I created and for users that I added.
The cleanup should be documented. I keep a list of everything I created during the engagement, including the tunnel, the files, and the accounts. The list makes cleanup faster and helps the report explain what was done.
UDP and other protocols
Most of my tunnels handle TCP traffic, but some services use UDP. DNS, NTP, and some game services are examples. SSH local forwarding can handle TCP, but not all UDP traffic. A dedicated tunneling tool may be needed for UDP.
I check the protocol before I choose the tunnel. If the service uses UDP, I look for a tool that supports it. The tool must be configured for the specific port and the target address.
The protocol also affects the stability. UDP has no connection state, so a tunnel that relies on a connection may behave differently. I test the service through the tunnel before I rely on it.
The choice between TCP and UDP is part of the connectivity map. I write the protocol next to the destination in my notes.
DNS through the tunnel
The DNS resolution is often the first thing that breaks in a tunnel. A hostname that only resolves inside the internal network will not resolve on my machine. I need to make sure the DNS lookup happens through the tunnel.
The SOCKS proxy can handle the DNS lookup if the client supports it. Some tools resolve the hostname locally and send the IP address to the proxy. If the hostname is internal, the lookup fails.
I test the resolution by using an IP address instead of a hostname. If the IP works and the hostname does not, the problem is DNS. I add the internal hostname to a local hosts file or configure the proxy to resolve remotely.
The DNS configuration should be documented. A team member who uses the same tunnel needs to know how to resolve the internal names.
Verifying the tunnel
A tunnel that appears to work may fail when a real client connects. I verify the tunnel with the actual service before I use it. The verification can be a simple TCP connection, a service banner, or a request that returns expected data.
For a database, I connect with the database client and run a read-only query. For a web service, I request a known page. For a custom service, I send a handshake message.
The verification also confirms the routing. If the tunnel goes through multiple hops, I test each hop. A misconfigured hop can be identified by the response.
The verification result is part of my notes. It tells me that the tunnel is ready and gives me a baseline for later troubleshooting.
Common mistakes I make
My biggest mistake is choosing a technique before drawing the connectivity map. The map determines which technique can work. My second mistake is using a reverse tunnel when local forwarding would have been simpler. The extra complexity increases the risk of detection and failure.
The third mistake is forgetting to test the tunnel with a simple request before relying on it. A tunnel that is misconfigured may appear to work but fail when a real client connects. The fourth mistake is leaving the tunnel running after the test is complete.
My quick checklist
- Draw a connectivity map and note who can reach whom.
- Use SSH local forwarding for a small number of fixed services.
- Use a SOCKS tunnel for broad access to many hosts.
- Use a reverse tunnel when the target cannot accept incoming connections.
- Check the target architecture and network access before uploading tools.
- Keep the chain short and document every hop.
- Monitor the tunnel and keep it stable.
- Remove every tunnel, file, and account during cleanup.
What I would do next time
Next time I want to spend more time on the connectivity map before I run any command. The map is the difference between a tunnel that works and a tunnel that almost works. I also want to practice more with reverse tunnels in a lab, because they are the techniques I am least comfortable with under pressure. Pivoting is not about memorizing commands. It is about understanding the path from my host to the target service.