github.com/gophish/gophish@v0.12.2-0.20230915144530-8e7929441393/models/attachment.go (about) 1 package models 2 3 import ( 4 "archive/zip" 5 "bytes" 6 "encoding/base64" 7 "io" 8 "io/ioutil" 9 "net/url" 10 "path/filepath" 11 "regexp" 12 "strings" 13 ) 14 15 // Attachment contains the fields and methods for 16 // an email attachment 17 type Attachment struct { 18 Id int64 `json:"-"` 19 TemplateId int64 `json:"-"` 20 Content string `json:"content"` 21 Type string `json:"type"` 22 Name string `json:"name"` 23 vanillaFile bool // Vanilla file has no template variables 24 } 25 26 // Validate ensures that the provided attachment uses the supported template variables correctly. 27 func (a Attachment) Validate() error { 28 vc := ValidationContext{ 29 FromAddress: "foo@bar.com", 30 BaseURL: "http://example.com", 31 } 32 td := Result{ 33 BaseRecipient: BaseRecipient{ 34 Email: "foo@bar.com", 35 FirstName: "Foo", 36 LastName: "Bar", 37 Position: "Test", 38 }, 39 RId: "123456", 40 } 41 ptx, err := NewPhishingTemplateContext(vc, td.BaseRecipient, td.RId) 42 if err != nil { 43 return err 44 } 45 _, err = a.ApplyTemplate(ptx) 46 return err 47 } 48 49 // ApplyTemplate parses different attachment files and applies the supplied phishing template. 50 func (a *Attachment) ApplyTemplate(ptx PhishingTemplateContext) (io.Reader, error) { 51 52 decodedAttachment := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content)) 53 54 // If we've already determined there are no template variables in this attachment return it immediately 55 if a.vanillaFile == true { 56 return decodedAttachment, nil 57 } 58 59 // Decided to use the file extension rather than the content type, as there seems to be quite 60 // a bit of variability with types. e.g sometimes a Word docx file would have: 61 // "application/vnd.openxmlformats-officedocument.wordprocessingml.document" 62 fileExtension := filepath.Ext(a.Name) 63 64 switch fileExtension { 65 66 case ".docx", ".docm", ".pptx", ".xlsx", ".xlsm": 67 // Most modern office formats are xml based and can be unarchived. 68 // .docm and .xlsm files are comprised of xml, and a binary blob for the macro code 69 70 // Zip archives require random access for reading, so it's hard to stream bytes. Solution seems to be to use a buffer. 71 // See https://stackoverflow.com/questions/16946978/how-to-unzip-io-readcloser 72 b := new(bytes.Buffer) 73 b.ReadFrom(decodedAttachment) 74 zipReader, err := zip.NewReader(bytes.NewReader(b.Bytes()), int64(b.Len())) // Create a new zip reader from the file 75 76 if err != nil { 77 return nil, err 78 } 79 80 newZipArchive := new(bytes.Buffer) 81 zipWriter := zip.NewWriter(newZipArchive) // For writing the new archive 82 83 // i. Read each file from the Word document archive 84 // ii. Apply the template to it 85 // iii. Add the templated content to a new zip Word archive 86 a.vanillaFile = true 87 for _, zipFile := range zipReader.File { 88 ff, err := zipFile.Open() 89 if err != nil { 90 return nil, err 91 } 92 defer ff.Close() 93 contents, err := ioutil.ReadAll(ff) 94 if err != nil { 95 return nil, err 96 } 97 subFileExtension := filepath.Ext(zipFile.Name) 98 var tFile string 99 if subFileExtension == ".xml" || subFileExtension == ".rels" { // Ignore other files, e.g binary ones and images 100 // 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. 101 // See https://stackoverflow.com/questions/68287630/disable-url-encoding-for-includepicture-in-microsoft-word 102 rx, _ := regexp.Compile("%7b%7b.([a-zA-Z]+)%7d%7d") 103 contents := rx.ReplaceAllFunc(contents, func(m []byte) []byte { 104 d, err := url.QueryUnescape(string(m)) 105 if err != nil { 106 return m 107 } 108 return []byte(d) 109 }) 110 111 // For each file apply the template. 112 tFile, err = ExecuteTemplate(string(contents), ptx) 113 if err != nil { 114 zipWriter.Close() // Don't use defer when writing files https://www.joeshaw.org/dont-defer-close-on-writable-files/ 115 return nil, err 116 } 117 // Check if the subfile changed. We only need this to be set once to know in the future to check the 'parent' file 118 if tFile != string(contents) { 119 a.vanillaFile = false 120 } 121 } else { 122 tFile = string(contents) // Could move this to the declaration of tFile, but might be confusing to read 123 } 124 // Write new Word archive 125 newZipFile, err := zipWriter.Create(zipFile.Name) 126 if err != nil { 127 zipWriter.Close() // Don't use defer when writing files https://www.joeshaw.org/dont-defer-close-on-writable-files/ 128 return nil, err 129 } 130 _, err = newZipFile.Write([]byte(tFile)) 131 if err != nil { 132 zipWriter.Close() 133 return nil, err 134 } 135 } 136 zipWriter.Close() 137 return bytes.NewReader(newZipArchive.Bytes()), err 138 139 case ".txt", ".html", ".ics": 140 b, err := ioutil.ReadAll(decodedAttachment) 141 if err != nil { 142 return nil, err 143 } 144 processedAttachment, err := ExecuteTemplate(string(b), ptx) 145 if err != nil { 146 return nil, err 147 } 148 if processedAttachment == string(b) { 149 a.vanillaFile = true 150 } 151 return strings.NewReader(processedAttachment), nil 152 default: 153 return decodedAttachment, nil // Default is to simply return the file 154 } 155 156 }