From ebb6cd61b202d3893eee58ad2d65235042260ce8 Mon Sep 17 00:00:00 2001 From: Jordan Wright Date: Fri, 8 Jun 2018 21:20:52 -0500 Subject: [PATCH] Implemented the ability to preview landing pages when sending a test email. --- controllers/api.go | 52 ++++++++-- controllers/api_test.go | 4 +- controllers/phish.go | 93 +++++++++++------- controllers/phish_test.go | 41 +++++++- ...180527213648_0.7.0_store_email_request.sql | 21 ++++ ...180527213648_0.7.0_store_email_request.sql | 21 ++++ models/campaign.go | 22 ++++- models/email_request.go | 92 ++++++++++++----- models/email_request_test.go | 98 ++++++++++++++----- models/group.go | 35 +++++-- models/group_test.go | 52 ++++++---- models/maillog.go | 57 ++--------- models/models_test.go | 4 +- models/result.go | 48 ++++----- models/result_test.go | 16 +-- models/template.go | 25 ++--- models/template_context.go | 74 ++++++++++++++ util/util.go | 10 +- util/util_test.go | 8 +- worker/worker.go | 2 +- worker/worker_test.go | 4 +- 21 files changed, 535 insertions(+), 244 deletions(-) create mode 100644 db/db_mysql/migrations/20180527213648_0.7.0_store_email_request.sql create mode 100644 db/db_sqlite3/migrations/20180527213648_0.7.0_store_email_request.sql create mode 100644 models/template_context.go diff --git a/controllers/api.go b/controllers/api.go index ca99f369..1bac5f12 100644 --- a/controllers/api.go +++ b/controllers/api.go @@ -652,8 +652,9 @@ func API_Import_Site(w http.ResponseWriter, r *http.Request) { // API_Send_Test_Email sends a test email using the template name // and Target given. func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) { - s := &models.SendTestEmailRequest{ + s := &models.EmailRequest{ ErrorChan: make(chan error), + UserId: ctx.Get(r, "user_id").(int64), } if r.Method != "POST" { JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest) @@ -664,11 +665,8 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) { JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest) return } - // Validate the given request - if err = s.Validate(); err != nil { - JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) - return - } + + storeRequest := false // If a Template is not specified use a default if s.Template.Name == "" { @@ -678,17 +676,15 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) { "{{if .FirstName}} First Name: {{.FirstName}}\n{{end}}" + "{{if .LastName}} Last Name: {{.LastName}}\n{{end}}" + "{{if .Position}} Position: {{.Position}}\n{{end}}" + - "{{if .TrackingURL}} Tracking URL: {{.TrackingURL}}\n{{end}}" + "\nNow go send some phish!" t := models.Template{ Subject: "Default Email from Gophish", Text: text, } s.Template = t - // Try to lookup the Template by name } else { // Get the Template requested by name - s.Template, err = models.GetTemplateByName(s.Template.Name, ctx.Get(r, "user_id").(int64)) + s.Template, err = models.GetTemplateByName(s.Template.Name, s.UserId) if err == gorm.ErrRecordNotFound { log.WithFields(logrus.Fields{ "template": s.Template.Name, @@ -700,12 +696,32 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) { JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) return } + s.TemplateId = s.Template.Id + // We'll only save the test request to the database if there is a + // user-specified template to use. + storeRequest = true + } + + if s.Page.Name != "" { + s.Page, err = models.GetPageByName(s.Page.Name, s.UserId) + if err == gorm.ErrRecordNotFound { + log.WithFields(logrus.Fields{ + "page": s.Page.Name, + }).Error("Page does not exist") + JSONResponse(w, models.Response{Success: false, Message: models.ErrPageNotFound.Error()}, http.StatusBadRequest) + return + } else if err != nil { + log.Error(err) + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) + return + } + s.PageId = s.Page.Id } // If a complete sending profile is provided use it if err := s.SMTP.Validate(); err != nil { // Otherwise get the SMTP requested by name - smtp, lookupErr := models.GetSMTPByName(s.SMTP.Name, ctx.Get(r, "user_id").(int64)) + smtp, lookupErr := models.GetSMTPByName(s.SMTP.Name, s.UserId) // If the Sending Profile doesn't exist, let's err on the side // of caution and assume that the validation failure was more important. if lookupErr != nil { @@ -716,9 +732,25 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) { s.SMTP = smtp } + // Validate the given request + if err = s.Validate(); err != nil { + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) + return + } + + // Store the request if this wasn't the default template + if storeRequest { + err = models.PostEmailRequest(s) + if err != nil { + log.Error(err) + JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) + return + } + } // Send the test email err = Worker.SendTestEmail(s) if err != nil { + log.Error(err) JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) return } diff --git a/controllers/api_test.go b/controllers/api_test.go index 40758413..8e024ece 100644 --- a/controllers/api_test.go +++ b/controllers/api_test.go @@ -63,8 +63,8 @@ func (s *ControllersSuite) SetupTest() { // Add a group group := models.Group{Name: "Test Group"} group.Targets = []models.Target{ - models.Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, - models.Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, + models.Target{BaseRecipient: models.BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}}, + models.Target{BaseRecipient: models.BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}}, } group.UserId = 1 models.PostGroup(&group) diff --git a/controllers/phish.go b/controllers/phish.go index d37c7954..4d5a2d99 100644 --- a/controllers/phish.go +++ b/controllers/phish.go @@ -1,14 +1,10 @@ package controllers import ( - "bytes" "errors" "fmt" - "html/template" "net" "net/http" - "net/mail" - "net/url" "strings" ctx "github.com/gophish/gophish/context" @@ -50,6 +46,11 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } + // Check for a preview + if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok { + http.ServeFile(w, r, "static/images/pixel.png") + return + } rs := ctx.Get(r, "result").(models.Result) d := ctx.Get(r, "details").(models.EventDetails) err = rs.HandleEmailOpened(d) @@ -70,6 +71,11 @@ func PhishReporter(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } + // Check for a preview + if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok { + w.WriteHeader(http.StatusNoContent) + return + } rs := ctx.Get(r, "result").(models.Result) d := ctx.Get(r, "details").(models.EventDetails) @@ -92,6 +98,24 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } + var ptx models.PhishingTemplateContext + // Check for a preview + if preview, ok := ctx.Get(r, "result").(models.EmailRequest); ok { + ptx, err = models.NewPhishingTemplateContext(&preview, preview.BaseRecipient, preview.RId) + if err != nil { + log.Error(err) + http.NotFound(w, r) + return + } + p, err := models.GetPage(preview.PageId, preview.UserId) + if err != nil { + log.Error(err) + http.NotFound(w, r) + return + } + renderPhishResponse(w, r, ptx, p) + return + } rs := ctx.Get(r, "result").(models.Result) c := ctx.Get(r, "campaign").(models.Campaign) d := ctx.Get(r, "details").(models.EventDetails) @@ -112,49 +136,35 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) { if err != nil { log.Error(err) } - // Redirect to the desired page + } + ptx, err = models.NewPhishingTemplateContext(&c, rs.BaseRecipient, rs.RId) + if err != nil { + log.Error(err) + http.NotFound(w, r) + } + renderPhishResponse(w, r, ptx, p) +} + +// renderPhishResponse handles rendering the correct response to the phishing +// connection. This usually involves writing out the page HTML or redirecting +// the user to the correct URL. +func renderPhishResponse(w http.ResponseWriter, r *http.Request, ptx models.PhishingTemplateContext, p models.Page) { + // If the request was a form submit and a redirect URL was specified, we + // should send the user to that URL + if r.Method == "POST" { if p.RedirectURL != "" { http.Redirect(w, r, p.RedirectURL, 302) return } } - var htmlBuff bytes.Buffer - tmpl, err := template.New("html_template").Parse(p.HTML) + // Otherwise, we just need to write out the templated HTML + html, err := models.ExecuteTemplate(p.HTML, ptx) if err != nil { log.Error(err) http.NotFound(w, r) return } - f, err := mail.ParseAddress(c.SMTP.FromAddress) - if err != nil { - log.Error(err) - } - fn := f.Name - if fn == "" { - fn = f.Address - } - - phishURL, _ := url.Parse(c.URL) - q := phishURL.Query() - q.Set(models.RecipientParameter, rs.RId) - phishURL.RawQuery = q.Encode() - - rsf := struct { - models.Result - URL string - From string - }{ - rs, - phishURL.String(), - fn, - } - err = tmpl.Execute(&htmlBuff, rsf) - if err != nil { - log.Error(err) - http.NotFound(w, r) - return - } - w.Write(htmlBuff.Bytes()) + w.Write([]byte(html)) } // RobotsHandler prevents search engines, etc. from indexing phishing materials @@ -173,6 +183,15 @@ func setupContext(r *http.Request) (error, *http.Request) { if id == "" { return ErrInvalidRequest, r } + // Check to see if this is a preview or a real result + if strings.HasPrefix(id, models.PreviewPrefix) { + rs, err := models.GetEmailRequestByResultId(id) + if err != nil { + return err, r + } + r = ctx.Set(r, "result", rs) + return nil, r + } rs, err := models.GetResult(id) if err != nil { return err, r diff --git a/controllers/phish_test.go b/controllers/phish_test.go index 4b034bc7..02288708 100644 --- a/controllers/phish_test.go +++ b/controllers/phish_test.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io/ioutil" + "log" "net/http" "github.com/gophish/gophish/models" @@ -15,6 +16,23 @@ func (s *ControllersSuite) getFirstCampaign() models.Campaign { return campaigns[0] } +func (s *ControllersSuite) getFirstEmailRequest() models.EmailRequest { + campaign := s.getFirstCampaign() + req := models.EmailRequest{ + TemplateId: campaign.TemplateId, + Template: campaign.Template, + PageId: campaign.PageId, + Page: campaign.Page, + URL: "http://localhost.localdomain", + UserId: 1, + BaseRecipient: campaign.Results[0].BaseRecipient, + SMTP: campaign.SMTP, + } + err := models.PostEmailRequest(&req) + s.Nil(err) + return req +} + func (s *ControllersSuite) openEmail(rid string) { resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid)) s.Nil(err) @@ -40,13 +58,14 @@ func (s *ControllersSuite) openEmail404(rid string) { s.Equal(resp.StatusCode, http.StatusNotFound) } -func (s *ControllersSuite) clickLink(rid string, campaign models.Campaign) { +func (s *ControllersSuite) clickLink(rid string, expectedHTML string) { resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", ps.URL, models.RecipientParameter, rid)) s.Nil(err) defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) s.Nil(err) - s.Equal(bytes.Compare(body, []byte(campaign.Page.HTML)), 0) + log.Printf("%s\n\n\n", body) + s.Equal(bytes.Compare(body, []byte(expectedHTML)), 0) } func (s *ControllersSuite) clickLink404(rid string) { @@ -93,7 +112,7 @@ func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() { s.Equal(result.Status, models.STATUS_SENDING) s.openEmail(result.RId) - s.clickLink(result.RId, campaign) + s.clickLink(result.RId, campaign.Page.HTML) campaign = s.getFirstCampaign() result = campaign.Results[0] @@ -153,3 +172,19 @@ func (s *ControllersSuite) TestRobotsHandler() { s.Nil(err) s.Equal(bytes.Compare(body, expected), 0) } + +func (s *ControllersSuite) TestInvalidPreviewID() { + bogusRId := fmt.Sprintf("%sbogus", models.PreviewPrefix) + s.openEmail404(bogusRId) + s.clickLink404(bogusRId) +} + +func (s *ControllersSuite) TestPreviewTrack() { + req := s.getFirstEmailRequest() + s.openEmail(req.RId) +} + +func (s *ControllersSuite) TestPreviewClick() { + req := s.getFirstEmailRequest() + s.clickLink(req.RId, req.Page.HTML) +} diff --git a/db/db_mysql/migrations/20180527213648_0.7.0_store_email_request.sql b/db/db_mysql/migrations/20180527213648_0.7.0_store_email_request.sql new file mode 100644 index 00000000..a2008b89 --- /dev/null +++ b/db/db_mysql/migrations/20180527213648_0.7.0_store_email_request.sql @@ -0,0 +1,21 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE TABLE IF NOT EXISTS email_requests ( + `id` integer primary key auto_increment, + `user_id` integer, + `template_id` integer, + `page_id` integer, + `first_name` varchar(255), + `last_name` varchar(255), + `email` varchar(255), + `position` varchar(255), + `url` varchar(255), + `r_id` varchar(255), + `from_address` varchar(255) +); + + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back +DROP TABLE email_requests diff --git a/db/db_sqlite3/migrations/20180527213648_0.7.0_store_email_request.sql b/db/db_sqlite3/migrations/20180527213648_0.7.0_store_email_request.sql new file mode 100644 index 00000000..69d68050 --- /dev/null +++ b/db/db_sqlite3/migrations/20180527213648_0.7.0_store_email_request.sql @@ -0,0 +1,21 @@ + +-- +goose Up +-- SQL in section 'Up' is executed when this migration is applied +CREATE TABLE IF NOT EXISTS "email_requests" ( + "id" integer primary key autoincrement, + "user_id" integer, + "template_id" integer, + "page_id" integer, + "first_name" varchar(255), + "last_name" varchar(255), + "email" varchar(255), + "position" varchar(255), + "url" varchar(255), + "r_id" varchar(255), + "from_address" varchar(255) +); + + +-- +goose Down +-- SQL section 'Down' is executed when this migration is rolled back + diff --git a/models/campaign.go b/models/campaign.go index 8c20d3d1..90d2c873 100644 --- a/models/campaign.go +++ b/models/campaign.go @@ -206,6 +206,18 @@ func (c *Campaign) getDetails() error { return nil } +// getBaseURL returns the Campaign's configured URL. +// This is used to implement the TemplateContext interface. +func (c *Campaign) getBaseURL() string { + return c.URL +} + +// getFromAddress returns the Campaign's configured SMTP "From" address. +// This is used to implement the TemplateContext interface. +func (c *Campaign) getFromAddress() string { + return c.SMTP.FromAddress +} + // getCampaignStats returns a CampaignStats object for the campaign with the given campaign ID. // It also backfills numbers as appropriate with a running total, so that the values are aggregated. func getCampaignStats(cid int64) (CampaignStats, error) { @@ -452,13 +464,15 @@ func PostCampaign(c *Campaign, uid int64) error { } resultMap[t.Email] = true r := &Result{ - Email: t.Email, - Position: t.Position, + BaseRecipient: BaseRecipient{ + Email: t.Email, + Position: t.Position, + FirstName: t.FirstName, + LastName: t.LastName, + }, Status: STATUS_SCHEDULED, CampaignId: c.Id, UserId: c.UserId, - FirstName: t.FirstName, - LastName: t.LastName, SendDate: c.LaunchDate, Reported: false, ModifiedDate: c.CreatedDate, diff --git a/models/email_request.go b/models/email_request.go index c255b007..f4b1280f 100644 --- a/models/email_request.go +++ b/models/email_request.go @@ -12,55 +12,94 @@ import ( "github.com/gophish/gophish/mailer" ) -// SendTestEmailRequest is the structure of a request +// PreviewPrefix is the standard prefix added to the rid parameter when sending +// test emails. +const PreviewPrefix = "preview-" + +// EmailRequest is the structure of a request // to send a test email to test an SMTP connection. // This type implements the mailer.Mail interface. -type SendTestEmailRequest struct { - Template Template `json:"template"` - Page Page `json:"page"` - SMTP SMTP `json:"smtp"` - URL string `json:"url"` - Tracker string `json:"tracker"` - TrackingURL string `json:"tracking_url"` - From string `json:"from"` - Target - ErrorChan chan (error) `json:"-"` +type EmailRequest struct { + Id int64 `json:"-"` + Template Template `json:"template"` + TemplateId int64 `json:"-"` + Page Page `json:"page"` + PageId int64 `json:"-"` + SMTP SMTP `json:"smtp"` + URL string `json:"url"` + Tracker string `json:"tracker" gorm:"-"` + TrackingURL string `json:"tracking_url" gorm:"-"` + UserId int64 `json:"-"` + ErrorChan chan (error) `json:"-" gorm:"-"` + RId string `json:"id"` + FromAddress string `json:"-"` + BaseRecipient +} + +func (s *EmailRequest) getBaseURL() string { + return s.URL +} + +func (s *EmailRequest) getFromAddress() string { + return s.FromAddress } // Validate ensures the SendTestEmailRequest structure // is valid. -func (s *SendTestEmailRequest) Validate() error { +func (s *EmailRequest) Validate() error { switch { case s.Email == "": return ErrEmailNotSpecified + case s.FromAddress == "" && s.SMTP.FromAddress == "": + return ErrFromAddressNotSpecified } return nil } // Backoff treats temporary errors as permanent since this is expected to be a // synchronous operation. It returns any errors given back to the ErrorChan -func (s *SendTestEmailRequest) Backoff(reason error) error { +func (s *EmailRequest) Backoff(reason error) error { s.ErrorChan <- reason return nil } // Error returns an error on the ErrorChan. -func (s *SendTestEmailRequest) Error(err error) error { +func (s *EmailRequest) Error(err error) error { s.ErrorChan <- err return nil } // Success returns nil on the ErrorChan to indicate that the email was sent // successfully. -func (s *SendTestEmailRequest) Success() error { +func (s *EmailRequest) Success() error { s.ErrorChan <- nil return nil } +// PostEmailRequest stores a SendTestEmailRequest in the database. +func PostEmailRequest(s *EmailRequest) error { + // Generate an ID to be used in the underlying Result object + rid, err := generateResultId() + if err != nil { + return err + } + s.RId = fmt.Sprintf("%s%s", PreviewPrefix, rid) + s.FromAddress = s.SMTP.FromAddress + return db.Save(&s).Error +} + +// GetEmailRequestByResultId retrieves the EmailRequest by the underlying rid +// parameter. +func GetEmailRequestByResultId(id string) (EmailRequest, error) { + s := EmailRequest{} + err := db.Table("email_requests").Where("r_id=?", id).First(&s).Error + return s, err +} + // Generate fills in the details of a gomail.Message with the contents // from the SendTestEmailRequest. -func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error { - f, err := mail.ParseAddress(s.SMTP.FromAddress) +func (s *EmailRequest) Generate(msg *gomail.Message) error { + f, err := mail.ParseAddress(s.FromAddress) if err != nil { return err } @@ -70,7 +109,12 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error { } msg.SetAddressHeader("From", f.Address, f.Name) - url, err := buildTemplate(s.URL, s) + ptx, err := NewPhishingTemplateContext(s, s.BaseRecipient, s.RId) + if err != nil { + return err + } + + url, err := ExecuteTemplate(s.URL, ptx) if err != nil { return err } @@ -78,12 +122,12 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error { // Parse the customHeader templates for _, header := range s.SMTP.Headers { - key, err := buildTemplate(header.Key, s) + key, err := ExecuteTemplate(header.Key, ptx) if err != nil { log.Error(err) } - value, err := buildTemplate(header.Value, s) + value, err := ExecuteTemplate(header.Value, ptx) if err != nil { log.Error(err) } @@ -93,7 +137,7 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error { } // Parse remaining templates - subject, err := buildTemplate(s.Template.Subject, s) + subject, err := ExecuteTemplate(s.Template.Subject, ptx) if err != nil { log.Error(err) } @@ -104,14 +148,14 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error { msg.SetHeader("To", s.FormatAddress()) if s.Template.Text != "" { - text, err := buildTemplate(s.Template.Text, s) + text, err := ExecuteTemplate(s.Template.Text, ptx) if err != nil { log.Error(err) } msg.SetBody("text/plain", text) } if s.Template.HTML != "" { - html, err := buildTemplate(s.Template.HTML, s) + html, err := ExecuteTemplate(s.Template.HTML, ptx) if err != nil { log.Error(err) } @@ -137,6 +181,6 @@ func (s *SendTestEmailRequest) Generate(msg *gomail.Message) error { } // GetDialer returns the mailer.Dialer for the underlying SMTP object -func (s *SendTestEmailRequest) GetDialer() (mailer.Dialer, error) { +func (s *EmailRequest) GetDialer() (mailer.Dialer, error) { return s.SMTP.GetDialer() } diff --git a/models/email_request_test.go b/models/email_request_test.go index 542c47f8..58ae5e9d 100644 --- a/models/email_request_test.go +++ b/models/email_request_test.go @@ -11,14 +11,16 @@ import ( ) func (s *ModelsSuite) TestEmailNotPresent(ch *check.C) { - req := &SendTestEmailRequest{} + req := &EmailRequest{} ch.Assert(req.Validate(), check.Equals, ErrEmailNotSpecified) req.Email = "test@example.com" + ch.Assert(req.Validate(), check.Equals, ErrFromAddressNotSpecified) + req.FromAddress = "from@example.com" ch.Assert(req.Validate(), check.Equals, nil) } func (s *ModelsSuite) TestEmailRequestBackoff(ch *check.C) { - req := &SendTestEmailRequest{ + req := &EmailRequest{ ErrorChan: make(chan error), } expected := errors.New("Temporary Error") @@ -30,7 +32,7 @@ func (s *ModelsSuite) TestEmailRequestBackoff(ch *check.C) { } func (s *ModelsSuite) TestEmailRequestError(ch *check.C) { - req := &SendTestEmailRequest{ + req := &EmailRequest{ ErrorChan: make(chan error), } expected := errors.New("Temporary Error") @@ -42,7 +44,7 @@ func (s *ModelsSuite) TestEmailRequestError(ch *check.C) { } func (s *ModelsSuite) TestEmailRequestSuccess(ch *check.C) { - req := &SendTestEmailRequest{ + req := &EmailRequest{ ErrorChan: make(chan error), } go func() { @@ -62,15 +64,15 @@ func (s *ModelsSuite) TestEmailRequestGenerate(ch *check.C) { Text: "{{.Email}} - Text", HTML: "{{.Email}} - HTML", } - target := Target{ - FirstName: "First", - LastName: "Last", - Email: "firstlast@example.com", - } - req := &SendTestEmailRequest{ + req := &EmailRequest{ SMTP: smtp, Template: template, - Target: target, + BaseRecipient: BaseRecipient{ + FirstName: "First", + LastName: "Last", + Email: "firstlast@example.com", + }, + FromAddress: smtp.FromAddress, } msg := gomail.NewMessage() @@ -104,23 +106,24 @@ func (s *ModelsSuite) TestEmailRequestURLTemplating(ch *check.C) { Text: "{{.URL}}", HTML: "{{.URL}}", } - target := Target{ - FirstName: "First", - LastName: "Last", - Email: "firstlast@example.com", - } - req := &SendTestEmailRequest{ + req := &EmailRequest{ SMTP: smtp, Template: template, - Target: target, - URL: "http://127.0.0.1/{{.Email}}", + URL: "http://127.0.0.1/{{.Email}}", + BaseRecipient: BaseRecipient{ + FirstName: "First", + LastName: "Last", + Email: "firstlast@example.com", + }, + FromAddress: smtp.FromAddress, + RId: fmt.Sprintf("%s-foobar", PreviewPrefix), } msg := gomail.NewMessage() err = req.Generate(msg) ch.Assert(err, check.Equals, nil) - expectedURL := fmt.Sprintf("http://127.0.0.1/%s", target.Email) + expectedURL := fmt.Sprintf("http://127.0.0.1/%s?%s=%s", req.Email, RecipientParameter, req.RId) msgBuff := &bytes.Buffer{} _, err = msg.WriteTo(msgBuff) @@ -142,15 +145,15 @@ func (s *ModelsSuite) TestEmailRequestGenerateEmptySubject(ch *check.C) { Text: "{{.Email}} - Text", HTML: "{{.Email}} - HTML", } - target := Target{ - FirstName: "First", - LastName: "Last", - Email: "firstlast@example.com", - } - req := &SendTestEmailRequest{ + req := &EmailRequest{ SMTP: smtp, Template: template, - Target: target, + BaseRecipient: BaseRecipient{ + FirstName: "First", + LastName: "Last", + Email: "firstlast@example.com", + }, + FromAddress: smtp.FromAddress, } msg := gomail.NewMessage() @@ -171,3 +174,44 @@ func (s *ModelsSuite) TestEmailRequestGenerateEmptySubject(ch *check.C) { ch.Assert(err, check.Equals, nil) ch.Assert(got.Subject, check.Equals, expected.Subject) } + +func (s *ModelsSuite) TestPostSendTestEmailRequest(ch *check.C) { + smtp := SMTP{ + FromAddress: "from@example.com", + } + template := Template{ + Name: "Test Template", + Subject: "", + Text: "{{.Email}} - Text", + HTML: "{{.Email}} - HTML", + UserId: 1, + } + err := PostTemplate(&template) + ch.Assert(err, check.Equals, nil) + + page := Page{ + Name: "Test Page", + HTML: "test", + UserId: 1, + } + err = PostPage(&page) + ch.Assert(err, check.Equals, nil) + + req := &EmailRequest{ + SMTP: smtp, + TemplateId: template.Id, + PageId: page.Id, + BaseRecipient: BaseRecipient{ + FirstName: "First", + LastName: "Last", + Email: "firstlast@example.com", + }, + } + err = PostEmailRequest(req) + ch.Assert(err, check.Equals, nil) + + got, err := GetEmailRequestByResultId(req.RId) + ch.Assert(err, check.Equals, nil) + ch.Assert(got.RId, check.Equals, req.RId) + ch.Assert(got.Email, check.Equals, req.Email) +} diff --git a/models/group.go b/models/group.go index 2415d159..a7f0248a 100644 --- a/models/group.go +++ b/models/group.go @@ -46,14 +46,33 @@ type GroupTarget struct { // Target contains the fields needed for individual targets specified by the user // Groups contain 1..* Targets, but 1 Target may belong to 1..* Groups type Target struct { - Id int64 `json:"-"` + Id int64 `json:"-"` + BaseRecipient +} + +// BaseRecipient contains the fields for a single recipient. This is the base +// struct used in members of groups and campaign results. +type BaseRecipient struct { + Email string `json:"email"` FirstName string `json:"first_name"` LastName string `json:"last_name"` - Email string `json:"email"` Position string `json:"position"` } -// Returns the email address to use in the "To" header of the email +// FormatAddress returns the email address to use in the "To" header of the email +func (r *BaseRecipient) FormatAddress() string { + addr := r.Email + if r.FirstName != "" && r.LastName != "" { + a := &mail.Address{ + Name: fmt.Sprintf("%s %s", r.FirstName, r.LastName), + Address: r.Email, + } + addr = a.String() + } + return addr +} + +// FormatAddress returns the email address to use in the "To" header of the email func (t *Target) FormatAddress() string { addr := t.Email if t.FirstName != "" && t.LastName != "" { @@ -66,7 +85,7 @@ func (t *Target) FormatAddress() string { return addr } -// ErrNoEmailSpecified is thrown when no email is specified for the Target +// ErrEmailNotSpecified is thrown when no email is specified for the Target var ErrEmailNotSpecified = errors.New("No email address specified") // ErrGroupNameNotSpecified is thrown when a group name is not specified @@ -274,11 +293,12 @@ func insertTargetIntoGroup(t Target, gid int64) error { return err } trans := db.Begin() - trans.Where(t).FirstOrCreate(&t) + err = trans.Where(t).FirstOrCreate(&t).Error if err != nil { log.WithFields(logrus.Fields{ "email": t.Email, - }).Error("Error adding target") + }).Error(err) + trans.Rollback() return err } err = trans.Where("group_id=? and target_id=?", gid, t.Id).Find(&GroupTarget{}).Error @@ -286,6 +306,7 @@ func insertTargetIntoGroup(t Target, gid int64) error { err = trans.Save(&GroupTarget{GroupId: gid, TargetId: t.Id}).Error if err != nil { log.Error(err) + trans.Rollback() return err } } @@ -293,10 +314,12 @@ func insertTargetIntoGroup(t Target, gid int64) error { log.WithFields(logrus.Fields{ "email": t.Email, }).Error("Error adding many-many mapping") + trans.Rollback() return err } err = trans.Commit().Error if err != nil { + trans.Rollback() log.Error("Error committing db changes") return err } diff --git a/models/group_test.go b/models/group_test.go index c03c279f..20713939 100644 --- a/models/group_test.go +++ b/models/group_test.go @@ -7,7 +7,7 @@ import ( func (s *ModelsSuite) TestPostGroup(c *check.C) { g := Group{Name: "Test Group"} - g.Targets = []Target{Target{Email: "test@example.com"}} + g.Targets = []Target{Target{BaseRecipient: BaseRecipient{Email: "test@example.com"}}} g.UserId = 1 err := PostGroup(&g) c.Assert(err, check.Equals, nil) @@ -17,7 +17,7 @@ func (s *ModelsSuite) TestPostGroup(c *check.C) { func (s *ModelsSuite) TestPostGroupNoName(c *check.C) { g := Group{Name: ""} - g.Targets = []Target{Target{Email: "test@example.com"}} + g.Targets = []Target{Target{BaseRecipient: BaseRecipient{Email: "test@example.com"}}} g.UserId = 1 err := PostGroup(&g) c.Assert(err, check.Equals, ErrGroupNameNotSpecified) @@ -34,14 +34,22 @@ func (s *ModelsSuite) TestPostGroupNoTargets(c *check.C) { func (s *ModelsSuite) TestGetGroups(c *check.C) { // Add groups. PostGroup(&Group{ - Name: "Test Group 1", - Targets: []Target{Target{Email: "test1@example.com"}}, - UserId: 1, + Name: "Test Group 1", + Targets: []Target{ + Target{ + BaseRecipient: BaseRecipient{Email: "test1@example.com"}, + }, + }, + UserId: 1, }) PostGroup(&Group{ - Name: "Test Group 2", - Targets: []Target{Target{Email: "test2@example.com"}}, - UserId: 1, + Name: "Test Group 2", + Targets: []Target{ + Target{ + BaseRecipient: BaseRecipient{Email: "test2@example.com"}, + }, + }, + UserId: 1, }) // Get groups and test result. @@ -65,9 +73,13 @@ func (s *ModelsSuite) TestGetGroupsNoGroups(c *check.C) { func (s *ModelsSuite) TestGetGroup(c *check.C) { // Add group. originalGroup := &Group{ - Name: "Test Group", - Targets: []Target{Target{Email: "test@example.com"}}, - UserId: 1, + Name: "Test Group", + Targets: []Target{ + Target{ + BaseRecipient: BaseRecipient{Email: "test@example.com"}, + }, + }, + UserId: 1, } c.Assert(PostGroup(originalGroup), check.Equals, nil) @@ -87,9 +99,13 @@ func (s *ModelsSuite) TestGetGroupNoGroups(c *check.C) { func (s *ModelsSuite) TestGetGroupByName(c *check.C) { // Add group. PostGroup(&Group{ - Name: "Test Group", - Targets: []Target{Target{Email: "test@example.com"}}, - UserId: 1, + Name: "Test Group", + Targets: []Target{ + Target{ + BaseRecipient: BaseRecipient{Email: "test@example.com"}, + }, + }, + UserId: 1, }) // Get group and test result. @@ -109,8 +125,8 @@ func (s *ModelsSuite) TestPutGroup(c *check.C) { // Add test group. group := Group{Name: "Test Group"} group.Targets = []Target{ - Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, - Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, + Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}}, + Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}}, } group.UserId = 1 PostGroup(&group) @@ -134,8 +150,8 @@ func (s *ModelsSuite) TestPutGroupEmptyAttribute(c *check.C) { // Add test group. group := Group{Name: "Test Group"} group.Targets = []Target{ - Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, - Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, + Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}}, + Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}}, } group.UserId = 1 PostGroup(&group) diff --git a/models/maillog.go b/models/maillog.go index c09d0fee..cd4a6fd4 100644 --- a/models/maillog.go +++ b/models/maillog.go @@ -1,17 +1,13 @@ package models import ( - "bytes" "encoding/base64" "errors" "fmt" "io" "math" "net/mail" - "net/url" - "path" "strings" - "text/template" "time" "github.com/gophish/gomail" @@ -136,18 +132,6 @@ func (m *MailLog) GetDialer() (mailer.Dialer, error) { return c.SMTP.GetDialer() } -// buildTemplate creates a templated string based on the provided -// template body and data. -func buildTemplate(text string, data interface{}) (string, error) { - buff := bytes.Buffer{} - tmpl, err := template.New("template").Parse(text) - if err != nil { - return buff.String(), err - } - err = tmpl.Execute(&buff, data) - return buff.String(), err -} - // Generate fills in the details of a gomail.Message instance with // the correct headers and body from the campaign and recipient listed in // the maillog. We accept the gomail.Message as an argument so that the caller @@ -161,51 +145,26 @@ func (m *MailLog) Generate(msg *gomail.Message) error { if err != nil { return err } + f, err := mail.ParseAddress(c.SMTP.FromAddress) if err != nil { return err } - fn := f.Name - if fn == "" { - fn = f.Address - } msg.SetAddressHeader("From", f.Address, f.Name) - campaignURL, err := buildTemplate(c.URL, r) + + ptx, err := NewPhishingTemplateContext(&c, r.BaseRecipient, r.RId) if err != nil { return err } - phishURL, _ := url.Parse(campaignURL) - q := phishURL.Query() - q.Set("rid", r.RId) - phishURL.RawQuery = q.Encode() - - trackingURL, _ := url.Parse(campaignURL) - trackingURL.Path = path.Join(trackingURL.Path, "/track") - trackingURL.RawQuery = q.Encode() - - td := struct { - Result - URL string - TrackingURL string - Tracker string - From string - }{ - r, - phishURL.String(), - trackingURL.String(), - "", - fn, - } - // Parse the customHeader templates for _, header := range c.SMTP.Headers { - key, err := buildTemplate(header.Key, td) + key, err := ExecuteTemplate(header.Key, ptx) if err != nil { log.Warn(err) } - value, err := buildTemplate(header.Value, td) + value, err := ExecuteTemplate(header.Value, ptx) if err != nil { log.Warn(err) } @@ -215,7 +174,7 @@ func (m *MailLog) Generate(msg *gomail.Message) error { } // Parse remaining templates - subject, err := buildTemplate(c.Template.Subject, td) + subject, err := ExecuteTemplate(c.Template.Subject, ptx) if err != nil { log.Warn(err) } @@ -226,14 +185,14 @@ func (m *MailLog) Generate(msg *gomail.Message) error { msg.SetHeader("To", r.FormatAddress()) if c.Template.Text != "" { - text, err := buildTemplate(c.Template.Text, td) + text, err := ExecuteTemplate(c.Template.Text, ptx) if err != nil { log.Warn(err) } msg.SetBody("text/plain", text) } if c.Template.HTML != "" { - html, err := buildTemplate(c.Template.HTML, td) + html, err := ExecuteTemplate(c.Template.HTML, ptx) if err != nil { log.Warn(err) } diff --git a/models/models_test.go b/models/models_test.go index b840f8f0..a4c1ed4c 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -45,8 +45,8 @@ func (s *ModelsSuite) createCampaignDependencies(ch *check.C, optional ...string // we use the optional parameter to pass an alternative subject group := Group{Name: "Test Group"} group.Targets = []Target{ - Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, - Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, + Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}}, + Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}}, } group.UserId = 1 ch.Assert(PostGroup(&group), check.Equals, nil) diff --git a/models/result.go b/models/result.go index 431b6f50..b18efc26 100644 --- a/models/result.go +++ b/models/result.go @@ -3,10 +3,8 @@ package models import ( "crypto/rand" "encoding/json" - "fmt" "math/big" "net" - "net/mail" "time" log "github.com/gophish/gophish/logger" @@ -30,10 +28,6 @@ type Result struct { CampaignId int64 `json:"-"` UserId int64 `json:"-"` RId string `json:"id"` - Email string `json:"email"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Position string `json:"position"` Status string `json:"status" sql:"not null"` IP string `json:"ip"` Latitude float64 `json:"latitude"` @@ -41,6 +35,7 @@ type Result struct { SendDate time.Time `json:"send_date"` Reported bool `json:"reported" sql:"not null"` ModifiedDate time.Time `json:"modified_date"` + BaseRecipient } func (r *Result) createEvent(status string, details interface{}) (*Event, error) { @@ -178,22 +173,30 @@ func (r *Result) UpdateGeo(addr string) error { return db.Save(r).Error } +func generateResultId() (string, error) { + const alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + k := make([]byte, 7) + for i := range k { + idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphaNum)))) + if err != nil { + return "", err + } + k[i] = alphaNum[idx.Int64()] + } + return string(k), nil +} + // GenerateId generates a unique key to represent the result // in the database func (r *Result) GenerateId() error { // Keep trying until we generate a unique key (shouldn't take more than one or two iterations) - const alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - k := make([]byte, 7) for { - for i := range k { - idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphaNum)))) - if err != nil { - return err - } - k[i] = alphaNum[idx.Int64()] + rid, err := generateResultId() + if err != nil { + return err } - r.RId = string(k) - err := db.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error + r.RId = rid + err = db.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error if err == gorm.ErrRecordNotFound { break } @@ -201,19 +204,6 @@ func (r *Result) GenerateId() error { return nil } -// FormatAddress returns the email address to use in the "To" header of the email -func (r *Result) FormatAddress() string { - addr := r.Email - if r.FirstName != "" && r.LastName != "" { - a := &mail.Address{ - Name: fmt.Sprintf("%s %s", r.FirstName, r.LastName), - Address: r.Email, - } - addr = a.String() - } - return addr -} - // GetResult returns the Result object from the database // given the ResultId func GetResult(rid string) (Result, error) { diff --git a/models/result_test.go b/models/result_test.go index e7cbdeb0..5b81b9bf 100644 --- a/models/result_test.go +++ b/models/result_test.go @@ -18,9 +18,11 @@ func (s *ModelsSuite) TestGenerateResultId(c *check.C) { func (s *ModelsSuite) TestFormatAddress(c *check.C) { r := Result{ - FirstName: "John", - LastName: "Doe", - Email: "johndoe@example.com", + BaseRecipient: BaseRecipient{ + FirstName: "John", + LastName: "Doe", + Email: "johndoe@example.com", + }, } expected := &mail.Address{ Name: "John Doe", @@ -29,7 +31,7 @@ func (s *ModelsSuite) TestFormatAddress(c *check.C) { c.Assert(r.FormatAddress(), check.Equals, expected.String()) r = Result{ - Email: "johndoe@example.com", + BaseRecipient: BaseRecipient{Email: "johndoe@example.com"}, } c.Assert(r.FormatAddress(), check.Equals, r.Email) } @@ -59,9 +61,9 @@ func (s *ModelsSuite) TestResultScheduledStatus(ch *check.C) { func (s *ModelsSuite) TestDuplicateResults(ch *check.C) { group := Group{Name: "Test Group"} group.Targets = []Target{ - Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, - Target{Email: "test1@example.com", FirstName: "Duplicate", LastName: "Duplicate"}, - Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, + Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}}, + Target{BaseRecipient: BaseRecipient{Email: "test1@example.com", FirstName: "Duplicate", LastName: "Duplicate"}}, + Target{BaseRecipient: BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}}, } group.UserId = 1 ch.Assert(PostGroup(&group), check.Equals, nil) diff --git a/models/template.go b/models/template.go index 8d71c789..b6630d38 100644 --- a/models/template.go +++ b/models/template.go @@ -1,9 +1,7 @@ package models import ( - "bytes" "errors" - "html/template" "time" log "github.com/gophish/gophish/logger" @@ -36,7 +34,6 @@ func (t *Template) Validate() error { case t.Text == "" && t.HTML == "": return ErrTemplateMissingParameter } - var buff bytes.Buffer // Test that the variables used in the template // validate with no issues td := struct { @@ -47,31 +44,27 @@ func (t *Template) Validate() error { From string }{ Result{ - Email: "foo@bar.com", - FirstName: "Foo", - LastName: "Bar", - Position: "Test", + BaseRecipient: BaseRecipient{ + Email: "foo@bar.com", + FirstName: "Foo", + LastName: "Bar", + Position: "Test", + }, }, "http://foo.bar", "http://foo.bar/track", "", + From: fn, + RId: rid, + }, nil +} + +// ExecuteTemplate creates a templated string based on the provided +// template body and data. +func ExecuteTemplate(text string, data interface{}) (string, error) { + buff := bytes.Buffer{} + tmpl, err := template.New("template").Parse(text) + if err != nil { + return buff.String(), err + } + err = tmpl.Execute(&buff, data) + return buff.String(), err +} diff --git a/util/util.go b/util/util.go index f0ffb0b9..414f2828 100644 --- a/util/util.go +++ b/util/util.go @@ -100,10 +100,12 @@ func ParseCSV(r *http.Request) ([]models.Target, error) { ps = record[pi] } t := models.Target{ - FirstName: fn, - LastName: ln, - Email: ea, - Position: ps, + BaseRecipient: models.BaseRecipient{ + FirstName: fn, + LastName: ln, + Email: ea, + Position: ps, + }, } ts = append(ts, t) } diff --git a/util/util_test.go b/util/util_test.go index d11ed5b5..1274071c 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -52,9 +52,11 @@ func buildCSVRequest(csvPayload string) (*http.Request, error) { func (s *UtilSuite) TestParseCSVEmail() { expected := models.Target{ - FirstName: "John", - LastName: "Doe", - Email: "johndoe@example.com", + BaseRecipient: models.BaseRecipient{ + FirstName: "John", + LastName: "Doe", + Email: "johndoe@example.com", + }, } csvPayload := fmt.Sprintf("%s,%s,<%s>", expected.FirstName, expected.LastName, expected.Email) diff --git a/worker/worker.go b/worker/worker.go index 5b918443..c73f555b 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -86,7 +86,7 @@ func (w *Worker) LaunchCampaign(c models.Campaign) { } // SendTestEmail sends a test email -func (w *Worker) SendTestEmail(s *models.SendTestEmailRequest) error { +func (w *Worker) SendTestEmail(s *models.EmailRequest) error { go func() { mailer.Mailer.Queue <- []mailer.Mail{s} }() diff --git a/worker/worker_test.go b/worker/worker_test.go index 292c784d..9ad6383d 100644 --- a/worker/worker_test.go +++ b/worker/worker_test.go @@ -35,8 +35,8 @@ func (s *WorkerSuite) SetupTest() { // Add a group group := models.Group{Name: "Test Group"} group.Targets = []models.Target{ - models.Target{Email: "test1@example.com", FirstName: "First", LastName: "Example"}, - models.Target{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}, + models.Target{BaseRecipient: models.BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}}, + models.Target{BaseRecipient: models.BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}}, } group.UserId = 1 models.PostGroup(&group)