CLI tool
Ne0nd0g/merlin avatar
Ne0nd0g/merlin

Merlin: a Go post-exploitation C2 server where the build seed is in the Makefile

Merlin is a cross-platform post-exploitation HTTP/2 Command & Control server and agent written in golang.

5,619 stars835 forksGoGPL-3.0

At a glance

What is it?
The server half of the Merlin command and control framework. Five agent protocols, an OPAQUE authenticator, and one plaintext SEED that both halves must share.
Who is it for?
Merlin's server repository is the smaller half of a two-repository project, and most of what makes it interesting is visible in three files rather than in the documentation. The go.mod pins gRPC to a version below the one with a known advisory and says why in a comment.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 14 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • Ne0nd0g/merlin5,619 ★ · +9
Star history
Repository2026-10-082026-10-25
Ne0nd0g/merlin5.6k5.6k

9 Stars in the last 17 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

This repository is the server half, not the whole framework

The single most important thing to know about Ne0nd0g/merlin before reading anything else is that it is not the whole product. The README's supporting repositories list separates the pieces: Merlin Agent for the agent source, Merlin Agent DLL for the reflective-loading variant, merlin-cli for the command line interface, merlin-documentation for the docs, a MythicAgents integration, a merlin-docker base image, and merlin-message as the Go library defining the message format shared between server and agent.

What stays in this repository is the server. That is reflected in the tree, which is short: main.go at the root, a pkg directory, test, data, docs, plus the three files worth reading closely, go.mod, Makefile and Dockerfile. The 5611 stars and 836 forks attach to this repository, so when you see adoption numbers quoted for Merlin they are measuring the server component while the agent component lives elsewhere and is versioned separately.

The practical consequence is that a build here does not produce something you can deploy on a target. You get a server that operators connect to over gRPC, which then serves listeners that agents check in against. The README's quick start reflects that division: the server package ships prebuilt CLI and agent binaries in a data/bin directory, so the very first run does not require building the agent at all.

The description says HTTP/2, the feature list says five protocols

The repository description calls Merlin a cross-platform post-exploitation HTTP/2 Command and Control server and agent written in golang. The README's highlighted features list says this instead.

code
- Supported Agent C2 Protocols: http/1.1 clear-text, http/1.1 over TLS, HTTP/2, HTTP/2 clear-text (h2c), http/3 (http/2 over QUIC)

Both statements are correct and neither is wrong. The description is a one-line summary and HTTP/2 is the sensible thing to name in it because it is the middle of the range. The feature list is the specification and it covers clear-text HTTP/1.1, HTTP/1.1 over TLS, HTTP/2, clear-text HTTP/2, and HTTP/3 carried over QUIC. quic-go at v0.63.0 is a direct dependency in go.mod, which corroborates the last entry rather than leaving it as a claim.

If you are writing detection logic or a network policy, that distinction matters more than it first appears. Clear-text h2c and http/1.1 are what a listener looks like on a network that cannot see TLS, while the QUIC path changes both transport and encryption assumptions. Merlin also lists configurable agent data encoding and encryption transforms, AES, Base64, gob, hex, JWE, RC4 and XOR, with the JWE transform specified in enough detail to matter, using PBES2_HS512_A256KW with HMAC SHA-512 as the PRF and AES Key Wrap over 256-bit keys. Protocol choice and transform choice are independent knobs, so the traffic shape is a product of two settings rather than one.

The authenticator is a PAKE, not a shared secret

Most command and control frameworks that authenticate agents do it with a shared secret that both sides hold in plaintext. Merlin lists two configurable agent authenticators and one of them is deliberately not that.

code
- Configurable agent authenticators:
  - None: No authentication 
  - [OPAQUE](https://tools.ietf.org/html/draft-krawczyk-cfrg-opaque-00): Asymmetric Password Authenticated Key Exchange (PAKE)

OPAQUE is a password-authenticated key exchange, so the server never receives the agent's password in a form it could replay or reuse. gopaque v0.1.0 is in the require block, so this is wired rather than aspirational. Message authentication is separate from the authenticator: the feature list also names encrypted JWT for message authentication, which is the layer that stops an observer from tampering with or replaying a beacon.

The other two entries on the same list are worth noting because they are evasion features with an ordinary name. Merlin supports configurable padding on agent message data, which the README states is there to combat beaconing detections based on a fixed message size, and it can change the agent's JA3 hash dynamically. JA3 is a fingerprint derived from a TLS ClientHello, so a changing value defeats allowlists that pin it. The agent side of that story lives in the merlin-agent repository, and the README links the JA3 and padding documentation to the agent CLI reference rather than documenting either in this tree.

The build seed is committed in plaintext, on purpose

The Makefile contains a value that reads like a leaked credential and is not one. It is the garble seed, and the file says in a comment that the server and agent must be built with the same seed value.

code
# Misc
# The Merlin server and agent MUST be built with the same seed value
# Set during build with "make linux-garble SEED=<insert seed>
SEED=d0d03a0ae4722535a0e1d5d0c8385ce42015511e68d960fadef4b4eaf5942feb

Garble is the obfuscator, and -seed fixes the obfuscation output so a build is reproducible. The same requirement appears above each garble target, along with a note that garble 0.5.2 or later must be installed and on PATH. The linux-garble target shows the flags in full.

code
linux-garble:
	export GOOS=linux GOARCH=amd64 && garble -tiny -literals -seed ${SEED} build ${LDFLAGS} -o ${DIR}/merlinServer-Linux-x64 main.go

So the operational rule is straightforward. Build both halves with the same seed or your obfuscated server and obfuscated agent will disagree about the shared key material they derive, and the failure will look like a connectivity problem rather than a build problem. Two other details from the same file are easy to miss. The output path is versioned and commit-scoped, since DIR is built from a version scraped out of pkg/merlin.go and a BUILD value from git rev-parse HEAD. And the default target builds main.go without garble at all, so a plain make produces a readable binary while make linux-garble produces the obfuscated one. Which of the two you want depends entirely on what you are doing with it.

The dependency pins carry a security advisory in a comment

The go.mod requires Go 1.27.0 and pins gRPC below its current release, with the reason written inline.

code
	google.golang.org/grpc v1.83.2 // pinned <1.84: v1.84.0 regressed GO-2026-6443; next stable fix is v1.85.x

This is a good pattern and worth calling out because it is rare. A dependency pin with a bare version number tells a reader nothing about whether the pin is caution or neglect. A pin with an advisory ID, the specific bad version, and the version expected to fix it tells a reader everything, and it tells a future maintainer when the pin can be lifted. If you are reviewing this repository, that line is the one to check against the current gRPC release list.

The rest of the direct requirements are worth reading as a map of what the server actually does. go-jose v3.0.5 for JOSE and JWE, cretz/gopaque for the PAKE, quic-go for the HTTP/3 listener, go.dedis.ch/kyber for the cryptography, protobuf for the message serialization, google/uuid for identifiers, golang.org/x/sync for concurrency, and go-donut for in-memory shellcode loading. golangci.yml at the root and an AGENTS.md file suggest the project is set up for automated linting and agent-assisted contribution, which is a reasonable expectation for a tool of this shape.

The container binds gRPC to every interface

The Dockerfile is six effective lines and the last one is the one to notice.

code
WORKDIR /opt/merlin

ENTRYPOINT ["./merlinServer-Linux-x64", "-addr", "0.0.0.0:50051"]

The address is 0.0.0.0, not localhost, so anything that can reach the published port can attempt a gRPC connection. The Dockerfile's own comment block explains the intended port map, with 50051 as the gRPC port for the Merlin CLI to connect to and 443 as the port where a Merlin listener will bind, and it suggests adding further -p arguments to expose more listener ports.

code
#  > sudo docker run -p 50051:50051 -p 443:443 -v ~/merlin:/opt/merlin/data merlin-server:latest

The base image is ne0nd0g/merlin-base:v1.5.0 and the upstream Dockerfile lives in the merlin-docker repository, so this file is a thin wrapper rather than the real build recipe. The volume mount matters for persistence, since data is where the tree shows the compiled binaries and build output directories land. The port exposure is what deserves a decision: because the entrypoint is already bound to all interfaces, publishing 50051 to the host without a firewall in front of it puts the operator interface on the network, and that interface is the one that creates listeners and deploys agents.

Release artifacts, a known password, and an eighteen-month gap

The newest release is v2.1.4 from 2025-04-17, while the last push is dated 2026-10-04. Both are real and they describe different things, releases and source activity. The release notes for v2.1.4 are a dependency bump list, moving go-jose from v3.0.3 to v3.0.4, golang.org/x/net from v0.24.0 to v0.39.0, golang.org/x/sync from v0.7.0 to v0.13.0, quic-go from v0.42.0 to v0.50.1, and protobuf from v1.33.0 to v1.36.5, along with raising the minimum Go version to v1.23.

Compare that against the go.mod on the default branch, which requires Go 1.27.0 and sits several versions ahead on every one of those libraries, and the gap is clear. Commits after the last release include the work that would become the next version. If you consume Merlin through tagged release archives rather than building from main, you are running the April 2025 dependency set, not the current one.

Two details from the release notes deserve repeating because they are easy to trip over. The data/bin directory inside the server package contains compiled CLI and agent binaries, so the archive is not just the server. And the compressed files carry a password, which the notes state as merlin and the README's Quick Start repeats as 7zip's x function with the same password. A password published in the release notes is not protecting anything, which is fine for a lab tool and worth stating plainly if you are evaluating it for anything else.

What is absent from this repository

The documentation lives elsewhere. The README links Merlin Documentation for the wiki and points at a readthedocs site, and its Misc section defers the custom build page and the full command list to that site rather than reproducing them here. So the authoritative answers to how do I configure a listener, what modules exist, and what do the CLI menus contain are not answerable from this tree alone.

The same applies to the agent. Every capability on the highlighted features list that runs on a target, the .NET assembly execution with invoke-assembly and execute-assembly, execute-pe for Windows binaries in a sacrificial process, the shellcode techniques CreateThread, CreateRemoteThread, RtlCreateUserThread and QueueUserAPC, and the Donut, sRDI and SharpGen integrations, belongs to the agent and its DLL repositories. From this side you see the listener configuration, the gRPC operator interface, and the message format.

Two smaller gaps are worth naming. The tree includes a test directory but the go.mod carries go.uber.org/mock as an indirect dependency rather than a direct one, so the testing story is not visible from the module file. And the topic list is six entries, agent, c2, command-and-control, golang, http2 and post-exploitation, where http2 again reflects the description rather than the five protocols in the feature list. For a project this size those are minor, but if you are searching for Merlin by topic rather than by name, the http3 and quic listeners are harder to find than they should be.

Editorial conclusion

Merlin's server repository is the smaller half of a two-repository project, and most of what makes it interesting is visible in three files rather than in the documentation. The go.mod pins gRPC to a version below the one with a known advisory and says why in a comment. The Makefile commits a garble SEED in plaintext and states that the server and the agent have to be compiled with the same one. The Dockerfile binds the gRPC port to all interfaces. None of that is hidden, and none of it is a defect by itself, but a reader deciding how to run this is better off knowing it before the first build than after the first mismatch. On naming, the repository description says HTTP/2 while the README lists five agent protocols including HTTP/3 over QUIC, and both statements are accurate at their own level of specificity, so treat the description as the summary and the feature list as the specification.

Frequently asked questions

What is the difference between Merlin and other command and control frameworks?

Merlin is a Go post-exploitation framework split across several repositories, with this one holding the server. Its distinguishing configuration surface is the listener protocol list, which covers http/1.1 clear-text, http/1.1 over TLS, HTTP/2, h2c and http/3 over QUIC, plus agent authenticators that include OPAQUE, a password-authenticated key exchange, rather than only shared secrets.

Why must the Merlin server and agent be built with the same seed?

The seed fixes garble's obfuscation output so the build is reproducible. Because the server and agent derive shared key material that has to line up, the Makefile commits a default SEED value and repeats the requirement in a comment above every garble target. Building one half with a different seed produces artifacts that look fine and fail to communicate, which reads like a network problem rather than a build problem.

What ports does the Merlin server expose and why does the Docker image bind to all interfaces?

The Dockerfile sets its entrypoint to the server with an address of 0.0.0.0:50051, which is the gRPC port the Merlin CLI connects to. Its comments identify port 443 as where a Merlin listener binds and suggest extra -p arguments for additional listener ports. Because the entrypoint is already on all interfaces, publishing 50051 without a firewall in front of it exposes the operator interface directly.

How do you extract the Merlin release archives?

The release archives are 7zip files. The README's Quick Start says to extract with the x function and states the password is merlin, and the v2.1.4 release notes repeat that the compressed files have a password of merlin. The same package also contains compiled CLI and agent binaries under a data/bin directory.

Official sources

  1. Issues
  2. License: GPL-3.0
  3. Ne0nd0g/merlin on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ne0nd0g-merlin.svg)](https://hysenlabs.com/projects/ne0nd0g-merlin)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/ne0nd0g-merlin.svg)](https://hysenlabs.com/projects/ne0nd0g-merlin)

Was this useful?