diff --git a/README.md b/README.md index e0b22ed..42709e6 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,19 @@ email: # Email host address host_addr: smtp.gmail.com:587 +# Ghostwriter Profile +ghostwriter: + # Ghostwriter graphql endpoint + graphql_endpoint: 'https://ghostwriter/v1/graphql' + # Ghostwriter API key + api_key: 'deadbeef' + # Oplog ID + oplog_id: 1 + # (Optional) Disable email, username, and credentials from being sent to ghostwriter + disable_credentials: true + # Ignore SSL Self Signed error + ignore_self_signed_certificate: true + # You can also supply an email template for each notification email_submitted_credentials_template: | Someone submitted credentials! diff --git a/config.go b/config.go index 16b422d..ea7eb9c 100644 --- a/config.go +++ b/config.go @@ -22,6 +22,14 @@ Email Address - {{ .Email }} IP Address - {{ .Address }} User Agent - {{ .UserAgent }}` +var defaultgraphqlTemplate = `mutation InsertGophishLog ($oplog: bigint!, $sourceIp: String, $tool: String, $userContext: String, $description: String, $output: String, $comments: String, $extraFields: jsonb!) { + insert_oplogEntry(objects: {oplog: $oplog, sourceIp: $sourceIp, tool: $tool, userContext: $userContext, description: $description, comments: $comments, output: $output, extraFields: $extraFields}) { + returning { + id + } + } + }` + func init() { viper.SetConfigName("config") viper.SetConfigType("yaml") @@ -48,6 +56,7 @@ func setDefaults() { viper.SetDefault("email_send_click_template", defaultClickedTemplate) viper.SetDefault("email_submitted_credentials_template", defaultSubmittedCredentailsTemplate) viper.SetDefault("email_default_email_open_template", defaultEmailOpenedTemplate) + viper.SetDefault("graphql_default_query", defaultgraphqlTemplate) viper.SetDefault("profiles", []string{"slack"}) } @@ -87,6 +96,12 @@ func validateConfig() { viper.GetString("email.recipient")) continue } + if profile == "ghostwriter" { + ghostwriterConfigs := []string{"ghostwriter.graphql_endpoint", "ghostwriter.api_key", "ghostwriter.oplog_id"} + checkKeysExist(ghostwriterConfigs...) + log.Infof("Using Ghostwriter sending profile. Will send messages to %s", viper.GetString("ghostwriter.graphql_endpoint")) + continue + } log.Fatalf("Profile \"%s\" does not exist", profile) } } diff --git a/go.mod b/go.mod index 1b25e90..20348cf 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4 // indirect + github.com/machinebox/graphql v0.2.2 // indirect github.com/parnurzeal/gorequest v0.2.16 // indirect github.com/sirupsen/logrus v1.8.1 github.com/spf13/viper v1.8.1 diff --git a/go.sum b/go.sum index ec7632c..4ab2bca 100644 --- a/go.sum +++ b/go.sum @@ -180,6 +180,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo= +github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= diff --git a/main.go b/main.go index 67f4216..3cd49a6 100644 --- a/main.go +++ b/main.go @@ -83,6 +83,12 @@ func handler(w http.ResponseWriter, r *http.Request) { return } } + if profile == "ghostwriter" { + if err := sender.SendGraphql(); err != nil { + log.Error(err) + return + } + } } w.WriteHeader(http.StatusNoContent) diff --git a/messages.go b/messages.go index b400709..66518cd 100644 --- a/messages.go +++ b/messages.go @@ -4,6 +4,7 @@ import ( "encoding/json" "html/template" "net/url" + "strconv" "strings" "github.com/ashwanthkumar/slack-go-webhook" @@ -21,6 +22,7 @@ var ( type Sender interface { SendSlack() error SendEmail() error + SendGraphql() error } func senderDispatch(status string, webhookResponse WebhookResponse, response []byte) (Sender, error) { @@ -130,6 +132,21 @@ func (w SubmittedDetails) SendEmail() error { return sendEmail("PhishBot - Credentials Submitted", body) } +func (w SubmittedDetails) SendGraphql() error { + var output string + if !viper.GetBool("ghostwriter.disable_credentials") { + output = "\nUsername: " + w.Username + "\nPassword: " + w.Password + } + oplog_entry := ghostwriterOplogEntry{ + SourceIp: w.Address, + UserContext: w.Email, + Description: "User ID: " + w.ID + "\nCampaign ID: " + strconv.FormatUint(uint64(w.CampaignID), 10), + Output: output, + Comments: SubmittedData, + } + return sendGraphql(oplog_entry) +} + type ClickDetails struct { CampaignID uint ID string @@ -175,6 +192,17 @@ func (w ClickDetails) SendEmail() error { return sendEmail("PhishBot - Email Clicked", body) } +func (w ClickDetails) SendGraphql() error { + oplog_entry := ghostwriterOplogEntry{ + SourceIp: w.Address, + UserContext: w.Email, + Description: "User ID: " + w.ID + "\nCampaign ID: " + strconv.FormatUint(uint64(w.CampaignID), 10), + Output: "UserAgent: " + w.UserAgent, + Comments: ClickedLink, + } + return sendGraphql(oplog_entry) +} + func getEmailBody(templateValue string, obj interface{}) (string, error) { out := new(strings.Builder) tpl, err := template.New("email").Parse(templateValue) @@ -231,3 +259,14 @@ func (w OpenedDetails) SendEmail() error { } return sendEmail("PhishBot - Email Opened", body) } + +func (w OpenedDetails) SendGraphql() error { + oplog_entry := ghostwriterOplogEntry{ + SourceIp: w.Address, + UserContext: w.Email, + Description: "User ID: " + w.ID + "\nCampaign ID: " + strconv.FormatUint(uint64(w.CampaignID), 10), + Output: "UserAgent: " + w.UserAgent, + Comments: EmailOpened, + } + return sendGraphql(oplog_entry) +} diff --git a/sending_helpers.go b/sending_helpers.go index 103ba28..b7a93bd 100644 --- a/sending_helpers.go +++ b/sending_helpers.go @@ -1,13 +1,34 @@ package main import ( + "context" + "crypto/tls" "fmt" + "net/http" "net/smtp" "github.com/ashwanthkumar/slack-go-webhook" + "github.com/machinebox/graphql" "github.com/spf13/viper" ) +type ghostwriterOplogEntry struct { + Oplog int + StartDate string + EndDate string + SourceIp string + DestIp string + Tool string + UserContext string + Command string + Description string + Output string + Comments string + OperatorName string + EntryIdentifier string + ExtraFields string +} + func sendSlackAttachment(attachment slack.Attachment) error { payload := slack.Payload{ Username: viper.GetString("slack.bot_username"), @@ -36,3 +57,41 @@ func sendEmail(subject, body string) error { } return nil } + +func sendGraphql(data ghostwriterOplogEntry) error { + url := viper.GetString("ghostwriter.graphql_endpoint") + apiKey := viper.GetString("ghostwriter.api_key") + ignoreSelfSigned := viper.GetBool("ghostwriter.ignore_self_signed_certificate") + query := viper.GetString("graphql_default_query") + oplogId := viper.GetInt("ghostwriter.oplog_id") + + var client *graphql.Client + if ignoreSelfSigned { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + httpClient := &http.Client{Transport: tr} + client = graphql.NewClient(url, graphql.WithHTTPClient(httpClient)) + } else { + client = graphql.NewClient(url) + } + + req := graphql.NewRequest(query) + req.Header.Set("Authorization", "Bearer "+apiKey) + req.Var("oplog", oplogId) + req.Var("sourceIp", data.SourceIp) + req.Var("tool", "gophish") + req.Var("userContext", data.UserContext) + req.Var("description", data.Description) + req.Var("output", data.Output) + req.Var("comments", data.Comments) + req.Var("extraFields", "") + + ctx := context.Background() + var respData map[string]interface{} + if err := client.Run(ctx, req, &respData); err != nil { + return err + } + + return nil +}