mirror of https://github.com/gophish/gophish
Implement functionality to support custom RId - Ability to specify both the character set and length
parent
db63ee978d
commit
e198ba98af
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
ALTER TABLE `campaigns` ADD COLUMN character_set VARCHAR(255);
|
||||||
|
ALTER TABLE `campaigns` ADD COLUMN r_id_length INTEGER;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
ALTER TABLE campaigns ADD COLUMN character_set VARCHAR(255);
|
||||||
|
ALTER TABLE campaigns ADD COLUMN r_id_length INTEGER;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
|
|
@ -2,7 +2,9 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/gophish/gophish/logger"
|
log "github.com/gophish/gophish/logger"
|
||||||
|
@ -31,6 +33,9 @@ type Campaign struct {
|
||||||
SMTPId int64 `json:"-"`
|
SMTPId int64 `json:"-"`
|
||||||
SMTP SMTP `json:"smtp"`
|
SMTP SMTP `json:"smtp"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
|
CustomRId bool `json:"custom_rid" gorm:"-"`
|
||||||
|
CharacterSet string `json:"character_set" sql:"not null"`
|
||||||
|
RIdLength int64 `json:"r_id_length"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CampaignResults is a struct representing the results from a campaign
|
// CampaignResults is a struct representing the results from a campaign
|
||||||
|
@ -58,6 +63,8 @@ type CampaignSummary struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Stats CampaignStats `json:"stats"`
|
Stats CampaignStats `json:"stats"`
|
||||||
|
CharacterSet string `json:"character_set"`
|
||||||
|
RIdLength int64 `json:"r_id_length"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CampaignStats is a struct representing the statistics for a single campaign
|
// CampaignStats is a struct representing the statistics for a single campaign
|
||||||
|
@ -126,6 +133,15 @@ var ErrSMTPNotFound = errors.New("Sending profile not found")
|
||||||
// launch date
|
// launch date
|
||||||
var ErrInvalidSendByDate = errors.New("The launch date must be before the \"send emails by\" date")
|
var ErrInvalidSendByDate = errors.New("The launch date must be before the \"send emails by\" date")
|
||||||
|
|
||||||
|
// ErrInvalidCharacterSet indicates that the user has entered an invalid character set, ie it's empty
|
||||||
|
var ErrInvalidCharacterSet = errors.New("The defined character set is invalid")
|
||||||
|
|
||||||
|
// ErrInvalidRIdLength indicates that an invalid RId length has been set (ie zero)
|
||||||
|
var ErrInvalidRIdLength = errors.New("The RId length entered is invalid")
|
||||||
|
|
||||||
|
// ErrInsufficientCharsetKeyspace indicates that there aren't enough RId combinations to match the total recipients, for the given CharacterSet and RIdLength.
|
||||||
|
var ErrInsufficientCharsetKeyspace = errors.New("The specified CharacterSet/Length combination cannot cover the total recipients in this campaign. Consider increasing the Character Set, RId Length, or both");
|
||||||
|
|
||||||
// RecipientParameter is the URL parameter that points to the result ID for a recipient.
|
// RecipientParameter is the URL parameter that points to the result ID for a recipient.
|
||||||
const RecipientParameter = "rid"
|
const RecipientParameter = "rid"
|
||||||
|
|
||||||
|
@ -144,6 +160,12 @@ func (c *Campaign) Validate() error {
|
||||||
return ErrSMTPNotSpecified
|
return ErrSMTPNotSpecified
|
||||||
case !c.SendByDate.IsZero() && !c.LaunchDate.IsZero() && c.SendByDate.Before(c.LaunchDate):
|
case !c.SendByDate.IsZero() && !c.LaunchDate.IsZero() && c.SendByDate.Before(c.LaunchDate):
|
||||||
return ErrInvalidSendByDate
|
return ErrInvalidSendByDate
|
||||||
|
case c.CustomRId:
|
||||||
|
if c.CharacterSet == "" {
|
||||||
|
return ErrInvalidCharacterSet
|
||||||
|
} else if c.RIdLength <= 0 {
|
||||||
|
return ErrInvalidRIdLength
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -324,7 +346,7 @@ func GetCampaignSummaries(uid int64) (CampaignSummaries, error) {
|
||||||
cs := []CampaignSummary{}
|
cs := []CampaignSummary{}
|
||||||
// Get the basic campaign information
|
// Get the basic campaign information
|
||||||
query := db.Table("campaigns").Where("user_id = ?", uid)
|
query := db.Table("campaigns").Where("user_id = ?", uid)
|
||||||
query = query.Select("id, name, created_date, launch_date, send_by_date, completed_date, status")
|
query = query.Select("id, name, created_date, launch_date, send_by_date, completed_date, status, character_set, r_id_length")
|
||||||
err := query.Scan(&cs).Error
|
err := query.Scan(&cs).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
|
@ -347,7 +369,7 @@ func GetCampaignSummaries(uid int64) (CampaignSummaries, error) {
|
||||||
func GetCampaignSummary(id int64, uid int64) (CampaignSummary, error) {
|
func GetCampaignSummary(id int64, uid int64) (CampaignSummary, error) {
|
||||||
cs := CampaignSummary{}
|
cs := CampaignSummary{}
|
||||||
query := db.Table("campaigns").Where("user_id = ? AND id = ?", uid, id)
|
query := db.Table("campaigns").Where("user_id = ? AND id = ?", uid, id)
|
||||||
query = query.Select("id, name, created_date, launch_date, send_by_date, completed_date, status")
|
query = query.Select("id, name, created_date, launch_date, send_by_date, completed_date, status, character_set, r_id_length")
|
||||||
err := query.Scan(&cs).Error
|
err := query.Scan(&cs).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
|
@ -487,6 +509,19 @@ func PostCampaign(c *Campaign, uid int64) error {
|
||||||
}
|
}
|
||||||
totalRecipients += len(c.Groups[i].Targets)
|
totalRecipients += len(c.Groups[i].Targets)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.CustomRId {
|
||||||
|
c.CharacterSet = CleanCharacterSet(c.CharacterSet)
|
||||||
|
// We need to check if the keyspace for the custom character set and rid is enough to match the totalRecipients.
|
||||||
|
// This is on purpose <= so that we don't come too close to the limit.
|
||||||
|
if int(math.Pow(float64(len(c.CharacterSet)), float64(c.RIdLength))) <= totalRecipients {
|
||||||
|
return ErrInsufficientCharsetKeyspace
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.CharacterSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
c.RIdLength = 7
|
||||||
|
}
|
||||||
|
|
||||||
// Check to make sure the template exists
|
// Check to make sure the template exists
|
||||||
t, err := GetTemplateByName(c.Template.Name, uid)
|
t, err := GetTemplateByName(c.Template.Name, uid)
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
@ -564,7 +599,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
||||||
Reported: false,
|
Reported: false,
|
||||||
ModifiedDate: c.CreatedDate,
|
ModifiedDate: c.CreatedDate,
|
||||||
}
|
}
|
||||||
err = r.GenerateId(tx)
|
err = r.GenerateId(tx, c.CharacterSet, c.RIdLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
|
@ -667,3 +702,16 @@ func CompleteCampaign(id int64, uid int64) error {
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CleanCharacterSet(character_set string) string {
|
||||||
|
keys := make(map[string]bool)
|
||||||
|
chars := strings.Split(character_set, "")
|
||||||
|
unique := []string{}
|
||||||
|
for _, char := range chars {
|
||||||
|
if _, value := keys[char]; !value {
|
||||||
|
keys[char] = true
|
||||||
|
unique = append(unique, char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(unique, "")
|
||||||
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ func (s *EmailRequest) Success() error {
|
||||||
// PostEmailRequest stores a SendTestEmailRequest in the database.
|
// PostEmailRequest stores a SendTestEmailRequest in the database.
|
||||||
func PostEmailRequest(s *EmailRequest) error {
|
func PostEmailRequest(s *EmailRequest) error {
|
||||||
// Generate an ID to be used in the underlying Result object
|
// Generate an ID to be used in the underlying Result object
|
||||||
rid, err := generateResultId()
|
rid, err := generateResultId("", 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package models
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
@ -170,9 +172,12 @@ func (r *Result) UpdateGeo(addr string) error {
|
||||||
return db.Save(r).Error
|
return db.Save(r).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateResultId() (string, error) {
|
func generateResultId(alphaNum string, r_id_length int64) (string, error) {
|
||||||
const alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
if len(alphaNum) == 0 || r_id_length <= 0 {
|
||||||
k := make([]byte, 7)
|
alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
r_id_length = 7
|
||||||
|
}
|
||||||
|
k := make([]byte, r_id_length)
|
||||||
for i := range k {
|
for i := range k {
|
||||||
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphaNum))))
|
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphaNum))))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -185,10 +190,12 @@ func generateResultId() (string, error) {
|
||||||
|
|
||||||
// GenerateId generates a unique key to represent the result
|
// GenerateId generates a unique key to represent the result
|
||||||
// in the database
|
// in the database
|
||||||
func (r *Result) GenerateId(tx *gorm.DB) error {
|
func (r *Result) GenerateId(tx *gorm.DB, character_set string, r_id_length int64) error {
|
||||||
// Keep trying until we generate a unique key (shouldn't take more than one or two iterations)
|
// Keep trying until we generate a unique key (shouldn't take more than one or two iterations)
|
||||||
|
max_iterations := int(math.Pow(float64(len(character_set)), float64(r_id_length)))
|
||||||
|
iteration_count := 0
|
||||||
for {
|
for {
|
||||||
rid, err := generateResultId()
|
rid, err := generateResultId(character_set, r_id_length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -197,6 +204,11 @@ func (r *Result) GenerateId(tx *gorm.DB) error {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iteration_count++
|
||||||
|
if iteration_count >= max_iterations {
|
||||||
|
return errors.New("Too many iterations - Consider increasing the character set and/or RId length")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,20 @@ import (
|
||||||
|
|
||||||
func (s *ModelsSuite) TestGenerateResultId(c *check.C) {
|
func (s *ModelsSuite) TestGenerateResultId(c *check.C) {
|
||||||
r := Result{}
|
r := Result{}
|
||||||
r.GenerateId(db)
|
r.GenerateId(db, "", 0)
|
||||||
match, err := regexp.Match("[a-zA-Z0-9]{7}", []byte(r.RId))
|
match, err := regexp.Match("[a-zA-Z0-9]{7}", []byte(r.RId))
|
||||||
c.Assert(err, check.Equals, nil)
|
c.Assert(err, check.Equals, nil)
|
||||||
c.Assert(match, check.Equals, true)
|
c.Assert(match, check.Equals, true)
|
||||||
|
|
||||||
|
r.GenerateId(db, "0123456789", 4)
|
||||||
|
match, err = regexp.Match("[0-9]{4}", []byte(r.RId))
|
||||||
|
c.Assert(err, check.Equals, nil)
|
||||||
|
c.Assert(match, check.Equals, true)
|
||||||
|
|
||||||
|
r.GenerateId(db, "abcdefghijklmnopqrstuvwxyz", 3)
|
||||||
|
match, err = regexp.Match("[a-z]{3}", []byte(r.RId))
|
||||||
|
c.Assert(err, check.Equals, nil)
|
||||||
|
c.Assert(match, check.Equals, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ModelsSuite) TestFormatAddress(c *check.C) {
|
func (s *ModelsSuite) TestFormatAddress(c *check.C) {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -37,6 +37,11 @@ function launch() {
|
||||||
if (send_by_date != "") {
|
if (send_by_date != "") {
|
||||||
send_by_date = moment(send_by_date, "MMMM Do YYYY, h:mm a").utc().format()
|
send_by_date = moment(send_by_date, "MMMM Do YYYY, h:mm a").utc().format()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var custom_rid = $('#customize_rid').is(':checked');
|
||||||
|
var character_set = $('#character_set').val().trim();
|
||||||
|
var r_id_length = parseInt($('#r_id_length').val().trim()) || 7;
|
||||||
|
|
||||||
campaign = {
|
campaign = {
|
||||||
name: $("#name").val(),
|
name: $("#name").val(),
|
||||||
template: {
|
template: {
|
||||||
|
@ -52,6 +57,9 @@ function launch() {
|
||||||
launch_date: moment($("#launch_date").val(), "MMMM Do YYYY, h:mm a").utc().format(),
|
launch_date: moment($("#launch_date").val(), "MMMM Do YYYY, h:mm a").utc().format(),
|
||||||
send_by_date: send_by_date || null,
|
send_by_date: send_by_date || null,
|
||||||
groups: groups,
|
groups: groups,
|
||||||
|
custom_rid: custom_rid,
|
||||||
|
character_set: character_set,
|
||||||
|
r_id_length: r_id_length
|
||||||
}
|
}
|
||||||
// Submit the campaign
|
// Submit the campaign
|
||||||
api.campaigns.post(campaign)
|
api.campaigns.post(campaign)
|
||||||
|
@ -378,10 +386,18 @@ $(document).ready(function () {
|
||||||
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
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var charSetText = "";
|
||||||
|
if (campaign.character_set == "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") {
|
||||||
|
charSetText = "default";
|
||||||
|
} else {
|
||||||
|
charSetText = escapeHtml(campaign.character_set) + " (Length: " + campaign.r_id_length + ")";
|
||||||
|
}
|
||||||
|
|
||||||
var row = [
|
var row = [
|
||||||
escapeHtml(campaign.name),
|
escapeHtml(campaign.name),
|
||||||
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
|
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||||
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
|
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
|
||||||
|
"<span>" + charSetText + "</span>",
|
||||||
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
|
"<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>\
|
<i class='fa fa-bar-chart'></i>\
|
||||||
</a>\
|
</a>\
|
||||||
|
@ -424,4 +440,10 @@ $(document).ready(function () {
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$('#customize_rid').change(function() {
|
||||||
|
$(this).is(':checked')
|
||||||
|
? $('#fields-customize_rid').removeClass('hidden')
|
||||||
|
: $('#fields-customize_rid').addClass('hidden')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -37,8 +37,9 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-md-3">Name</th>
|
<th class="col-md-3">Name</th>
|
||||||
<th class="col-md-4">Created Date</th>
|
<th class="col-md-2">Created Date</th>
|
||||||
<th class="col-md-2">Status</th>
|
<th class="col-md-2">Status</th>
|
||||||
|
<th class="col-md-2">Character Set</th>
|
||||||
<th class="col-md-3 no-sort"></th>
|
<th class="col-md-3 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -60,8 +61,9 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-md-3">Name</th>
|
<th class="col-md-3">Name</th>
|
||||||
<th class="col-md-4">Created Date</th>
|
<th class="col-md-2">Created Date</th>
|
||||||
<th class="col-md-2">Status</th>
|
<th class="col-md-2">Status</th>
|
||||||
|
<th class="col-md-2">Character Set</th>
|
||||||
<th class="col-md-3 no-sort"></th>
|
<th class="col-md-3 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -124,6 +126,24 @@
|
||||||
</div>
|
</div>
|
||||||
<label class="control-label" for="users">Groups:</label>
|
<label class="control-label" for="users">Groups:</label>
|
||||||
<select class="form-control" id="users" multiple="multiple"></select>
|
<select class="form-control" id="users" multiple="multiple"></select>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<input type="checkbox" id="customize_rid">
|
||||||
|
<label for="customize_rid">Customize RId</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row hidden" id="fields-customize_rid">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<label for="character_set">RId Character Set</label>
|
||||||
|
<input type="text" class="form-control" id="character_set" placeholder="Character set for RId variable" value="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="r_id_length">RId Length</label>
|
||||||
|
<input type="number" class="form-control" id="r_id_length" placeholder="Length for RId variable" value="7">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|
Loading…
Reference in New Issue