Added initial functionality to allow arbitrary events

pull/1929/head
Glenn Wilkinson 2020-08-07 20:51:25 +01:00
parent b684fb4ebd
commit c8abed4896
4 changed files with 1158 additions and 980 deletions

View File

@ -112,6 +112,7 @@ func (ps *PhishingServer) registerRoutes() {
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
router.HandleFunc("/track", ps.TrackHandler)
router.HandleFunc("/robots.txt", ps.RobotsHandler)
router.HandleFunc("/arbevent", ps.ArbitraryEventHandler)
router.HandleFunc("/{path:.*}/track", ps.TrackHandler)
router.HandleFunc("/{path:.*}/report", ps.ReportHandler)
router.HandleFunc("/report", ps.ReportHandler)
@ -126,6 +127,32 @@ func (ps *PhishingServer) registerRoutes() {
ps.server.Handler = phishHandler
}
// ArbitraryEventHandler deals with arbitrary events - for example opening Word documents, secondary links, etc
func (ps *PhishingServer) ArbitraryEventHandler(w http.ResponseWriter, r *http.Request) {
r, err := setupContext(r)
if err != nil {
// Log the error if it wasn't something we can safely ignore
if err != ErrInvalidRequest && err != ErrCampaignComplete {
log.Error(err)
}
http.NotFound(w, r)
return
}
rs := ctx.Get(r, "result").(models.Result)
d := ctx.Get(r, "details").(models.EventDetails)
err = rs.HandleArbitraryEvent(d)
if err != nil {
log.Error(err)
w.Write([]byte(err.Error()))
} else {
w.Write([]byte("Event received"))
}
}
// TrackHandler tracks emails as they are opened, updating the status for the given Result
func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) {
r, err := setupContext(r)

View File

@ -40,25 +40,26 @@ const InitialAdminPassword = "GOPHISH_INITIAL_ADMIN_PASSWORD"
const InitialAdminApiToken = "GOPHISH_INITIAL_ADMIN_API_TOKEN"
const (
CampaignInProgress string = "In progress"
CampaignQueued string = "Queued"
CampaignCreated string = "Created"
CampaignEmailsSent string = "Emails Sent"
CampaignComplete string = "Completed"
EventSent string = "Email Sent"
EventSendingError string = "Error Sending Email"
EventOpened string = "Email Opened"
EventClicked string = "Clicked Link"
EventDataSubmit string = "Submitted Data"
EventReported string = "Email Reported"
EventProxyRequest string = "Proxied request"
StatusSuccess string = "Success"
StatusQueued string = "Queued"
StatusSending string = "Sending"
StatusUnknown string = "Unknown"
StatusScheduled string = "Scheduled"
StatusRetry string = "Retrying"
Error string = "Error"
CampaignInProgress string = "In progress"
CampaignQueued string = "Queued"
CampaignCreated string = "Created"
CampaignEmailsSent string = "Emails Sent"
CampaignComplete string = "Completed"
EventSent string = "Email Sent"
EventSendingError string = "Error Sending Email"
EventOpened string = "Email Opened"
EventClicked string = "Clicked Link"
EventDataSubmit string = "Submitted Data"
EventReported string = "Email Reported"
EventArbitraryEvent string = "Arbitrary Event"
EventProxyRequest string = "Proxied request"
StatusSuccess string = "Success"
StatusQueued string = "Queued"
StatusSending string = "Sending"
StatusUnknown string = "Unknown"
StatusScheduled string = "Scheduled"
StatusRetry string = "Retrying"
Error string = "Error"
)
// Flash is used to hold flash information for use in templates.

View File

@ -3,6 +3,7 @@ package models
import (
"crypto/rand"
"encoding/json"
"errors"
"math/big"
"net"
"time"
@ -135,6 +136,24 @@ func (r *Result) HandleFormSubmit(details EventDetails) error {
return db.Save(r).Error
}
// HandleArbitraryEvent updates a Result with an arbitrary event (e.g Word document opened, secondary link clicked)
func (r *Result) HandleArbitraryEvent(details EventDetails) error {
EventTitle := details.Payload.Get("title")
if EventTitle == "" {
return errors.New("No title supplied for arbitrary event")
}
event, err := r.createEvent(EventArbitraryEvent, details)
if err != nil {
return err
}
r.Status = EventTitle
r.ModifiedDate = event.Time
return db.Save(r).Error
}
// HandleEmailReport updates a Result in the case where they report a simulated
// phishing email using the HTTP handler.
func (r *Result) HandleEmailReport(details EventDetails) error {

View File

@ -1,8 +1,10 @@
console.log("Running campaign_results.min.js")
var map = null
var doPoll = true;
// statuses is a helper map to point result statuses to ui classes
var statuses = {
"Email Sent": {
color: "#1abc9c",
label: "label-success",
@ -344,6 +346,7 @@ var renderDevice = function (event_details) {
var browserIcon = 'info-circle'
var browserVersion = ''
if (ua.browser && ua.browser.name) {
deviceBrowser = ua.browser.name
// Handle the "mobile safari" case
@ -365,6 +368,7 @@ var renderDevice = function (event_details) {
}
function renderTimeline(data) {
record = {
"id": data[0],
"first_name": data[2],
@ -383,50 +387,99 @@ function renderTimeline(data) {
$.each(campaign.timeline, function (i, event) {
if (!event.email || event.email == record.email) {
// Add the event
results += '<div class="timeline-entry">' +
' <div class="timeline-bar"></div>'
results +=
' <div class="timeline-icon ' + statuses[event.message].label + '">' +
' <i class="fa ' + statuses[event.message].icon + '"></i></div>' +
' <div class="timeline-message">' + escapeHtml(event.message) +
' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>'
if (event.details) {
details = JSON.parse(event.details)
if (event.message == "Clicked Link" || event.message == "Submitted Data") {
deviceView = renderDevice(details)
if (deviceView) {
results += deviceView
// Handle arbitrary event as a special case
// We could collapse the first half into the regular code, but for now it feels neater to keep it separate and live with the code re-use (*waves @ jordan*)
if (event.message == "Arbitrary Event"){
if (event.details) { // Should always be data, otherwise we can ignore the event
details = JSON.parse(event.details)
message = details.payload.title
results += '<div class="timeline-entry">' +
' <div class="timeline-bar"></div>'
results +=
' <div class="timeline-icon ' + statuses[message].label+ '">' +
' <i class="fa ' + statuses[message].icon + '"></i></div>' +
' <div class="timeline-message">' + escapeHtml(message) + // This is the case that makes code reuse tricky, as we want the title of the arbitrary event from the payload. TODO Give some more thought. Perhaps we scrap 'Arbitrary Event' and put the title in the message, and have some other indicator of the requirement to uniquely parse the contents of 'payload' e.g. payload['ae'] = 1
' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>'
// Check if user agent present && requested to display it
if ("browser" in details && details.payload.ua == 1){
deviceView = renderDevice(details)
if (deviceView) {
results += deviceView
}
}
//Iterate over sub-items
if ("sub_text" in details.payload) {
results += '<div class="timeline-device-details">'
details.payload.sub_text.forEach(function (text, index) {
results += "<div>"
// Check if there's an associated icon
if ("sub_icon" in details.payload && details.payload.sub_icon.length >= index){
results = results + '<span class="' + details.payload.sub_icon[index] + '"></span> ' //+ text
}
results += text
results += "</div>"
})
results += '</div>'
}
results += '</div></div>'
} // End arbitrary event processsing
} else { // else, if regular event
results += '<div class="timeline-entry">' +
' <div class="timeline-bar"></div>'
results +=
' <div class="timeline-icon ' + statuses[event.message].label + '">' +
' <i class="fa ' + statuses[event.message].icon + '"></i></div>' +
' <div class="timeline-message">' + escapeHtml(event.message) +
' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>'
if (event.details) {
details = JSON.parse(event.details)
if (event.message == "Clicked Link" || event.message == "Submitted Data" || event.message == "Email Opened") {
deviceView = renderDevice(details)
if (deviceView) {
results += deviceView
}
}
if (event.message == "Submitted Data") {
results += '<div class="timeline-replay-button"><button onclick="replay(' + i + ')" class="btn btn-success">'
results += '<i class="fa fa-refresh"></i> Replay Credentials</button></div>'
results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>'
}
if (details.payload) {
results += '<div class="timeline-event-results">'
results += ' <table class="table table-condensed table-bordered table-striped">'
results += ' <thead><tr><th>Parameter</th><th>Value(s)</tr></thead><tbody>'
$.each(Object.keys(details.payload), function (i, param) {
if (param == "rid") {
return true;
}
results += ' <tr>'
results += ' <td>' + escapeHtml(param) + '</td>'
results += ' <td>' + escapeHtml(details.payload[param]) + '</td>'
results += ' </tr>'
})
results += ' </tbody></table>'
results += '</div>'
}
if (details.error) {
results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>'
results += '<div class="timeline-event-results">'
results += '<span class="label label-default">Error</span> ' + details.error
results += '</div>'
}
}
if (event.message == "Submitted Data") {
results += '<div class="timeline-replay-button"><button onclick="replay(' + i + ')" class="btn btn-success">'
results += '<i class="fa fa-refresh"></i> Replay Credentials</button></div>'
results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>'
}
if (details.payload) {
results += '<div class="timeline-event-results">'
results += ' <table class="table table-condensed table-bordered table-striped">'
results += ' <thead><tr><th>Parameter</th><th>Value(s)</tr></thead><tbody>'
$.each(Object.keys(details.payload), function (i, param) {
if (param == "rid") {
return true;
}
results += ' <tr>'
results += ' <td>' + escapeHtml(param) + '</td>'
results += ' <td>' + escapeHtml(details.payload[param]) + '</td>'
results += ' </tr>'
})
results += ' </tbody></table>'
results += '</div>'
}
if (details.error) {
results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>'
results += '<div class="timeline-event-results">'
results += '<span class="label label-default">Error</span> ' + details.error
results += '</div>'
}
results += '</div></div>'
}
results += '</div></div>'
}
})
// Add the scheduled send event at the bottom
@ -612,7 +665,13 @@ var updateMap = function (results) {
* @param {moment(datetime)} send_date
*/
function createStatusLabel(status, send_date) {
var label = statuses[status].label || "label-default";
if (status in statuses){
var label = statuses[status].label || "label-default";
} else {
var label = "label-default"
}
var statusColumn = "<span class=\"label " + label + "\">" + status + "</span>"
// Add the tooltip if the email is scheduled to be sent
if (status == "Scheduled" || status == "Retrying") {
@ -637,14 +696,25 @@ function poll() {
/* Update the timeline */
var timeline_series_data = []
$.each(campaign.timeline, function (i, event) {
// Handle arbitary event
if (event.message == "Arbitrary Event") {
details = JSON.parse(event.details)
message = details.payload.title
} else {
message = event.message
//color = statuses[event.message].color
}
var event_date = moment.utc(event.time).local()
timeline_series_data.push({
email: event.email,
message: event.message,
message: message, //event.message,
x: event_date.valueOf(),
y: 1,
marker: {
fillColor: statuses[event.message].color
fillColor: statuses[message].color //statuses[event.message].color
}
})
})
@ -700,6 +770,7 @@ function poll() {
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("#caret").removeClass("fa-caret-right")
@ -720,12 +791,59 @@ function poll() {
}
function load() {
campaign.id = window.location.pathname.split('/').slice(-1)[0]
var use_map = JSON.parse(localStorage.getItem('gophish.use_map'))
api.campaignId.results(campaign.id)
.success(function (c) {
campaign = c
if (campaign) {
// We add arbitrary events to the statuses dict and arbitrary event names the progressListing array.
tmpTitles = {}
campaign.timeline.forEach(function(event) {
if (event.message == "Arbitrary Event") {
details = JSON.parse(event.details) // TODO Validate this exists
title = "Arbitrary Event"
if ("title" in details.payload){
title = String(details.payload.title)
}
statuses[title] = {"arbitrary event" : 1} // Set true to be arbitrary event, just so we can discern if we need to
statuses[title]["color"] = "#00FFFF" // Default
if ("color" in details.payload ){
color = String(details.payload.color)
if (!(/^#[0-9A-F]{6}$/i.test(color))) {
color = "#00FFFF" // Default to Cyan if the color is invalid
}
statuses[title]["color"] = color
}
statuses[title]["icon"] = "fa fa-info" // Default
if ("icon" in details.payload ){
icon = String(details.payload.icon)
statuses[title]["icon"] = icon
}
statuses[title]["label"] = "label-info" // Default
if ("label" in details.payload ){
label = String(details.payload.label)
statuses[title]["label"] = label
}
// Store unique event titles to be added to the progressListing once we exit the loop
tmpTitles[title] = 1
}
})
// We add the arbitary event titles to the progress listing
// Small problem here is that the ordering will assume anything appended is more serious than 'data submitted'
progressListing = progressListing.concat(Object.keys(tmpTitles))
$("title").text(c.name + " - Gophish")
$("#loading").hide()
$("#campaignResults").show()
@ -794,6 +912,7 @@ function load() {
email_series_data[k] = 0
});
$.each(campaign.results, function (i, result) {
resultsTable.row.add([
result.id,
"<i id=\"caret\" class=\"fa fa-caret-right\"></i>",
@ -842,15 +961,25 @@ function load() {
return true
}
var event_date = moment.utc(event.time).local()
// Handle arbitary event
if (event.message == "Arbitrary Event") {
details = JSON.parse(event.details)
message = details.payload.title
} else {
message = event.message
}
timeline_series_data.push({
email: event.email,
message: event.message,
message: message, //event.message,
x: event_date.valueOf(),
y: 1,
marker: {
fillColor: statuses[event.message].color
fillColor: statuses[message].color //statuses[event.message].color
}
})
})
renderTimelineChart({
data: timeline_series_data
@ -869,6 +998,7 @@ function load() {
name: '',
y: 100 - Math.floor((count / campaign.results.length) * 100)
})
var chart = renderPieChart({
elemId: statusMapping[status] + '_chart',
title: status,
@ -876,6 +1006,7 @@ function load() {
data: email_data,
colors: [statuses[status].color, '#dddddd']
})
})
if (use_map) {