2014-07-12 18:46:38 +00:00
package models
2022-02-02 14:41:27 +00:00
import (
"archive/zip"
"bytes"
"encoding/base64"
"io"
"io/ioutil"
"net/url"
"path/filepath"
"regexp"
"strings"
)
2016-01-25 02:47:16 +00:00
// Attachment contains the fields and methods for
// an email attachment
2014-07-12 18:46:38 +00:00
type Attachment struct {
2022-02-02 14:41:27 +00:00
Id int64 ` json:"-" `
TemplateId int64 ` json:"-" `
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
}
2023-02-08 08:13:08 +00:00
// ApplyTemplateMSOffice parses MS Office attachment files and applies the supplied phishing template.
func ( a * Attachment ) ApplyTemplateMSOffice ( ptx PhishingTemplateContext , decodedAttachment [ ] byte ) ( io . Reader , error ) {
// 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 . Write ( 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
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
}
// ApplyTemplateTextFiles applies the supplied phishing template to text file attachments (txt, html, ics)
func ( a * Attachment ) ApplyTemplateTextFiles ( ptx PhishingTemplateContext , decodedAttachment [ ] byte ) ( io . Reader , error ) {
processedAttachment , err := ExecuteTemplate ( string ( decodedAttachment ) , ptx )
if err != nil {
return nil , err
}
if processedAttachment != string ( decodedAttachment ) {
a . vanillaFile = false
}
return strings . NewReader ( processedAttachment ) , nil
}
2022-02-02 14:41:27 +00:00
// 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 )
2023-02-08 08:13:08 +00:00
b , err := ioutil . ReadAll ( decodedAttachment )
if err != nil {
return nil , err
}
2022-02-02 14:41:27 +00:00
2023-02-08 08:13:08 +00:00
// Initially assume that the attachment is vanilla and alter the state if changes are made to files
a . vanillaFile = true
2022-02-02 14:41:27 +00:00
2023-02-08 08:13:08 +00:00
switch fileExtension {
2022-02-02 14:41:27 +00:00
2023-02-08 08:13:08 +00:00
case ".zip" :
zipReader , err := zip . NewReader ( bytes . NewReader ( b ) , int64 ( len ( b ) ) )
2022-02-02 14:41:27 +00:00
if err != nil {
return nil , err
}
newZipArchive := new ( bytes . Buffer )
zipWriter := zip . NewWriter ( newZipArchive ) // For writing the new archive
2023-02-08 08:13:08 +00:00
// Iterate over every file in the zip and apply the template depending on the filetype
2022-02-02 14:41:27 +00:00
for _ , zipFile := range zipReader . File {
ff , err := zipFile . Open ( )
if err != nil {
return nil , err
}
2023-02-08 08:13:08 +00:00
contents , err := io . ReadAll ( ff )
2022-02-02 14:41:27 +00:00
if err != nil {
return nil , err
}
2023-02-08 08:13:08 +00:00
defer ff . Close ( )
subfileExtension := filepath . Ext ( zipFile . Name )
var tFile io . Reader
switch subfileExtension {
case ".docx" , ".docm" , ".pptx" , ".xlsx" , ".xlsm" :
tFile , err = a . ApplyTemplateMSOffice ( ptx , contents )
2022-02-02 14:41:27 +00:00
if err != nil {
2023-02-08 08:13:08 +00:00
zipWriter . Close ( )
2022-02-02 14:41:27 +00:00
return nil , err
}
2023-02-08 08:13:08 +00:00
case ".txt" , ".html" , ".ics" :
tFile , err = a . ApplyTemplateTextFiles ( ptx , contents )
if err != nil {
zipWriter . Close ( )
return nil , err
2022-02-02 14:41:27 +00:00
}
2023-02-08 08:13:08 +00:00
default :
tFile = bytes . NewReader ( contents )
2022-02-02 14:41:27 +00:00
}
2023-02-08 08:13:08 +00:00
// Write the possibly changed file to the new zip file
tmp , err := zipWriter . Create ( zipFile . Name )
2022-02-02 14:41:27 +00:00
if err != nil {
2023-02-08 08:13:08 +00:00
zipWriter . Close ( )
2022-02-02 14:41:27 +00:00
return nil , err
}
2023-02-08 08:13:08 +00:00
if _ , err := io . Copy ( tmp , tFile ) ; err != nil {
2022-02-02 14:41:27 +00:00
zipWriter . Close ( )
return nil , err
}
}
zipWriter . Close ( )
return bytes . NewReader ( newZipArchive . Bytes ( ) ) , err
2023-02-08 08:13:08 +00:00
case ".docx" , ".docm" , ".pptx" , ".xlsx" , ".xlsm" :
return a . ApplyTemplateMSOffice ( ptx , b )
2022-02-02 14:41:27 +00:00
case ".txt" , ".html" , ".ics" :
2023-02-08 08:13:08 +00:00
return a . ApplyTemplateTextFiles ( ptx , b )
2022-02-02 14:41:27 +00:00
default :
return decodedAttachment , nil // Default is to simply return the file
}
2014-07-12 18:46:38 +00:00
}