Adding support for custom headers in sending profiles (#544)

Closes #215 
Closes #128
pull/535/merge
Jordan Wright 2017-02-19 18:43:08 -06:00 committed by GitHub
parent dbadac3eca
commit 66c4be3d4f
12 changed files with 429 additions and 151 deletions

View File

@ -0,0 +1,12 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE IF NOT EXISTS headers(
id integer primary key auto_increment,
`key` varchar(255),
`value` varchar(255),
`smtp_id` bigint
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE headers;

View File

@ -0,0 +1,12 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE IF NOT EXISTS headers(
id integer primary key autoincrement,
key varchar(255),
value varchar(255),
"smtp_id" bigint
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE headers;

View File

@ -189,6 +189,11 @@ func (c *Campaign) getDetails() error {
c.SMTP = SMTP{Name: "[Deleted]"} c.SMTP = SMTP{Name: "[Deleted]"}
Logger.Printf("%s: sending profile not found for campaign\n", err) Logger.Printf("%s: sending profile not found for campaign\n", err)
} }
err = db.Where("smtp_id=?", c.SMTP.Id).Find(&c.SMTP.Headers).Error
if err != nil && err != gorm.ErrRecordNotFound {
Logger.Println(err)
return err
}
return nil return nil
} }

View File

@ -246,6 +246,24 @@ func (s *ModelsSuite) TestPostSMTPNoFrom(c *check.C) {
c.Assert(err, check.Equals, ErrFromAddressNotSpecified) c.Assert(err, check.Equals, ErrFromAddressNotSpecified)
} }
func (s *ModelsSuite) TestPostSMTPValidHeader(c *check.C) {
smtp := SMTP{
Name: "Test SMTP",
Host: "1.1.1.1:25",
FromAddress: "Foo Bar <foo@example.com>",
UserId: 1,
Headers: []Header{
Header{Key: "Reply-To", Value: "test@example.com"},
Header{Key: "X-Mailer", Value: "gophish"},
},
}
err = PostSMTP(&smtp)
c.Assert(err, check.Equals, nil)
ss, err := GetSMTPs(1)
c.Assert(err, check.Equals, nil)
c.Assert(len(ss), check.Equals, 1)
}
func (s *ModelsSuite) TestPostPage(c *check.C) { func (s *ModelsSuite) TestPostPage(c *check.C) {
html := `<html> html := `<html>
<head></head> <head></head>

View File

@ -6,6 +6,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/jinzhu/gorm"
) )
// SMTP contains the attributes needed to handle the sending of campaign emails // SMTP contains the attributes needed to handle the sending of campaign emails
@ -19,9 +21,19 @@ type SMTP struct {
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
FromAddress string `json:"from_address"` FromAddress string `json:"from_address"`
IgnoreCertErrors bool `json:"ignore_cert_errors"` IgnoreCertErrors bool `json:"ignore_cert_errors"`
Headers []Header `json:"headers"`
ModifiedDate time.Time `json:"modified_date"` ModifiedDate time.Time `json:"modified_date"`
} }
// Header contains the fields and methods for a sending profile to have
// custom headers
type Header struct {
Id int64 `json:"-"`
SMTPId int64 `json:"-"`
Key string `json:"key"`
Value string `json:"value"`
}
// ErrFromAddressNotSpecified is thrown when there is no "From" address // ErrFromAddressNotSpecified is thrown when there is no "From" address
// specified in the SMTP configuration // specified in the SMTP configuration
var ErrFromAddressNotSpecified = errors.New("No From Address specified") var ErrFromAddressNotSpecified = errors.New("No From Address specified")
@ -70,8 +82,16 @@ func GetSMTPs(uid int64) ([]SMTP, error) {
err := db.Where("user_id=?", uid).Find(&ss).Error err := db.Where("user_id=?", uid).Find(&ss).Error
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)
}
return ss, err return ss, err
}
for i, _ := range ss {
err = db.Where("smtp_id=?", ss[i].Id).Find(&ss[i].Headers).Error
if err != nil && err != gorm.ErrRecordNotFound {
Logger.Println(err)
return ss, err
}
}
return ss, nil
} }
// GetSMTP returns the SMTP, if it exists, specified by the given id and user_id. // GetSMTP returns the SMTP, if it exists, specified by the given id and user_id.
@ -81,6 +101,11 @@ func GetSMTP(id int64, uid int64) (SMTP, error) {
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)
} }
err = db.Where("smtp_id=?", s.Id).Find(&s.Headers).Error
if err != nil && err != gorm.ErrRecordNotFound {
Logger.Println(err)
return s, err
}
return s, err return s, err
} }
@ -90,6 +115,11 @@ func GetSMTPByName(n string, uid int64) (SMTP, error) {
err := db.Where("user_id=? and name=?", uid, n).Find(&s).Error err := db.Where("user_id=? and name=?", uid, n).Find(&s).Error
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)
return s, err
}
err = db.Where("smtp_id=?", s.Id).Find(&s.Headers).Error
if err != nil && err != gorm.ErrRecordNotFound {
Logger.Println(err)
} }
return s, err return s, err
} }
@ -106,6 +136,15 @@ func PostSMTP(s *SMTP) error {
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)
} }
// Save custom headers
for i, _ := range s.Headers {
s.Headers[i].SMTPId = s.Id
err := db.Save(&s.Headers[i]).Error
if err != nil {
Logger.Println(err)
return err
}
}
return err return err
} }
@ -121,12 +160,32 @@ func PutSMTP(s *SMTP) error {
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)
} }
// Delete all custom headers, and replace with new ones
err = db.Where("smtp_id=?", s.Id).Delete(&Header{}).Error
if err != nil && err != gorm.ErrRecordNotFound {
Logger.Println(err)
return err
}
for i, _ := range s.Headers {
s.Headers[i].SMTPId = s.Id
err := db.Save(&s.Headers[i]).Error
if err != nil {
Logger.Println(err)
return err
}
}
return err return err
} }
// DeleteSMTP deletes an existing SMTP in the database. // DeleteSMTP deletes an existing SMTP in the database.
// An error is returned if a SMTP with the given user id and SMTP id is not found. // An error is returned if a SMTP with the given user id and SMTP id is not found.
func DeleteSMTP(id int64, uid int64) error { func DeleteSMTP(id int64, uid int64) error {
// Delete all custom headers
err = db.Where("smtp_id=?", id).Delete(&Header{}).Error
if err != nil {
Logger.Println(err)
return err
}
err = db.Where("user_id=?", uid).Delete(SMTP{Id: id}).Error err = db.Where("user_id=?", uid).Delete(SMTP{Id: id}).Error
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)

View File

@ -64,6 +64,7 @@ func (t *Template) Validate() error {
if err != nil { if err != nil {
return err return err
} }
tmpl, err = template.New("text_template").Parse(t.Text) tmpl, err = template.New("text_template").Parse(t.Text)
if err != nil { if err != nil {
return err return err
@ -81,6 +82,7 @@ func GetTemplates(uid int64) ([]Template, error) {
return ts, err return ts, err
} }
for i, _ := range ts { for i, _ := range ts {
// Get Attachments
err = db.Where("template_id=?", ts[i].Id).Find(&ts[i].Attachments).Error err = db.Where("template_id=?", ts[i].Id).Find(&ts[i].Attachments).Error
if err == nil && len(ts[i].Attachments) == 0 { if err == nil && len(ts[i].Attachments) == 0 {
ts[i].Attachments = make([]Attachment, 0) ts[i].Attachments = make([]Attachment, 0)
@ -101,6 +103,8 @@ func GetTemplate(id int64, uid int64) (Template, error) {
Logger.Println(err) Logger.Println(err)
return t, err return t, err
} }
// Get Attachments
err = db.Where("template_id=?", t.Id).Find(&t.Attachments).Error err = db.Where("template_id=?", t.Id).Find(&t.Attachments).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
Logger.Println(err) Logger.Println(err)
@ -120,6 +124,8 @@ func GetTemplateByName(n string, uid int64) (Template, error) {
Logger.Println(err) Logger.Println(err)
return t, err return t, err
} }
// Get Attachments
err = db.Where("template_id=?", t.Id).Find(&t.Attachments).Error err = db.Where("template_id=?", t.Id).Find(&t.Attachments).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
Logger.Println(err) Logger.Println(err)
@ -142,6 +148,8 @@ func PostTemplate(t *Template) error {
Logger.Println(err) Logger.Println(err)
return err return err
} }
// Save every attachment
for i, _ := range t.Attachments { for i, _ := range t.Attachments {
Logger.Println(t.Attachments[i].Name) Logger.Println(t.Attachments[i].Name)
t.Attachments[i].TemplateId = t.Id t.Attachments[i].TemplateId = t.Id
@ -177,6 +185,8 @@ func PutTemplate(t *Template) error {
return err return err
} }
} }
// Save final template
err = db.Where("id=?", t.Id).Save(t).Error err = db.Where("id=?", t.Id).Save(t).Error
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)
@ -188,11 +198,14 @@ func PutTemplate(t *Template) error {
// DeleteTemplate deletes an existing template in the database. // DeleteTemplate deletes an existing template in the database.
// An error is returned if a template with the given user id and template id is not found. // An error is returned if a template with the given user id and template id is not found.
func DeleteTemplate(id int64, uid int64) error { func DeleteTemplate(id int64, uid int64) error {
// Delete attachments
err := db.Where("template_id=?", id).Delete(&Attachment{}).Error err := db.Where("template_id=?", id).Delete(&Attachment{}).Error
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)
return err return err
} }
// Finally, delete the template itself
err = db.Where("user_id=?", uid).Delete(Template{Id: id}).Error err = db.Where("user_id=?", uid).Delete(Template{Id: id}).Error
if err != nil { if err != nil {
Logger.Println(err) Logger.Println(err)

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,13 @@ var profiles = []
// Attempts to send a test email by POSTing to /campaigns/ // Attempts to send a test email by POSTing to /campaigns/
function sendTestEmail() { function sendTestEmail() {
var headers = [];
$.each($("#headersTable").DataTable().rows().data(), function(i, header) {
headers.push({
key: unescapeHtml(header[0]),
value: unescapeHtml(header[1]),
})
})
var test_email_request = { var test_email_request = {
template: {}, template: {},
first_name: $("input[name=to_first_name]").val(), first_name: $("input[name=to_first_name]").val(),
@ -14,7 +21,8 @@ function sendTestEmail() {
host: $("#host").val(), host: $("#host").val(),
username: $("#username").val(), username: $("#username").val(),
password: $("#password").val(), password: $("#password").val(),
ignore_cert_errors: $("#ignore_cert_errors").prop("checked") ignore_cert_errors: $("#ignore_cert_errors").prop("checked"),
headers: headers,
} }
} }
btnHtml = $("#sendTestModalSubmit").html() btnHtml = $("#sendTestModalSubmit").html()
@ -35,7 +43,15 @@ function sendTestEmail() {
// Save attempts to POST to /smtp/ // Save attempts to POST to /smtp/
function save(idx) { function save(idx) {
var profile = {} var profile = {
headers: []
}
$.each($("#headersTable").DataTable().rows().data(), function(i, header) {
profile.headers.push({
key: unescapeHtml(header[0]),
value: unescapeHtml(header[1]),
})
})
profile.name = $("#name").val() profile.name = $("#name").val()
profile.interface_type = $("#interface_type").val() profile.interface_type = $("#interface_type").val()
profile.from_address = $("#from").val() profile.from_address = $("#from").val()
@ -77,6 +93,7 @@ function dismiss() {
$("#username").val("") $("#username").val("")
$("#password").val("") $("#password").val("")
$("#ignore_cert_errors").prop("checked", true) $("#ignore_cert_errors").prop("checked", true)
$("#headersTable").dataTable().DataTable().clear().draw()
$("#modal").modal('hide') $("#modal").modal('hide')
} }
@ -91,6 +108,14 @@ function deleteProfile(idx) {
} }
function edit(idx) { function edit(idx) {
headers = $("#headersTable").dataTable({
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
})
$("#modalSubmit").unbind('click').click(function() { $("#modalSubmit").unbind('click').click(function() {
save(idx) save(idx)
}) })
@ -104,6 +129,9 @@ function edit(idx) {
$("#username").val(profile.username) $("#username").val(profile.username)
$("#password").val(profile.password) $("#password").val(profile.password)
$("#ignore_cert_errors").prop("checked", profile.ignore_cert_errors) $("#ignore_cert_errors").prop("checked", profile.ignore_cert_errors)
$.each(profile.headers, function(i, record) {
addCustomHeader(record.key, record.value)
});
} }
} }
@ -167,6 +195,34 @@ function load() {
}) })
} }
function addCustomHeader(header, value) {
// Create new data row.
var newRow = [
escapeHtml(header),
escapeHtml(value),
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
];
// Check table to see if header already exists.
var headersTable = headers.DataTable();
var existingRowIndex = headersTable
.column(0) // Email column has index of 2
.data()
.indexOf(escapeHtml(header));
// Update or add new row as necessary.
if (existingRowIndex >= 0) {
headersTable
.row(existingRowIndex, {
order: "index"
})
.data(newRow);
} else {
headersTable.row.add(newRow);
}
headersTable.draw();
}
$(document).ready(function() { $(document).ready(function() {
// Setup multiple modals // Setup multiple modals
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html // Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
@ -208,5 +264,26 @@ $(document).ready(function() {
$('#modal').on('hidden.bs.modal', function(event) { $('#modal').on('hidden.bs.modal', function(event) {
dismiss() dismiss()
}); });
// Code to deal with custom email headers
$("#headersForm").on('submit', function() {
headerKey = $("#headerKey").val();
headerValue = $("#headerValue").val();
if (headerKey == "" || headerValue == "") {
return false;
}
addCustomHeader(headerKey, headerValue);
// Reset user input.
$("#headersForm>div>input").val('');
$("#headerKey").focus();
return false;
});
// Handle Deletion
$("#headersTable").on("click", "span>i.fa-trash-o", function() {
headers.DataTable()
.row($(this).parents('tr'))
.remove()
.draw();
});
load() load()
}) })

View File

@ -42,6 +42,7 @@ function save(idx) {
type: target[4], type: target[4],
}) })
}) })
if (idx != -1) { if (idx != -1) {
template.id = templates[idx].id template.id = templates[idx].id
api.templateId.put(template) api.templateId.put(template)
@ -169,6 +170,7 @@ function edit(idx) {
} else { } else {
$("#use_tracker_checkbox").prop("checked", false) $("#use_tracker_checkbox").prop("checked", false)
} }
} }
// Handle Deletion // Handle Deletion
$("#attachmentsTable").unbind('click').on("click", "span>i.fa-trash-o", function() { $("#attachmentsTable").unbind('click').on("click", "span>i.fa-trash-o", function() {
@ -346,4 +348,5 @@ $(document).ready(function() {
dismiss() dismiss()
}); });
load() load()
}) })

View File

@ -86,8 +86,32 @@
<input id="ignore_cert_errors" type="checkbox" checked> <input id="ignore_cert_errors" type="checkbox" checked>
<label for="ignore_cert_errors">Ignore Certificate Errors <i class="fa fa-question-circle" data-toggle="tooltip" data-placement="right" title="Ignore common certificate errors such as self-signed certs (exposes you to MiTM attacks - use carefully!)"></i></label> <label for="ignore_cert_errors">Ignore Certificate Errors <i class="fa fa-question-circle" data-toggle="tooltip" data-placement="right" title="Ignore common certificate errors such as self-signed certs (exposes you to MiTM attacks - use carefully!)"></i></label>
</div> </div>
<label class="control-label" for="headersForm">Email Headers:</label>
<form id="headersForm">
<div class="col-md-4">
<input type="text" class="form-control" name="headerKey" id="headerKey" placeholder="X-Custom-Header">
</div>
<div class="col-md-4">
<input type="text" class="form-control" name="headerValue" id="headerValue" placeholder="{{"{{"}}.URL{{"}}"}}-gophish">
</div>
<div class="col-md-2">
<button class="btn btn-danger btn-headers" type="submit"><i class="fa fa-plus"></i> Add Custom Header</button>
</div>
</form>
<br />
<br />
<table id="headersTable" class="table table-hover table-striped table-condensed">
<thead>
<tr>
<th>Header</th>
<th>Value</th>
<th class="no-sort"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" data-toggle="modal" data-target="#sendTestEmailModal" class="btn btn-primary"><i class="fa fa-envelope"></i> Send Test Email</button> <button type="button" data-toggle="modal" data-target="#sendTestEmailModal" class="btn btn-primary"><i class="fa fa-envelope"></i> Send Test Email</button>
<!-- disable sendTestEmail functionality on sending profile page until update handling of /util/send_test_email -->
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -136,7 +160,5 @@
</div> </div>
{{end}} {{end}}
{{define "scripts"}} {{define "scripts"}}
<script src="/js/ckeditor/ckeditor.js"></script>
<script src="/js/ckeditor/adapters/jquery.js"></script>
<script src="/js/dist/app/sending_profiles.min.js"></script> <script src="/js/dist/app/sending_profiles.min.js"></script>
{{end}} {{end}}

View File

@ -117,6 +117,7 @@
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
<hr>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" data-dismiss="modal" class="btn btn-default" onclick="dismiss()">Cancel</button> <button type="button" data-dismiss="modal" class="btn btn-default" onclick="dismiss()">Cancel</button>

View File

@ -127,7 +127,36 @@ func processCampaign(c *models.Campaign) {
"<img alt='' style='display: none' src='" + c.URL + "/track?rid=" + t.RId + "'/>", "<img alt='' style='display: none' src='" + c.URL + "/track?rid=" + t.RId + "'/>",
fn, fn,
} }
// Parse the templates
// Parse the customHeader templates
for _, header := range c.SMTP.Headers {
parsedHeader := struct {
Key bytes.Buffer
Value bytes.Buffer
}{}
keytmpl, err := template.New("text_template").Parse(header.Key)
if err != nil {
Logger.Println(err)
}
err = keytmpl.Execute(&parsedHeader.Key, td)
if err != nil {
Logger.Println(err)
}
valtmpl, err := template.New("text_template").Parse(header.Value)
if err != nil {
Logger.Println(err)
}
err = valtmpl.Execute(&parsedHeader.Value, td)
if err != nil {
Logger.Println(err)
}
// Add our header immediately
e.SetHeader(parsedHeader.Key.String(), parsedHeader.Value.String())
}
// Parse remaining templates
var subjBuff bytes.Buffer var subjBuff bytes.Buffer
tmpl, err := template.New("text_template").Parse(c.Template.Subject) tmpl, err := template.New("text_template").Parse(c.Template.Subject)
if err != nil { if err != nil {
@ -243,10 +272,37 @@ func SendTestEmail(s *models.SendTestEmailRequest) error {
Logger.Println(err) Logger.Println(err)
return err return err
} }
Logger.Println("Creating email using template")
e := gomail.NewMessage() e := gomail.NewMessage()
// Parse the customHeader templates
for _, header := range s.SMTP.Headers {
parsedHeader := struct {
Key bytes.Buffer
Value bytes.Buffer
}{}
keytmpl, err := template.New("text_template").Parse(header.Key)
if err != nil {
Logger.Println(err)
}
err = keytmpl.Execute(&parsedHeader.Key, s)
if err != nil {
Logger.Println(err)
}
valtmpl, err := template.New("text_template").Parse(header.Value)
if err != nil {
Logger.Println(err)
}
err = valtmpl.Execute(&parsedHeader.Value, s)
if err != nil {
Logger.Println(err)
}
// Add our header immediately
e.SetHeader(parsedHeader.Key.String(), parsedHeader.Value.String())
}
e.SetHeader("From", s.SMTP.FromAddress) e.SetHeader("From", s.SMTP.FromAddress)
e.SetHeader("To", s.Email) e.SetHeader("To", s.Email)
Logger.Println("Creating email using template")
// Parse the templates // Parse the templates
var subjBuff bytes.Buffer var subjBuff bytes.Buffer
tmpl, err := template.New("text_template").Parse(s.Template.Subject) tmpl, err := template.New("text_template").Parse(s.Template.Subject)