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  }