diff --git a/controllers/phish.go b/controllers/phish.go index 4d5a2d99..5a3a934e 100644 --- a/controllers/phish.go +++ b/controllers/phish.go @@ -6,7 +6,9 @@ import ( "net" "net/http" "strings" + "time" + "github.com/gophish/gophish/config" ctx "github.com/gophish/gophish/context" log "github.com/gophish/gophish/logger" "github.com/gophish/gophish/models" @@ -21,6 +23,21 @@ var ErrInvalidRequest = errors.New("Invalid request") // has already been marked as complete. var ErrCampaignComplete = errors.New("Event received on completed campaign") +// TransparencyResponse is the JSON response provided when a third-party +// makes a request to the transparency handler. +type TransparencyResponse struct { + Server string `json:"server"` + ContactAddress string `json:"contact_address"` + SendDate time.Time `json:"send_date"` +} + +// TransparencySuffix (when appended to a valid result ID), will cause Gophish +// to return a transparency response. +const TransparencySuffix = "+" + +// ServerName is the server type that is returned in the transparency response. +const ServerName = "gophish" + // CreatePhishingRouter creates the router that handles phishing connections. func CreatePhishingRouter() http.Handler { router := mux.NewRouter() @@ -52,7 +69,15 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) { return } rs := ctx.Get(r, "result").(models.Result) + rid := ctx.Get(r, "rid").(string) d := ctx.Get(r, "details").(models.EventDetails) + + // Check for a transparency request + if strings.HasSuffix(rid, TransparencySuffix) { + TransparencyHandler(w, r) + return + } + err = rs.HandleEmailOpened(d) if err != nil { log.Error(err) @@ -77,8 +102,15 @@ func PhishReporter(w http.ResponseWriter, r *http.Request) { return } rs := ctx.Get(r, "result").(models.Result) + rid := ctx.Get(r, "rid").(string) d := ctx.Get(r, "details").(models.EventDetails) + // Check for a transparency request + if strings.HasSuffix(rid, TransparencySuffix) { + TransparencyHandler(w, r) + return + } + err = rs.HandleEmailReport(d) if err != nil { log.Error(err) @@ -117,8 +149,16 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) { return } rs := ctx.Get(r, "result").(models.Result) + rid := ctx.Get(r, "rid").(string) c := ctx.Get(r, "campaign").(models.Campaign) d := ctx.Get(r, "details").(models.EventDetails) + + // Check for a transparency request + if strings.HasSuffix(rid, TransparencySuffix) { + TransparencyHandler(w, r) + return + } + p, err := models.GetPage(c.PageId, c.UserId) if err != nil { log.Error(err) @@ -172,6 +212,18 @@ func RobotsHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "User-agent: *\nDisallow: /") } +// TransparencyHandler returns a TransparencyResponse for the provided result +// and campaign. +func TransparencyHandler(w http.ResponseWriter, r *http.Request) { + rs := ctx.Get(r, "result").(models.Result) + tr := &TransparencyResponse{ + Server: ServerName, + SendDate: rs.SendDate, + ContactAddress: config.Conf.ContactAddress, + } + JSONResponse(w, tr, http.StatusOK) +} + // setupContext handles some of the administrative work around receiving a new request, such as checking the result ID, the campaign, etc. func setupContext(r *http.Request) (error, *http.Request) { err := r.ParseForm() @@ -179,10 +231,24 @@ func setupContext(r *http.Request) (error, *http.Request) { log.Error(err) return err, r } - id := r.Form.Get(models.RecipientParameter) - if id == "" { + rid := r.Form.Get(models.RecipientParameter) + if rid == "" { return ErrInvalidRequest, r } + // Since we want to support the common case of adding a "+" to indicate a + // transparency request, we need to take care to handle the case where the + // request ends with a space, since a "+" is technically reserved for use + // as a URL encoding of a space. + if strings.HasSuffix(rid, " ") { + // We'll trim off the space + rid = strings.TrimRight(rid, " ") + // Then we'll add the transparency suffix + rid = fmt.Sprintf("%s%s", rid, TransparencySuffix) + } + // Finally, if this is a transparency request, we'll need to verify that + // a valid rid has been provided, so we'll look up the result with a + // trimmed parameter. + id := strings.TrimSuffix(rid, TransparencySuffix) // Check to see if this is a preview or a real result if strings.HasPrefix(id, models.PreviewPrefix) { rs, err := models.GetEmailRequestByResultId(id) @@ -226,6 +292,7 @@ func setupContext(r *http.Request) (error, *http.Request) { d.Browser["address"] = ip d.Browser["user-agent"] = r.Header.Get("User-Agent") + r = ctx.Set(r, "rid", rid) r = ctx.Set(r, "result", rs) r = ctx.Set(r, "campaign", c) r = ctx.Set(r, "details", d) diff --git a/controllers/phish_test.go b/controllers/phish_test.go index 02288708..21855412 100644 --- a/controllers/phish_test.go +++ b/controllers/phish_test.go @@ -2,11 +2,13 @@ package controllers import ( "bytes" + "encoding/json" "fmt" "io/ioutil" "log" "net/http" + "github.com/gophish/gophish/config" "github.com/gophish/gophish/models" ) @@ -50,6 +52,12 @@ func (s *ControllersSuite) reportedEmail(rid string) { s.Equal(resp.StatusCode, http.StatusNoContent) } +func (s *ControllersSuite) reportEmail404(rid string) { + resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", ps.URL, models.RecipientParameter, rid)) + s.Nil(err) + s.Equal(resp.StatusCode, http.StatusNotFound) +} + func (s *ControllersSuite) openEmail404(rid string) { resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid)) s.Nil(err) @@ -76,6 +84,19 @@ func (s *ControllersSuite) clickLink404(rid string) { s.Equal(resp.StatusCode, http.StatusNotFound) } +func (s *ControllersSuite) transparencyRequest(r models.Result, rid, path string) { + resp, err := http.Get(fmt.Sprintf("%s%s?%s=%s", ps.URL, path, models.RecipientParameter, rid)) + s.Nil(err) + defer resp.Body.Close() + s.Equal(resp.StatusCode, http.StatusOK) + tr := &TransparencyResponse{} + err = json.NewDecoder(resp.Body).Decode(tr) + s.Nil(err) + s.Equal(tr.ContactAddress, config.Conf.ContactAddress) + s.Equal(tr.SendDate, r.SendDate) + s.Equal(tr.Server, ServerName) +} + func (s *ControllersSuite) TestOpenedPhishingEmail() { campaign := s.getFirstCampaign() result := campaign.Results[0] @@ -134,13 +155,9 @@ func (s *ControllersSuite) TestNoRecipientID() { func (s *ControllersSuite) TestInvalidRecipientID() { rid := "XXXXXXXXXX" - resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid)) - s.Nil(err) - s.Equal(resp.StatusCode, http.StatusNotFound) - - resp, err = http.Get(fmt.Sprintf("%s/?%s=%s", ps.URL, models.RecipientParameter, rid)) - s.Nil(err) - s.Equal(resp.StatusCode, http.StatusNotFound) + s.openEmail404(rid) + s.clickLink404(rid) + s.reportEmail404(rid) } func (s *ControllersSuite) TestCompletedCampaignClick() { @@ -177,6 +194,7 @@ func (s *ControllersSuite) TestInvalidPreviewID() { bogusRId := fmt.Sprintf("%sbogus", models.PreviewPrefix) s.openEmail404(bogusRId) s.clickLink404(bogusRId) + s.reportEmail404(bogusRId) } func (s *ControllersSuite) TestPreviewTrack() { @@ -188,3 +206,25 @@ func (s *ControllersSuite) TestPreviewClick() { req := s.getFirstEmailRequest() s.clickLink(req.RId, req.Page.HTML) } + +func (s *ControllersSuite) TestInvalidTransparencyRequest() { + bogusRId := fmt.Sprintf("bogus%s", TransparencySuffix) + s.openEmail404(bogusRId) + s.clickLink404(bogusRId) + s.reportEmail404(bogusRId) +} + +func (s *ControllersSuite) TestTransparencyRequest() { + campaign := s.getFirstCampaign() + result := campaign.Results[0] + rid := fmt.Sprintf("%s%s", result.RId, TransparencySuffix) + s.transparencyRequest(result, rid, "/") + s.transparencyRequest(result, rid, "/track") + s.transparencyRequest(result, rid, "/report") + + // And check with the URL encoded version of a + + rid = fmt.Sprintf("%s%s", result.RId, "%2b") + s.transparencyRequest(result, rid, "/") + s.transparencyRequest(result, rid, "/track") + s.transparencyRequest(result, rid, "/report") +} diff --git a/models/maillog_test.go b/models/maillog_test.go index 4cadb048..6edba882 100644 --- a/models/maillog_test.go +++ b/models/maillog_test.go @@ -155,6 +155,7 @@ func (s *ModelsSuite) TestMailLogSuccess(ch *check.C) { Time: gotEvent.Time, } ch.Assert(gotEvent, check.DeepEquals, expectedEvent) + ch.Assert(result.SendDate, check.Equals, gotEvent.Time) ms, err := GetMailLogsByCampaign(campaign.Id) ch.Assert(err, check.Equals, nil) diff --git a/models/result.go b/models/result.go index b18efc26..988ddffd 100644 --- a/models/result.go +++ b/models/result.go @@ -62,6 +62,7 @@ func (r *Result) HandleEmailSent() error { if err != nil { return err } + r.SendDate = event.Time r.Status = EVENT_SENT r.ModifiedDate = event.Time return db.Save(r).Error