package models import ( "archive/zip" "bytes" "crypto/rand" "crypto/sha1" "encoding/base64" "errors" "fmt" "io" "io/ioutil" "math" "math/big" "net/mail" "os" "path/filepath" "strings" "time" "github.com/gophish/gomail" "github.com/gophish/gophish/config" log "github.com/gophish/gophish/logger" "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 // ErrMaxSendAttempts is thrown when the maximum number of sending attempts for a given // MailLog is exceeded. var ErrMaxSendAttempts = errors.New("max send attempts exceeded") // 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 // 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:"-"` cachedCampaign *Campaign } // GenerateMailLog creates a new maillog for the given campaign and // result. It sets the initial send date to match the campaign's launch date. func GenerateMailLog(c *Campaign, r *Result, sendDate time.Time) error { m := &MailLog{ UserId: c.UserId, CampaignId: c.Id, RId: r.RId, SendDate: sendDate, } return db.Save(m).Error } // 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 } if m.SendAttempt == MaxSendAttempts { r.HandleEmailError(ErrMaxSendAttempts) return ErrMaxSendAttempts } // 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 } err = r.HandleEmailBackoff(reason, m.SendDate) 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 { log.Warn(err) return err } err = r.HandleEmailError(e) if err != nil { log.Warn(err) 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 } err = r.HandleEmailSent() if err != nil { return err } err = db.Delete(m).Error return err } // GetDialer returns a dialer based on the maillog campaign's SMTP configuration func (m *MailLog) GetDialer() (mailer.Dialer, error) { c := m.cachedCampaign if c == nil { campaign, err := GetCampaignMailContext(m.CampaignId, m.UserId) if err != nil { return nil, err } c = &campaign } return c.SMTP.GetDialer() } // 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 } // 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 } c := m.cachedCampaign if c == nil { campaign, err := GetCampaignMailContext(m.CampaignId, m.UserId) if err != nil { return err } c = &campaign } f, err := mail.ParseAddress(c.SMTP.FromAddress) if err != nil { return err } msg.SetAddressHeader("From", f.Address, f.Name) ptx, err := NewPhishingTemplateContext(c, r.BaseRecipient, r.RId) if err != nil { return err } // Add the transparency headers msg.SetHeader("X-Mailer", config.ServerName) if conf.ContactAddress != "" { msg.SetHeader("X-Gophish-Contact", conf.ContactAddress) } // Add Message-Id header as described in RFC 2822. messageID, err := m.generateMessageID() if err != nil { return err } msg.SetHeader("Message-Id", messageID) // Parse the customHeader templates for _, header := range c.SMTP.Headers { key, err := ExecuteTemplate(header.Key, ptx) if err != nil { log.Warn(err) } value, err := ExecuteTemplate(header.Value, ptx) if err != nil { log.Warn(err) } // Add our header immediately msg.SetHeader(key, value) } // Parse remaining templates subject, err := ExecuteTemplate(c.Template.Subject, ptx) if err != nil { log.Warn(err) } // don't set Subject header if the subject is empty if len(subject) != 0 { msg.SetHeader("Subject", subject) } msg.SetHeader("To", r.FormatAddress()) if c.Template.Text != "" { text, err := ExecuteTemplate(c.Template.Text, ptx) if err != nil { log.Warn(err) } msg.SetBody("text/plain", text) } if c.Template.HTML != "" { html, err := ExecuteTemplate(c.Template.HTML, ptx) if err != nil { log.Warn(err) } 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 { //decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content)) decoder, err := applyAttachmentTemplate(a, ptx) if err != nil { return err } _, err = io.Copy(w, decoder) return err }), gomail.SetHeader(h) }(a)) } return nil } // 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 } // 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 { log.Warn(err) } 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 err := tx.Save(ms[i]).Error 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 { return db.Model(&MailLog{}).Update("processing", false).Error } 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 }