Adding "Report Email" Support (#1014)

Adds the capability to report phishing campaigns using an email client extension.

**Note: Gophish does not currently provide an email client extension out of the box. This is simply a mechanism to let existing email client add-ons send report status information to Gophish, and have that information reflected in the dashboard.**
This commit is contained in:
Jordan Wright 2018-03-18 22:03:00 -05:00 committed by GitHub
parent 709e83bade
commit f21536da7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 209 additions and 52 deletions

View file

@ -39,6 +39,8 @@ func CreatePhishingRouter() http.Handler {
router.HandleFunc("/track", PhishTracker)
router.HandleFunc("/robots.txt", RobotsHandler)
router.HandleFunc("/{path:.*}/track", PhishTracker)
router.HandleFunc("/{path:.*}/report", PhishReporter)
router.HandleFunc("/report", PhishReporter)
router.HandleFunc("/{path:.*}", PhishHandler)
return router
}
@ -71,6 +73,29 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "static/images/pixel.png")
}
// PhishReporter tracks emails as they are reported, updating the status for the given Result
func PhishReporter(w http.ResponseWriter, r *http.Request) {
err, r := setupContext(r)
if err != nil {
// Log the error if it wasn't something we can safely ignore
if err != ErrInvalidRequest && err != ErrCampaignComplete {
Logger.Println(err)
}
http.NotFound(w, r)
return
}
rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign)
rj := ctx.Get(r, "details").([]byte)
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_REPORTED, Details: string(rj)})
err = rs.UpdateReported(true)
if err != nil {
Logger.Println(err)
}
w.WriteHeader(http.StatusNoContent)
}
// PhishHandler handles incoming client connections and registers the associated actions performed
// (such as clicked link, etc.)
func PhishHandler(w http.ResponseWriter, r *http.Request) {

View file

@ -26,6 +26,12 @@ func (s *ControllersSuite) openEmail(rid string) {
s.Equal(bytes.Compare(body, expected), 0)
}
func (s *ControllersSuite) reportedEmail(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.StatusNoContent)
}
func (s *ControllersSuite) openEmail404(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid))
s.Nil(err)
@ -63,6 +69,19 @@ func (s *ControllersSuite) TestOpenedPhishingEmail() {
s.Equal(result.Status, models.EVENT_OPENED)
}
func (s *ControllersSuite) TestReportedPhishingEmail() {
campaign := s.getFirstCampaign()
result := campaign.Results[0]
s.Equal(result.Status, models.STATUS_SENDING)
s.reportedEmail(result.RId)
campaign = s.getFirstCampaign()
result = campaign.Results[0]
s.Equal(result.Reported, true)
s.Equal(campaign.Events[len(campaign.Events)-1].Message, models.EVENT_REPORTED)
}
func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
campaign := s.getFirstCampaign()
result := campaign.Results[0]

View file

@ -0,0 +1,8 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE results ADD COLUMN reported boolean default 0;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View file

@ -0,0 +1,8 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE results ADD COLUMN reported boolean default 0;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View file

@ -33,6 +33,7 @@ type CampaignResults struct {
Id int64 `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Reported string `json:"reported"`
Results []Result `json:"results, omitempty"`
Events []Event `json:"timeline,omitempty"`
}
@ -61,6 +62,7 @@ type CampaignStats struct {
OpenedEmail int64 `json:"opened"`
ClickedLink int64 `json:"clicked"`
SubmittedData int64 `json:"submitted_data"`
EmailReported int64 `json:"email_reported"`
Error int64 `json:"error"`
}
@ -194,6 +196,10 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
if err != nil {
return s, err
}
query.Where("reported=?", true).Count(&s.EmailReported)
if err != nil {
return s, err
}
// Every submitted data event implies they clicked the link
s.ClickedLink += s.SubmittedData
err = query.Where("status=?", EVENT_OPENED).Count(&s.OpenedEmail).Error
@ -426,6 +432,7 @@ func PostCampaign(c *Campaign, uid int64) error {
FirstName: t.FirstName,
LastName: t.LastName,
SendDate: c.LaunchDate,
Reported: false,
}
if c.Status == CAMPAIGN_IN_PROGRESS {
r.Status = STATUS_SENDING

View file

@ -32,6 +32,7 @@ const (
EVENT_OPENED string = "Email Opened"
EVENT_CLICKED string = "Clicked Link"
EVENT_DATA_SUBMIT string = "Submitted Data"
EVENT_REPORTED string = "Email Reported"
EVENT_PROXY_REQUEST string = "Proxied request"
STATUS_SUCCESS string = "Success"
STATUS_QUEUED string = "Queued"

View file

@ -38,6 +38,7 @@ type Result struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
SendDate time.Time `json:"send_date"`
Reported bool `json:"reported" sql:"not null"`
}
// UpdateStatus updates the status of the result in the database
@ -45,6 +46,11 @@ func (r *Result) UpdateStatus(s string) error {
return db.Table("results").Where("id=?", r.Id).Update("status", s).Error
}
// UpdateReported updates when a user reports a campaign
func (r *Result) UpdateReported(s bool) error {
return db.Table("results").Where("id=?", r.Id).Update("reported", s).Error
}
// UpdateGeo updates the latitude and longitude of the result in
// the database given an IP address
func (r *Result) UpdateGeo(addr string) error {

File diff suppressed because one or more lines are too long

7
static/css/main.css vendored
View file

@ -655,6 +655,11 @@ table.dataTable {
color: #f39c12;
}
.color-reported{
font-weight: bold;
color:#45d6ef;
}
.color-success {
color: #f05b4f;
}
@ -666,4 +671,4 @@ table.dataTable {
#resultsMapContainer {
display: none;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -27,7 +27,7 @@ var statuses = {
"Email Opened": {
color: "#f9bf3b",
label: "label-warning",
icon: "fa-envelope",
icon: "fa-envelope-open",
point: "ct-point-opened"
},
"Clicked Link": {
@ -42,6 +42,13 @@ var statuses = {
icon: "fa-exclamation",
point: "ct-point-clicked"
},
//not a status, but is used for the campaign timeline and user timeline
"Email Reported": {
color: "#45d6ef",
label: "label-info",
icon: "fa-bullhorn",
point: "ct-point-reported"
},
"Error": {
color: "#6c7a89",
label: "label-default",
@ -95,6 +102,7 @@ var statusMapping = {
"Email Opened": "opened",
"Clicked Link": "clicked",
"Submitted Data": "submitted_data",
"Email Reported": "reported",
}
// This is an underwhelming attempt at an enum
@ -282,7 +290,8 @@ function renderTimeline(data) {
"email": data[4],
"position": data[5],
"status": data[6],
"send_date": data[7]
"send_date": data[7],
"reported": data[8]
}
results = '<div class="timeline col-sm-12 well well-lg">' +
'<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
@ -571,6 +580,9 @@ function poll() {
});
$.each(campaign.results, function (i, result) {
email_series_data[result.status]++;
if (result.reported) {
email_series_data['Email Reported']++
}
// Backfill status values
var step = progressListing.indexOf(result.status)
for (var i = 0; i < step; i++) {
@ -595,6 +607,7 @@ function poll() {
data: email_data
})
})
/* Update the datatable */
resultsTable = $("#resultsTable").DataTable()
resultsTable.rows().every(function (i, tableLoop, rowLoop) {
@ -603,12 +616,13 @@ function poll() {
var rid = rowData[0]
$.each(campaign.results, function (j, result) {
if (result.id == rid) {
rowData[7] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
rowData[7] = result.reported
rowData[6] = result.status
resultsTable.row(i).data(rowData)
if (row.child.isShown()) {
$(row.node()).find("i").removeClass("fa-caret-right")
$(row.node()).find("i").addClass("fa-caret-down")
$(row.node()).find("#caret").removeClass("fa-caret-right")
$(row.node()).find("#caret").addClass("fa-caret-down")
row.child(renderTimeline(row.data()))
}
return false
@ -669,13 +683,24 @@ function load() {
"targets": [1]
}, {
"visible": false,
"targets": [0, 7]
"targets": [0, 8]
},
{
"render": function (data, type, row) {
return createStatusLabel(data, row[7])
return createStatusLabel(data, row[8])
},
"targets": [6]
},
{
className: "text-center",
"render": function (reported, type, row) {
if (reported) {
return "<i class='fa fa-check-circle text-center text-success'></i>"
} else {
return "<i class='fa fa-times-circle text-center text-danger'></i>"
}
},
"targets": [7]
}
]
});
@ -688,15 +713,19 @@ function load() {
$.each(campaign.results, function (i, result) {
resultsTable.row.add([
result.id,
"<i class=\"fa fa-caret-right\"></i>",
"<i id=\"caret\" class=\"fa fa-caret-right\"></i>",
escapeHtml(result.first_name) || "",
escapeHtml(result.last_name) || "",
escapeHtml(result.email) || "",
escapeHtml(result.position) || "",
result.status,
result.reported,
moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
])
email_series_data[result.status]++;
if (result.reported) {
email_series_data['Email Reported']++
}
// Backfill status values
var step = progressListing.indexOf(result.status)
for (var i = 0; i < step; i++) {
@ -761,6 +790,7 @@ function load() {
colors: [statuses[status].color, '#dddddd']
})
})
if (use_map) {
$("#resultsMapContainer").show()
map = new Datamap({

View file

@ -324,7 +324,7 @@ $(document).ready(function () {
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
} else {
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "Reported : " + campaign.stats.reported
}
campaignTable.row.add([
@ -366,4 +366,4 @@ $(document).ready(function () {
return 0;
});
})
})
})

View file

@ -1,5 +1,4 @@
var campaigns = []
// statuses is a helper map to point result statuses to ui classes
var statuses = {
"Email Sent": {
@ -29,6 +28,12 @@ var statuses = {
icon: "fa-envelope",
point: "ct-point-opened"
},
"Email Reported": {
color: "#45d6ef",
label: "label-warning",
icon: "fa-bullhorne",
point: "ct-point-reported"
},
"Clicked Link": {
color: "#F39C12",
label: "label-clicked",
@ -80,6 +85,7 @@ var statuses = {
var statsMapping = {
"sent": "Email Sent",
"opened": "Email Opened",
"email_reported": "Email Reported",
"clicked": "Clicked Link",
"submitted_data": "Submitted Data",
}
@ -107,16 +113,18 @@ function renderPieChart(chartopts) {
left = chart.plotLeft + pie.center[0],
top = chart.plotTop + pie.center[1];
this.innerText = rend.text(chartopts['data'][0].count, left, top).
attr({
'text-anchor': 'middle',
'font-size': '24px',
'font-weight': 'bold',
'fill': chartopts['colors'][0],
'font-family': 'Helvetica,Arial,sans-serif'
}).add();
attr({
'text-anchor': 'middle',
'font-size': '16px',
'font-weight': 'bold',
'fill': chartopts['colors'][0],
'font-family': 'Helvetica,Arial,sans-serif'
}).add();
},
render: function () {
this.innerText.attr({ text: chartopts['data'][0].count })
this.innerText.attr({
text: chartopts['data'][0].count
})
}
}
},
@ -190,6 +198,7 @@ function generateStatsPieCharts(campaigns) {
data: stats_data,
colors: [statuses[status_label].color, "#dddddd"]
})
stats_data = []
});
}
@ -289,13 +298,30 @@ $(document).ready(function () {
// Create the overview chart data
campaignTable = $("#campaignTable").DataTable({
columnDefs: [{
orderable: false,
targets: "no-sort"
},
{ className: "color-sent", targets: [2] },
{ className: "color-opened", targets: [3] },
{ className: "color-clicked", targets: [4] },
{ className: "color-success", targets: [5] }],
orderable: false,
targets: "no-sort"
},
{
className: "color-sent",
targets: [2]
},
{
className: "color-opened",
targets: [3]
},
{
className: "color-clicked",
targets: [4]
},
{
className: "color-success",
targets: [5]
},
{
className: "color-reported",
targets: [6]
}
],
order: [
[1, "desc"]
]
@ -310,7 +336,7 @@ $(document).ready(function () {
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
} else {
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "<br><br>" + "Reported : " + campaign.stats.email_reported
}
// Add it to the table
campaignTable.row.add([
@ -320,6 +346,7 @@ $(document).ready(function () {
campaign.stats.opened,
campaign.stats.clicked,
campaign.stats.submitted_data,
campaign.stats.email_reported,
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
<i class='fa fa-bar-chart'></i>\
@ -340,4 +367,4 @@ $(document).ready(function () {
.error(function () {
errorFlash("Error fetching campaigns")
})
})
})

View file

@ -3,26 +3,35 @@
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li><a href="/">Dashboard</a>
<li>
<a href="/">Dashboard</a>
</li>
<li class="active"><a href="/campaigns">Campaigns</a>
<li class="active">
<a href="/campaigns">Campaigns</a>
</li>
<li><a href="/users">Users &amp; Groups</a>
<li>
<a href="/users">Users &amp; Groups</a>
</li>
<li><a href="/templates">Email Templates</a>
<li>
<a href="/templates">Email Templates</a>
</li>
<li><a href="/landing_pages">Landing Pages</a>
<li>
<a href="/landing_pages">Landing Pages</a>
</li>
<li><a href="/sending_profiles">Sending Profiles</a>
<li>
<a href="/sending_profiles">Sending Profiles</a>
</li>
<li><a href="/settings">Settings</a>
<li>
<a href="/settings">Settings</a>
</li>
<li>
<hr>
</li>
<li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
<li>
<a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
</li>
<li><a href="/api/">API Documentation</a>
<li>
<a href="/api/">API Documentation</a>
</li>
</ul>
</div>
@ -47,8 +56,12 @@
<i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="exportButton">
<li><a href="#" onclick="exportAsCSV('results')">Results</a></li>
<li><a href="#" onclick="exportAsCSV('events')">Raw Events</a></li>
<li>
<a href="#" onclick="exportAsCSV('results')">Results</a>
</li>
<li>
<a href="#" onclick="exportAsCSV('events')">Raw Events</a>
</li>
</ul>
</div>
<button id="complete_button" type="button" class="btn btn-blue" data-toggle="tooltip" onclick="completeCampaign()">
@ -73,10 +86,13 @@
</div>
</div>
<div class="row">
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
<div id="sent_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="opened_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="clicked_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="reported_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
</div>
<div class="row" id="resultsMapContainer">
<div class="col-md-6">
@ -99,6 +115,7 @@
<th>Email</th>
<th>Position</th>
<th>Status</th>
<th class="text-center">Reported</th>
</tr>
</thead>
<tbody>

View file

@ -43,10 +43,13 @@
<div id="overview_chart" style="height:200px;" class="col-lg-12 col-md-12 col-sm-12 col-xs-12"></div>
</div>
<div class="row">
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
<div id="sent_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="opened_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="clicked_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div id="email_reported_chart" style="height:200px;" class="col-lg-2 col-md-2"></div>
<div style="height:200px;" class="col-lg-1 col-md-1"></div>
</div>
<div class="row">
<h2>Recent Campaigns</h2>
@ -65,6 +68,7 @@
<th class="col-md-1 col-sm-1"><i class="fa fa-envelope-open-o"></i></th>
<th class="col-md-1 col-sm-1"><i class="fa fa-mouse-pointer"></i></th>
<th class="col-md-1 col-sm-1"><i class="fa fa-exclamation-circle"></i></th>
<th class="col-md-1 col-sm-1"><i class="fa fa-bullhorn"></i></th>
<th class="col-md-1 col-sm-1">Status</th>
<th class="col-md-2 col-sm-2 no-sort"></i></th>
</tr>