mirror of https://github.com/gophish/gophish
Added support for templating attachments (#1936)
The following attachment types support template variables: docx, docm, pptx, xlsx, xlsm, txt, html, ics.pull/2110/merge
parent
0646f14c99
commit
a6627dfc6b
|
@ -1,5 +1,17 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Attachment contains the fields and methods for
|
||||
// an email attachment
|
||||
type Attachment struct {
|
||||
|
@ -8,4 +20,137 @@ type Attachment struct {
|
|||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
vanillaFile bool // Vanilla file has no template variables
|
||||
}
|
||||
|
||||
// Validate ensures that the provided attachment uses the supported template variables correctly.
|
||||
func (a Attachment) Validate() error {
|
||||
vc := ValidationContext{
|
||||
FromAddress: "foo@bar.com",
|
||||
BaseURL: "http://example.com",
|
||||
}
|
||||
td := Result{
|
||||
BaseRecipient: BaseRecipient{
|
||||
Email: "foo@bar.com",
|
||||
FirstName: "Foo",
|
||||
LastName: "Bar",
|
||||
Position: "Test",
|
||||
},
|
||||
RId: "123456",
|
||||
}
|
||||
ptx, err := NewPhishingTemplateContext(vc, td.BaseRecipient, td.RId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = a.ApplyTemplate(ptx)
|
||||
return err
|
||||
}
|
||||
|
||||
// ApplyTemplate parses different attachment files and applies the supplied phishing template.
|
||||
func (a *Attachment) ApplyTemplate(ptx PhishingTemplateContext) (io.Reader, error) {
|
||||
|
||||
decodedAttachment := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
|
||||
|
||||
// If we've already determined there are no template variables in this attachment return it immediately
|
||||
if a.vanillaFile == true {
|
||||
return decodedAttachment, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Zip archives require random access for reading, so it's hard to stream bytes. Solution seems to be to use a buffer.
|
||||
// See https://stackoverflow.com/questions/16946978/how-to-unzip-io-readcloser
|
||||
b := new(bytes.Buffer)
|
||||
b.ReadFrom(decodedAttachment)
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(b.Bytes()), int64(b.Len())) // Create a new zip reader from the file
|
||||
|
||||
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
|
||||
a.vanillaFile = true
|
||||
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
|
||||
// First we look for instances where Word has URL escaped our template variables. This seems to happen when inserting a remote image, converting {{.Foo}} to %7b%7b.foo%7d%7d.
|
||||
// See https://stackoverflow.com/questions/68287630/disable-url-encoding-for-includepicture-in-microsoft-word
|
||||
rx, _ := regexp.Compile("%7b%7b.([a-zA-Z]+)%7d%7d")
|
||||
contents := rx.ReplaceAllFunc(contents, func(m []byte) []byte {
|
||||
d, err := url.QueryUnescape(string(m))
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
return []byte(d)
|
||||
})
|
||||
|
||||
// For each file apply the template.
|
||||
tFile, err = ExecuteTemplate(string(contents), ptx)
|
||||
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
|
||||
}
|
||||
// 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) {
|
||||
a.vanillaFile = false
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
zipWriter.Close()
|
||||
return bytes.NewReader(newZipArchive.Bytes()), err
|
||||
|
||||
case ".txt", ".html", ".ics":
|
||||
b, err := ioutil.ReadAll(decodedAttachment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processedAttachment, err := ExecuteTemplate(string(b), ptx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if processedAttachment == string(b) {
|
||||
a.vanillaFile = true
|
||||
}
|
||||
return strings.NewReader(processedAttachment), nil
|
||||
default:
|
||||
return decodedAttachment, nil // Default is to simply return the file
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func (s *ModelsSuite) TestAttachment(c *check.C) {
|
||||
ptx := PhishingTemplateContext{
|
||||
BaseRecipient: BaseRecipient{
|
||||
FirstName: "Foo",
|
||||
LastName: "Bar",
|
||||
Email: "foo@bar.com",
|
||||
Position: "Space Janitor",
|
||||
},
|
||||
BaseURL: "http://testurl.com",
|
||||
URL: "http://testurl.com/?rid=1234567",
|
||||
TrackingURL: "http://testurl.local/track?rid=1234567",
|
||||
Tracker: "<img alt='' style='display: none' src='http://testurl.local/track?rid=1234567'/>",
|
||||
From: "From Address",
|
||||
RId: "1234567",
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir("testdata")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open attachment folder 'testdata': %v\n", err)
|
||||
}
|
||||
for _, ff := range files {
|
||||
if !ff.IsDir() && !strings.Contains(ff.Name(), "templated") {
|
||||
fname := ff.Name()
|
||||
fmt.Printf("Checking attachment file -> %s\n", fname)
|
||||
data := readFile("testdata/" + fname)
|
||||
if filepath.Ext(fname) == ".b64" {
|
||||
fname = fname[:len(fname)-4]
|
||||
}
|
||||
a := Attachment{
|
||||
Content: data,
|
||||
Name: fname,
|
||||
}
|
||||
t, err := a.ApplyTemplate(ptx)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(a.vanillaFile, check.Equals, strings.Contains(fname, "without-vars"))
|
||||
c.Assert(a.vanillaFile, check.Not(check.Equals), strings.Contains(fname, "with-vars"))
|
||||
|
||||
// Verfify template was applied as expected
|
||||
tt, err := ioutil.ReadAll(t)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse templated file '%s': %v\n", fname, err)
|
||||
}
|
||||
templatedFile := base64.StdEncoding.EncodeToString(tt)
|
||||
expectedOutput := readFile("testdata/" + strings.TrimSuffix(ff.Name(), filepath.Ext(ff.Name())) + ".templated" + filepath.Ext(ff.Name())) // e.g text-file-with-vars.templated.txt
|
||||
c.Assert(templatedFile, check.Equals, expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(fname string) string {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open file '%s': %v\n", fname, err)
|
||||
}
|
||||
reader := bufio.NewReader(f)
|
||||
content, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read file '%s': %v\n", fname, err)
|
||||
}
|
||||
data := ""
|
||||
if filepath.Ext(fname) == ".b64" {
|
||||
data = string(content)
|
||||
} else {
|
||||
data = base64.StdEncoding.EncodeToString(content)
|
||||
}
|
||||
return data
|
||||
}
|
|
@ -2,7 +2,6 @@ package models
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -10,7 +9,6 @@ import (
|
|||
"math/big"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gophish/gomail"
|
||||
|
@ -211,6 +209,7 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
|
|||
|
||||
// Parse remaining templates
|
||||
subject, err := ExecuteTemplate(c.Template.Subject, ptx)
|
||||
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
@ -239,12 +238,16 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
|
|||
}
|
||||
}
|
||||
// Attach the files
|
||||
for _, a := range c.Template.Attachments {
|
||||
msg.Attach(func(a Attachment) (string, gomail.FileSetting, gomail.FileSetting) {
|
||||
for i, _ := range c.Template.Attachments {
|
||||
a := &c.Template.Attachments[i]
|
||||
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))
|
||||
_, err = io.Copy(w, decoder)
|
||||
content, err := a.ApplyTemplate(ptx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, content)
|
||||
return err
|
||||
}), gomail.SetHeader(h)
|
||||
}(a))
|
||||
|
|
|
@ -40,6 +40,12 @@ func (t *Template) Validate() error {
|
|||
if err := ValidateTemplate(t.Text); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range t.Attachments {
|
||||
if err := a.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//zoom.us//iCalendar Event//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
CLASS:PUBLIC
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/London
|
||||
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/London
|
||||
X-LIC-LOCATION:Europe/London
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0000
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:BST
|
||||
DTSTART:19700329T010000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0000
|
||||
TZNAME:GMT
|
||||
DTSTART:19701025T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20210306T182251Z
|
||||
DTSTART;TZID=Europe/London:20210306T183000
|
||||
DTEND;TZID=Europe/London:20210306T190000
|
||||
SUMMARY:Gophish Test Calendar
|
||||
UID:20210306T182251Z-89336450000@fe80:0:0:0:31:49ff:fec9:f252ens5
|
||||
TZID:Europe/London
|
||||
DESCRIPTION:Glenn Wilkinson is inviting you to a scheduled Zoom meeting.\
|
||||
n\nJoin Zoom Meeting\n{{.URL}}\n\nMeeting ID: 893 3645 9466\nPasscode: 31337\
|
||||
nOne tap mobile\n+442039017895\,\,89336450000#\,\,\,\,*509879# United Ki
|
||||
ngdom\n+441314601196\,\,89336450000#\,\,\,\,*509879# United Kingdom\n\nD
|
||||
ial by your location\n +44 203 901 7895 United Kingdom\n +
|
||||
44 131 460 1196 United Kingdom\n +44 203 051 2874 United Kingdom\
|
||||
n +44 203 481 5237 United Kingdom\n +44 203 481 5240 Unite
|
||||
d Kingdom\n +1 253 215 8782 US (Tacoma)\n +1 301 715 8592
|
||||
US (Washington DC)\n +1 312 626 6799 US (Chicago)\n +1 346
|
||||
248 7799 US (Houston)\n +1 646 558 8656 US (New York)\n +
|
||||
1 669 900 9128 US https://us02web.zoom.us/u/kpXDbMrN\n\n
|
||||
LOCATION:{{.URL}}
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT10M
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,51 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//zoom.us//iCalendar Event//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
CLASS:PUBLIC
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/London
|
||||
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/London
|
||||
X-LIC-LOCATION:Europe/London
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0000
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:BST
|
||||
DTSTART:19700329T010000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0000
|
||||
TZNAME:GMT
|
||||
DTSTART:19701025T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20210306T182251Z
|
||||
DTSTART;TZID=Europe/London:20210306T183000
|
||||
DTEND;TZID=Europe/London:20210306T190000
|
||||
SUMMARY:Gophish Test Calendar
|
||||
UID:20210306T182251Z-89336450000@fe80:0:0:0:31:49ff:fec9:f252ens5
|
||||
TZID:Europe/London
|
||||
DESCRIPTION:Glenn Wilkinson is inviting you to a scheduled Zoom meeting.\
|
||||
n\nJoin Zoom Meeting\nhttp://testurl.com/?rid=1234567\n\nMeeting ID: 893 3645 9466\nPasscode: 31337\
|
||||
nOne tap mobile\n+442039017895\,\,89336450000#\,\,\,\,*509879# United Ki
|
||||
ngdom\n+441314601196\,\,89336450000#\,\,\,\,*509879# United Kingdom\n\nD
|
||||
ial by your location\n +44 203 901 7895 United Kingdom\n +
|
||||
44 131 460 1196 United Kingdom\n +44 203 051 2874 United Kingdom\
|
||||
n +44 203 481 5237 United Kingdom\n +44 203 481 5240 Unite
|
||||
d Kingdom\n +1 253 215 8782 US (Tacoma)\n +1 301 715 8592
|
||||
US (Washington DC)\n +1 312 626 6799 US (Chicago)\n +1 346
|
||||
248 7799 US (Houston)\n +1 646 558 8656 US (New York)\n +
|
||||
1 669 900 9128 US https://us02web.zoom.us/u/kpXDbMrN\n\n
|
||||
LOCATION:http://testurl.com/?rid=1234567
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT10M
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,13 @@
|
|||
<html>
|
||||
<head><title>Page for {{.FirstName}}</title></head>
|
||||
|
||||
<body>
|
||||
|
||||
Hello {{.FirstName}} {{.LastName}} <p>
|
||||
|
||||
|
||||
Click <a href="{{.URL}}">here</a> for a legit link.
|
||||
|
||||
</body>
|
||||
{{.Tracker}}
|
||||
</html>
|
|
@ -0,0 +1,13 @@
|
|||
<html>
|
||||
<head><title>Page for Foo</title></head>
|
||||
|
||||
<body>
|
||||
|
||||
Hello Foo Bar <p>
|
||||
|
||||
|
||||
Click <a href="http://testurl.com/?rid=1234567">here</a> for a legit link.
|
||||
|
||||
</body>
|
||||
<img alt='' style='display: none' src='http://testurl.local/track?rid=1234567'/>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<head><title>There are no variables here.</title></head>
|
||||
|
||||
<body>
|
||||
|
||||
There are no vars in this file.
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<head><title>There are no variables here.</title></head>
|
||||
|
||||
<body>
|
||||
|
||||
There are no vars in this file.
|
||||
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,19 @@
|
|||
The target's unique ID: 1234567
|
||||
|
||||
The target's first name: Foo
|
||||
|
||||
The target's last name: Bar
|
||||
|
||||
The target's position: Space Janitor
|
||||
|
||||
The target's email address: foo@bar.com
|
||||
|
||||
The spoofed sender: From Address
|
||||
|
||||
The URL to the tracking handler: http://testurl.local/track?rid=1234567
|
||||
|
||||
An alias for tracker image: <img alt='' style='display: none' src='http://testurl.local/track?rid=1234567'/>
|
||||
|
||||
The phishing URL: http://testurl.com/?rid=1234567
|
||||
|
||||
The base URL with the path and rid parameter stripped. Useful for making links to static files: http://testurl.com
|
|
@ -0,0 +1,19 @@
|
|||
The target's unique ID: {{.RId}}
|
||||
|
||||
The target's first name: {{.FirstName}}
|
||||
|
||||
The target's last name: {{.LastName}}
|
||||
|
||||
The target's position: {{.Position}}
|
||||
|
||||
The target's email address: {{.Email}}
|
||||
|
||||
The spoofed sender: {{.From}}
|
||||
|
||||
The URL to the tracking handler: {{.TrackingURL}}
|
||||
|
||||
An alias for tracker image: {{.Tracker}}
|
||||
|
||||
The phishing URL: {{.URL}}
|
||||
|
||||
The base URL with the path and rid parameter stripped. Useful for making links to static files: {{.BaseURL}}
|
|
@ -0,0 +1 @@
|
|||
There are no variables in this file.
|
|
@ -0,0 +1 @@
|
|||
There are no variables in this file.
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue