From 9d15ca617a036730512ebc51ad5892d4a50d8c51 Mon Sep 17 00:00:00 2001 From: Jack McKenna Date: Wed, 6 Apr 2022 14:22:41 -0400 Subject: [PATCH 1/4] Initial poc working with test server. Looking into building server side contact. --- docs/Sandcat-Details.md | 18 +++ gocat/contact/websocket_rev_contact.go | 209 +++++++++++++++++++++++++ gocat/go.mod | 1 + gocat/go.sum | 2 + 4 files changed, 230 insertions(+) create mode 100644 gocat/contact/websocket_rev_contact.go diff --git a/docs/Sandcat-Details.md b/docs/Sandcat-Details.md index 1d2a6bca..cd0cfa21 100644 --- a/docs/Sandcat-Details.md +++ b/docs/Sandcat-Details.md @@ -63,6 +63,7 @@ When running the Sandcat agent binary, there are optional parameters you can use Additionally, the sandcat agent can tunnel its communications to the C2 using the following options (for more details, see the [C2 tunneling documentation](../../C2-Tunneling.md) + ## Extensions In order to keep the agent code lightweight, the default Sandcat agent binary ships with limited basic functionality. Users can dynamically compile additional features, referred to as "gocat extensions". @@ -121,6 +122,23 @@ Additional functionality can be found in the following agent extensions: **Other Extensions** - `shared` extension provides the C sharing functionality for Sandcat. This can be used to compile Sandcat as a DLL rather than a `.exe` for Windows targets. +### Building sandcat locally for development +Build sandcat without extensions: +```cd ./gocat; go build``` +Build sandcat with extensions: +```cd ./gocat; cp ../gocat-extensions//.go .//; go build``` + +### Building a new extension +#### Contacts +1. For testing your contact will need to be in the core code base to get compiled in. + - Create a copy of `api.go` in the `sandcat/gocat/contact/` folder. + - Name your copy something descriptive of your contact. `Eg. websocket_rev_contact` +2. Add any static configuration to the vars section. ie. If you have a specific endpoint that won't change often. +3. Add any dynamic variables that may change during the course of execution or at run time like upstream address to the struct for your contact. +4. Update the init names for your contact struct. Eg. API -> Websocket +5. Update the standard functions `GetBeaconBytes`, `GetPayloadBytes`, `C2RequirementsMet`, `SetUpstreamDestAddr`, `SendExecutionResults`, `GetName`, and `UploadFileBytes` to be implementations of your contacts struct and to perform the functions how you want. + + ## Customizing Default Options & Execution Without CLI Options It is possible to customize the default values of these options when pulling Sandcat from the CALDERA server. diff --git a/gocat/contact/websocket_rev_contact.go b/gocat/contact/websocket_rev_contact.go new file mode 100644 index 00000000..583a4c0e --- /dev/null +++ b/gocat/contact/websocket_rev_contact.go @@ -0,0 +1,209 @@ +package contact + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + + "github.com/gorilla/websocket" + + "github.com/mitre/gocat/output" +) + +var ( + websocket_url = "/ws_interactive" + websocket_proto = "ws" + ws_userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36" +) + +//API communicates through HTTP +type Websocket struct { + name string + client *http.Client + upstreamDestAddr string + ws_client *websocket.Conn +} + +func init() { + CommunicationChannels["Websocket"] = &Websocket{name: "Websocket"} +} + +//GetInstructions sends a beacon and returns response. +func (a *Websocket) GetBeaconBytes(profile map[string]interface{}) []byte { + output.VerbosePrint("[*] Getting commands") + data, err := json.Marshal(profile) + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Cannot request beacon. Error with profile marshal: %s", err.Error())) + return nil + } else { + // address := fmt.Sprintf("%s%s", a.upstreamDestAddr, apiBeacon) + return a.request(data) + } +} + +// Return the file bytes for the requested payload. +func (a *Websocket) GetPayloadBytes(profile map[string]interface{}, payload string) ([]byte, string) { + var payloadBytes []byte + var filename string + platform := profile["platform"] + if platform != nil { + address := fmt.Sprintf("%s/file/download", a.upstreamDestAddr) + req, err := http.NewRequest("POST", address, nil) + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Failed to create HTTP request: %s", err.Error())) + return nil, "" + } + req.Header.Set("file", payload) + req.Header.Set("platform", platform.(string)) + req.Header.Set("paw", profile["paw"].(string)) + resp, err := a.client.Do(req) + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Error sending payload request: %s", err.Error())) + return nil, "" + } + defer resp.Body.Close() + if resp.StatusCode == ok { + buf, err := io.ReadAll(resp.Body) + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Error reading HTTP response: %s", err.Error())) + return nil, "" + } + payloadBytes = buf + if name_header, ok := resp.Header["Filename"]; ok { + filename = filepath.Join(name_header[0]) + } else { + output.VerbosePrint("[-] HTTP response missing Filename header.") + } + } + } + return payloadBytes, filename +} + +//C2RequirementsMet determines if sandcat can use the selected comm channel +func (a *Websocket) C2RequirementsMet(profile map[string]interface{}, c2Config map[string]string) (bool, map[string]string) { + upstreamurl, err := url.Parse(a.upstreamDestAddr) + if err != nil { + output.VerbosePrint(fmt.Sprintf("Invalid URL: %v", err)) + return false, nil + } + a.SetUpstreamDestAddr(fmt.Sprintf("%s://%s/%s", websocket_proto, upstreamurl.Host, websocket_url)) + // a.SetUpstreamDestAddr("ws://localhost:7012/ws_interactive") + + output.VerbosePrint(fmt.Sprintf("Interactive endpoint=%s", a.upstreamDestAddr)) + + // Gorilla handles the HTTP upgrade to websocket so we don't need that client anymore. + // Using a unique name ws_client to avoid name confliction with api.go + c, _, err := websocket.DefaultDialer.Dial(a.upstreamDestAddr, nil) + if err != nil { + output.VerbosePrint(fmt.Sprintf("dial: %v", err)) + return false, nil + } + a.ws_client = c + return true, nil +} + +func (a *Websocket) SetUpstreamDestAddr(upstreamDestAddr string) { + upstreamDestAddr = "ws://localhost:7012/ws_interactive" + a.upstreamDestAddr = upstreamDestAddr +} + +// SendExecutionResults will send the execution results to the upstream destination. +func (a *Websocket) SendExecutionResults(profile map[string]interface{}, result map[string]interface{}) { + output.VerbosePrint("[*] Sending results") + _ = fmt.Sprintf("%s%s", a.upstreamDestAddr, apiBeacon) + profileCopy := make(map[string]interface{}) + for k, v := range profile { + profileCopy[k] = v + } + results := make([]map[string]interface{}, 1) + results[0] = result + profileCopy["results"] = results + data, err := json.Marshal(profileCopy) + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Cannot send results. Error with profile marshal: %s", err.Error())) + } else { + a.request(data) + } +} + +func (a *Websocket) GetName() string { + return a.name +} + +func (a *Websocket) UploadFileBytes(profile map[string]interface{}, uploadName string, data []byte) error { + uploadUrl := a.upstreamDestAddr + "/file/upload" + + // Set up the form + requestBody := bytes.Buffer{} + contentType, err := createUploadForm(&requestBody, data, uploadName) + if err != nil { + return nil + } + + // Set up the request + headers := map[string]string{ + "Content-Type": contentType, + "X-Request-Id": fmt.Sprintf("%s-%s", profile["host"].(string), profile["paw"].(string)), + "User-Agent": userAgent, + "X-Paw": profile["paw"].(string), + "X-Host": profile["host"].(string), + } + req, err := createUploadRequest(uploadUrl, &requestBody, headers) + if err != nil { + return err + } + + // Perform request and process response + resp, err := a.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + return nil + } else { + return errors.New(fmt.Sprintf("Non-successful HTTP response status code: %d", resp.StatusCode)) + } + return nil +} + +func (a *Websocket) request(data []byte) []byte { + output.VerbosePrint(string(data)) + encodedData := []byte(base64.StdEncoding.EncodeToString(data)) + output.VerbosePrint("[*] Making request") + + err := a.ws_client.WriteMessage(websocket.TextMessage, encodedData) + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Cannot send websocket message: %s", err.Error())) + return nil + } + _, message, err := a.ws_client.ReadMessage() + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Cannot recieve websocket message: %s", err.Error())) + return nil + } + + decodedData, err := base64.StdEncoding.DecodeString(string(message)) + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Cannot decode websocket message: %s", err.Error())) + return nil + } + output.VerbosePrint(fmt.Sprintf("[*] Decoded message:\n %s", decodedData)) + var jsonData interface{} + err = json.Unmarshal(decodedData, &jsonData) + if err != nil { + output.VerbosePrint(fmt.Sprintf("[-] Cannot unmarshal json data: %s", err.Error())) + return nil + } + // if val, ok := jsonData["sleep"]; ok { + // jsonData["sleep"] = float64(0) + // } + + return decodedData +} diff --git a/gocat/go.mod b/gocat/go.mod index b434492d..c5b1b1cb 100644 --- a/gocat/go.mod +++ b/gocat/go.mod @@ -19,6 +19,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/golang/protobuf v1.4.2 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/websocket v1.5.0 github.com/jmespath/go-jmespath v0.4.0 // indirect golang.org/x/mod v0.4.2 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect diff --git a/gocat/go.sum b/gocat/go.sum index cd673003..1dac037a 100644 --- a/gocat/go.sum +++ b/gocat/go.sum @@ -106,6 +106,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE= github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= From 495df70333226fc55ebf180bf29102fcc287e417 Mon Sep 17 00:00:00 2001 From: Jack McKenna Date: Thu, 5 May 2022 11:42:43 -0400 Subject: [PATCH 2/4] Removed unimplemented code. --- gocat/contact/websocket_rev_contact.go | 76 ++------------------------ 1 file changed, 4 insertions(+), 72 deletions(-) diff --git a/gocat/contact/websocket_rev_contact.go b/gocat/contact/websocket_rev_contact.go index 583a4c0e..ffe6e91c 100644 --- a/gocat/contact/websocket_rev_contact.go +++ b/gocat/contact/websocket_rev_contact.go @@ -1,15 +1,11 @@ package contact import ( - "bytes" "encoding/base64" "encoding/json" - "errors" "fmt" - "io" "net/http" "net/url" - "path/filepath" "github.com/gorilla/websocket" @@ -49,40 +45,8 @@ func (a *Websocket) GetBeaconBytes(profile map[string]interface{}) []byte { // Return the file bytes for the requested payload. func (a *Websocket) GetPayloadBytes(profile map[string]interface{}, payload string) ([]byte, string) { - var payloadBytes []byte - var filename string - platform := profile["platform"] - if platform != nil { - address := fmt.Sprintf("%s/file/download", a.upstreamDestAddr) - req, err := http.NewRequest("POST", address, nil) - if err != nil { - output.VerbosePrint(fmt.Sprintf("[-] Failed to create HTTP request: %s", err.Error())) - return nil, "" - } - req.Header.Set("file", payload) - req.Header.Set("platform", platform.(string)) - req.Header.Set("paw", profile["paw"].(string)) - resp, err := a.client.Do(req) - if err != nil { - output.VerbosePrint(fmt.Sprintf("[-] Error sending payload request: %s", err.Error())) - return nil, "" - } - defer resp.Body.Close() - if resp.StatusCode == ok { - buf, err := io.ReadAll(resp.Body) - if err != nil { - output.VerbosePrint(fmt.Sprintf("[-] Error reading HTTP response: %s", err.Error())) - return nil, "" - } - payloadBytes = buf - if name_header, ok := resp.Header["Filename"]; ok { - filename = filepath.Join(name_header[0]) - } else { - output.VerbosePrint("[-] HTTP response missing Filename header.") - } - } - } - return payloadBytes, filename + // Not implemented due to interactive nature. + return nil, "" } //C2RequirementsMet determines if sandcat can use the selected comm channel @@ -109,7 +73,7 @@ func (a *Websocket) C2RequirementsMet(profile map[string]interface{}, c2Config m } func (a *Websocket) SetUpstreamDestAddr(upstreamDestAddr string) { - upstreamDestAddr = "ws://localhost:7012/ws_interactive" + upstreamDestAddr = "ws://localhost:7013/ws_interactive" a.upstreamDestAddr = upstreamDestAddr } @@ -137,39 +101,7 @@ func (a *Websocket) GetName() string { } func (a *Websocket) UploadFileBytes(profile map[string]interface{}, uploadName string, data []byte) error { - uploadUrl := a.upstreamDestAddr + "/file/upload" - - // Set up the form - requestBody := bytes.Buffer{} - contentType, err := createUploadForm(&requestBody, data, uploadName) - if err != nil { - return nil - } - - // Set up the request - headers := map[string]string{ - "Content-Type": contentType, - "X-Request-Id": fmt.Sprintf("%s-%s", profile["host"].(string), profile["paw"].(string)), - "User-Agent": userAgent, - "X-Paw": profile["paw"].(string), - "X-Host": profile["host"].(string), - } - req, err := createUploadRequest(uploadUrl, &requestBody, headers) - if err != nil { - return err - } - - // Perform request and process response - resp, err := a.client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - if resp.StatusCode == http.StatusOK { - return nil - } else { - return errors.New(fmt.Sprintf("Non-successful HTTP response status code: %d", resp.StatusCode)) - } + // Not implemented due to interactive nature. return nil } From 6a2ef2be5932e3ba28e023180c5e431d3c0f0e8f Mon Sep 17 00:00:00 2001 From: Jack McKenna Date: Thu, 5 May 2022 11:48:03 -0400 Subject: [PATCH 3/4] Moved websocket_rev to gocat extensions. --- {gocat => gocat-extensions}/contact/websocket_rev_contact.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {gocat => gocat-extensions}/contact/websocket_rev_contact.go (100%) diff --git a/gocat/contact/websocket_rev_contact.go b/gocat-extensions/contact/websocket_rev_contact.go similarity index 100% rename from gocat/contact/websocket_rev_contact.go rename to gocat-extensions/contact/websocket_rev_contact.go From d620fab04180bf838f1729134e24226bdfbc0e41 Mon Sep 17 00:00:00 2001 From: Jack McKenna Date: Thu, 5 May 2022 11:51:33 -0400 Subject: [PATCH 4/4] Added support continuous function. --- gocat-extensions/contact/websocket_rev_contact.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gocat-extensions/contact/websocket_rev_contact.go b/gocat-extensions/contact/websocket_rev_contact.go index ffe6e91c..4775df39 100644 --- a/gocat-extensions/contact/websocket_rev_contact.go +++ b/gocat-extensions/contact/websocket_rev_contact.go @@ -30,6 +30,10 @@ func init() { CommunicationChannels["Websocket"] = &Websocket{name: "Websocket"} } +func (f *Websocket) SupportsContinuous() bool { + return false +} + //GetInstructions sends a beacon and returns response. func (a *Websocket) GetBeaconBytes(profile map[string]interface{}) []byte { output.VerbosePrint("[*] Getting commands")