2017-12-09 21:42:07 +00:00
package models
import (
2020-08-15 16:03:52 +00:00
"archive/zip"
"bytes"
2019-04-23 22:31:30 +00:00
"crypto/rand"
2020-08-15 16:03:52 +00:00
"crypto/sha1"
2017-12-09 21:42:07 +00:00
"encoding/base64"
"errors"
"fmt"
"io"
2020-08-15 16:03:52 +00:00
"io/ioutil"
2017-12-09 21:42:07 +00:00
"math"
2019-04-23 22:31:30 +00:00
"math/big"
2017-12-09 21:42:07 +00:00
"net/mail"
2019-04-23 22:31:30 +00:00
"os"
2020-08-15 16:03:52 +00:00
"path/filepath"
2017-12-09 21:42:07 +00:00
"strings"
"time"
"github.com/gophish/gomail"
2018-06-19 02:37:59 +00:00
"github.com/gophish/gophish/config"
2018-05-04 00:07:41 +00:00
log "github.com/gophish/gophish/logger"
2017-12-09 21:42:07 +00:00
"github.com/gophish/gophish/mailer"
)
// MaxSendAttempts set to 8 since we exponentially backoff after each failed send
// attempt. This will give us a maximum send delay of 256 minutes, or about 4.2 hours.
var MaxSendAttempts = 8
2018-12-16 03:38:51 +00:00
// ErrMaxSendAttempts is thrown when the maximum number of sending attempts for a given
2017-12-09 21:42:07 +00:00
// MailLog is exceeded.
var ErrMaxSendAttempts = errors . New ( "max send attempts exceeded" )
2020-08-15 16:03:52 +00:00
// processAttachment is used to to keep track of which email attachments have templated values.
// This allows us to skip re-templating attach
var processAttachment = map [ [ 20 ] byte ] bool { } // Considered using attachmentLookup[campaignid][filehash] but given the low number of files current approach should be fine
2017-12-09 21:42:07 +00:00
// MailLog is a struct that holds information about an email that is to be
// sent out.
type MailLog struct {
Id int64 ` json:"-" `
UserId int64 ` json:"-" `
CampaignId int64 ` json:"campaign_id" `
RId string ` json:"id" `
SendDate time . Time ` json:"send_date" `
SendAttempt int ` json:"send_attempt" `
Processing bool ` json:"-" `
2020-03-01 02:19:54 +00:00
cachedCampaign * Campaign
2017-12-09 21:42:07 +00:00
}
// GenerateMailLog creates a new maillog for the given campaign and
// result. It sets the initial send date to match the campaign's launch date.
2018-09-02 16:17:52 +00:00
func GenerateMailLog ( c * Campaign , r * Result , sendDate time . Time ) error {
2017-12-09 21:42:07 +00:00
m := & MailLog {
UserId : c . UserId ,
CampaignId : c . Id ,
RId : r . RId ,
2018-09-02 16:17:52 +00:00
SendDate : sendDate ,
2017-12-09 21:42:07 +00:00
}
2018-12-15 21:42:32 +00:00
return db . Save ( m ) . Error
2017-12-09 21:42:07 +00:00
}
// Backoff sets the MailLog SendDate to be the next entry in an exponential
// backoff. ErrMaxRetriesExceeded is thrown if this maillog has been retried
// too many times. Backoff also unlocks the maillog so that it can be processed
// again in the future.
func ( m * MailLog ) Backoff ( reason error ) error {
r , err := GetResult ( m . RId )
if err != nil {
return err
}
2018-05-27 02:26:34 +00:00
if m . SendAttempt == MaxSendAttempts {
r . HandleEmailError ( ErrMaxSendAttempts )
return ErrMaxSendAttempts
}
2017-12-09 21:42:07 +00:00
// Add an error, since we had to backoff because of a
// temporary error of some sort during the SMTP transaction
m . SendAttempt ++
backoffDuration := math . Pow ( 2 , float64 ( m . SendAttempt ) )
m . SendDate = m . SendDate . Add ( time . Minute * time . Duration ( backoffDuration ) )
err = db . Save ( m ) . Error
if err != nil {
return err
}
2018-05-27 02:26:34 +00:00
err = r . HandleEmailBackoff ( reason , m . SendDate )
2017-12-09 21:42:07 +00:00
if err != nil {
return err
}
err = m . Unlock ( )
return err
}
// Unlock removes the processing flag so the maillog can be processed again
func ( m * MailLog ) Unlock ( ) error {
m . Processing = false
return db . Save ( & m ) . Error
}
// Lock sets the processing flag so that other processes cannot modify the maillog
func ( m * MailLog ) Lock ( ) error {
m . Processing = true
return db . Save ( & m ) . Error
}
// Error sets the error status on the models.Result that the
// maillog refers to. Since MailLog errors are permanent,
// this action also deletes the maillog.
func ( m * MailLog ) Error ( e error ) error {
r , err := GetResult ( m . RId )
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
return err
}
2018-05-27 02:26:34 +00:00
err = r . HandleEmailError ( e )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
return err
}
err = db . Delete ( m ) . Error
return err
}
// Success deletes the maillog from the database and updates the underlying
// campaign result.
func ( m * MailLog ) Success ( ) error {
r , err := GetResult ( m . RId )
if err != nil {
return err
}
2018-05-27 02:26:34 +00:00
err = r . HandleEmailSent ( )
2017-12-09 21:42:07 +00:00
if err != nil {
return err
}
err = db . Delete ( m ) . Error
2020-05-26 02:46:36 +00:00
return err
2017-12-09 21:42:07 +00:00
}
// GetDialer returns a dialer based on the maillog campaign's SMTP configuration
func ( m * MailLog ) GetDialer ( ) ( mailer . Dialer , error ) {
2020-03-01 02:19:54 +00:00
c := m . cachedCampaign
if c == nil {
campaign , err := GetCampaignMailContext ( m . CampaignId , m . UserId )
if err != nil {
return nil , err
}
c = & campaign
2017-12-09 21:42:07 +00:00
}
return c . SMTP . GetDialer ( )
}
2020-03-01 02:19:54 +00:00
// CacheCampaign allows bulk-mail workers to cache the otherwise expensive
// campaign lookup operation by providing a pointer to the campaign here.
func ( m * MailLog ) CacheCampaign ( campaign * Campaign ) error {
if campaign . Id != m . CampaignId {
return fmt . Errorf ( "incorrect campaign provided for caching. expected %d got %d" , m . CampaignId , campaign . Id )
}
m . cachedCampaign = campaign
return nil
}
2017-12-09 21:42:07 +00:00
// Generate fills in the details of a gomail.Message instance with
// the correct headers and body from the campaign and recipient listed in
// the maillog. We accept the gomail.Message as an argument so that the caller
// can choose to re-use the message across recipients.
func ( m * MailLog ) Generate ( msg * gomail . Message ) error {
r , err := GetResult ( m . RId )
if err != nil {
return err
}
2020-03-01 02:19:54 +00:00
c := m . cachedCampaign
if c == nil {
campaign , err := GetCampaignMailContext ( m . CampaignId , m . UserId )
if err != nil {
return err
}
c = & campaign
2017-12-09 21:42:07 +00:00
}
2018-06-09 02:20:52 +00:00
2017-12-09 21:42:07 +00:00
f , err := mail . ParseAddress ( c . SMTP . FromAddress )
if err != nil {
return err
}
msg . SetAddressHeader ( "From" , f . Address , f . Name )
2018-06-09 02:20:52 +00:00
2020-03-01 02:19:54 +00:00
ptx , err := NewPhishingTemplateContext ( c , r . BaseRecipient , r . RId )
2018-01-13 23:49:42 +00:00
if err != nil {
return err
}
2018-02-23 04:10:50 +00:00
2018-06-19 02:37:59 +00:00
// Add the transparency headers
msg . SetHeader ( "X-Mailer" , config . ServerName )
2018-12-15 21:42:32 +00:00
if conf . ContactAddress != "" {
msg . SetHeader ( "X-Gophish-Contact" , conf . ContactAddress )
2018-06-19 02:37:59 +00:00
}
2019-04-23 22:31:30 +00:00
// Add Message-Id header as described in RFC 2822.
messageID , err := m . generateMessageID ( )
if err != nil {
return err
}
msg . SetHeader ( "Message-Id" , messageID )
2017-12-09 21:42:07 +00:00
// Parse the customHeader templates
for _ , header := range c . SMTP . Headers {
2018-06-09 02:20:52 +00:00
key , err := ExecuteTemplate ( header . Key , ptx )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
2018-06-09 02:20:52 +00:00
value , err := ExecuteTemplate ( header . Value , ptx )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
// Add our header immediately
msg . SetHeader ( key , value )
}
// Parse remaining templates
2018-06-09 02:20:52 +00:00
subject , err := ExecuteTemplate ( c . Template . Subject , ptx )
2020-08-15 16:03:52 +00:00
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
2018-02-10 19:46:08 +00:00
// don't set Subject header if the subject is empty
if len ( subject ) != 0 {
msg . SetHeader ( "Subject" , subject )
}
2017-12-09 21:42:07 +00:00
msg . SetHeader ( "To" , r . FormatAddress ( ) )
if c . Template . Text != "" {
2018-06-09 02:20:52 +00:00
text , err := ExecuteTemplate ( c . Template . Text , ptx )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
msg . SetBody ( "text/plain" , text )
}
if c . Template . HTML != "" {
2018-06-09 02:20:52 +00:00
html , err := ExecuteTemplate ( c . Template . HTML , ptx )
2017-12-09 21:42:07 +00:00
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
if c . Template . Text == "" {
msg . SetBody ( "text/html" , html )
} else {
msg . AddAlternative ( "text/html" , html )
}
}
// Attach the files
for _ , a := range c . Template . Attachments {
msg . Attach ( func ( a Attachment ) ( string , gomail . FileSetting , gomail . FileSetting ) {
h := map [ string ] [ ] string { "Content-ID" : { fmt . Sprintf ( "<%s>" , a . Name ) } }
return a . Name , gomail . SetCopyFunc ( func ( w io . Writer ) error {
2020-08-15 16:03:52 +00:00
//decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
decoder , err := applyAttachmentTemplate ( a , ptx )
if err != nil {
return err
}
2017-12-09 21:42:07 +00:00
_ , err = io . Copy ( w , decoder )
return err
} ) , gomail . SetHeader ( h )
} ( a ) )
}
return nil
}
2020-08-15 16:03:52 +00:00
// applyAttachmentTemplate parses different attachment files and applies the supplied phishing template.
func applyAttachmentTemplate ( a Attachment , ptx PhishingTemplateContext ) ( io . Reader , error ) {
fileContentsHash := sha1 . Sum ( [ ] byte ( a . Content ) ) // Hash of the file content
var processedAttachment string // Attachment content to return
decodedAttachment , err := base64 . StdEncoding . DecodeString ( a . Content ) // Decode the attachment
if err != nil {
return nil , err
}
// Keep track of which files have no template variables so we don't parse them repeatidly
if _ , ok := processAttachment [ fileContentsHash ] ; ! ok {
processAttachment [ fileContentsHash ] = true // Default to true to process a file
}
if processAttachment [ fileContentsHash ] == true {
// Decided to use the file extension rather than the content type, as there seems to be quite
// a bit of variability with types. e.g sometimes a Word docx file would have:
// "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
fileExtension := filepath . Ext ( a . Name )
switch fileExtension {
case ".docx" , ".docm" , ".pptx" , ".xlsx" , ".xlsm" :
// Most modern office formats are xml based and can be unarchived.
// .docm and .xlsm files are comprised of xml, and a binary blob for the macro code
// Create a new zip reader from the file
zipReader , err := zip . NewReader ( bytes . NewReader ( decodedAttachment ) , int64 ( len ( decodedAttachment ) ) )
if err != nil {
return nil , err
}
newZipArchive := new ( bytes . Buffer )
zipWriter := zip . NewWriter ( newZipArchive ) // For writing the new archive
// i. Read each file from the Word document archive
// ii. Apply the template to it
// iii. Add the templated content to a new zip Word archive
fileContainedTemplatesVars := false
for _ , zipFile := range zipReader . File {
ff , err := zipFile . Open ( )
if err != nil {
return nil , err
}
defer ff . Close ( )
contents , err := ioutil . ReadAll ( ff )
if err != nil {
return nil , err
}
subFileExtension := filepath . Ext ( zipFile . Name )
var tFile string
if subFileExtension == ".xml" || subFileExtension == ".rels" { // Ignore other files, e.g binary ones and images
// For each file apply the template.
tFile , err = ExecuteTemplate ( string ( contents ) , ptx )
if err != nil {
return nil , err
}
// Check if the subfile changed. We only need this to be set once to know in the future to check the 'parent' file
if tFile != string ( contents ) {
fileContainedTemplatesVars = true
}
} else {
tFile = string ( contents ) // Could move this to the declaration of tFile, but might be confusing to read
}
// Write new Word archive
newZipFile , err := zipWriter . Create ( zipFile . Name )
if err != nil {
zipWriter . Close ( ) // Don't use defer when writing files https://www.joeshaw.org/dont-defer-close-on-writable-files/
return nil , err
}
_ , err = newZipFile . Write ( [ ] byte ( tFile ) )
if err != nil {
zipWriter . Close ( )
return nil , err
}
}
// If no files in the archive had template variables, we set the 'parent' file to not be checked in the future
if fileContainedTemplatesVars == false {
processAttachment [ fileContentsHash ] = false
}
zipWriter . Close ( )
processedAttachment = newZipArchive . String ( )
case ".txt" , ".html" :
processedAttachment , err = ExecuteTemplate ( string ( decodedAttachment ) , ptx )
case ".pdf" :
// Todo.
// See: https://stackoverflow.com/questions/8099927/tracking-code-into-a-pdf-or-postscript-file
case ".exe" :
// Todo. Perhaps we ignore the .exe and build our own, with a simple callback to the server
// A special extension of 'exef' or some such might be useful in case users want to attach
// an actual exe file. Does anyone email exe files in 2020 ?
default :
// We have two options here; either apply template to all files, or none. Probably safer to err on the side of none.
processedAttachment = string ( decodedAttachment ) // Option one: Do nothing
//processedAttachment, err = ExecuteTemplate(string(decodedAttachment), ptx) // Option two: Template all files
}
// Handle err from all the switch statement ExecuteTemplate functions
if err != nil {
return nil , err
}
// Check if applying the template altered the file contents. If not, let's not apply the template again to that file.
// This doesn't work very well with .docx etc files, as the unzipping and rezipping seems to alter them.
if processedAttachment == string ( decodedAttachment ) {
processAttachment [ fileContentsHash ] = false
}
} else {
processedAttachment = string ( decodedAttachment )
}
decoder := strings . NewReader ( processedAttachment )
return decoder , nil
}
2017-12-09 21:42:07 +00:00
// GetQueuedMailLogs returns the mail logs that are queued up for the given minute.
func GetQueuedMailLogs ( t time . Time ) ( [ ] * MailLog , error ) {
ms := [ ] * MailLog { }
err := db . Where ( "send_date <= ? AND processing = ?" , t , false ) .
Find ( & ms ) . Error
if err != nil {
2018-05-04 00:07:41 +00:00
log . Warn ( err )
2017-12-09 21:42:07 +00:00
}
return ms , err
}
// GetMailLogsByCampaign returns all of the mail logs for a given campaign.
func GetMailLogsByCampaign ( cid int64 ) ( [ ] * MailLog , error ) {
ms := [ ] * MailLog { }
err := db . Where ( "campaign_id = ?" , cid ) . Find ( & ms ) . Error
return ms , err
}
// LockMailLogs locks or unlocks a slice of maillogs for processing.
func LockMailLogs ( ms [ ] * MailLog , lock bool ) error {
tx := db . Begin ( )
for i := range ms {
ms [ i ] . Processing = lock
2018-01-13 23:49:42 +00:00
err := tx . Save ( ms [ i ] ) . Error
2017-12-09 21:42:07 +00:00
if err != nil {
tx . Rollback ( )
return err
}
}
tx . Commit ( )
return nil
}
// UnlockAllMailLogs removes the processing lock for all maillogs
// in the database. This is intended to be called when Gophish is started
// so that any previously locked maillogs can resume processing.
func UnlockAllMailLogs ( ) error {
2018-12-15 21:42:32 +00:00
return db . Model ( & MailLog { } ) . Update ( "processing" , false ) . Error
2017-12-09 21:42:07 +00:00
}
2019-04-23 22:31:30 +00:00
var maxBigInt = big . NewInt ( math . MaxInt64 )
// generateMessageID generates and returns a string suitable for an RFC 2822
// compliant Message-ID, e.g.:
// <1444789264909237300.3464.1819418242800517193@DESKTOP01>
//
// The following parameters are used to generate a Message-ID:
// - The nanoseconds since Epoch
// - The calling PID
// - A cryptographically random int64
// - The sending hostname
func ( m * MailLog ) generateMessageID ( ) ( string , error ) {
t := time . Now ( ) . UnixNano ( )
pid := os . Getpid ( )
rint , err := rand . Int ( rand . Reader , maxBigInt )
if err != nil {
return "" , err
}
h , err := os . Hostname ( )
// If we can't get the hostname, we'll use localhost
if err != nil {
h = "localhost.localdomain"
}
msgid := fmt . Sprintf ( "<%d.%d.%d@%s>" , t , pid , rint , h )
return msgid , nil
}