Added support for templating attachments (#1936)

The following attachment types support template variables: docx, docm, pptx, xlsx, xlsm, txt, html, ics.
pull/2110/merge
Glenn Wilkinson 2022-02-02 15:41:27 +01:00 committed by GitHub
parent 0646f14c99
commit a6627dfc6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 441 additions and 11 deletions

View File

@ -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
}
}

82
models/attachment_test.go Normal file
View 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
}

View File

@ -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))

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,9 @@
<html>
<head><title>There are no variables here.</title></head>
<body>
There are no vars in this file.
</body>
</html>

View File

@ -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

View File

@ -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

19
models/testdata/text-file-with-vars.txt vendored Normal file
View File

@ -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}}

View File

@ -0,0 +1 @@
There are no variables in this file.

View 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