github.com/Ne0nd0g/gophish@v0.7.1-0.20190220040016-11493024a07d/controllers/phish.go (about)

     1  package controllers
     2  
     3  import (
     4  	"compress/gzip"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"net"
     9  	"net/http"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/NYTimes/gziphandler"
    14  	"github.com/gophish/gophish/config"
    15  	ctx "github.com/gophish/gophish/context"
    16  	log "github.com/gophish/gophish/logger"
    17  	"github.com/gophish/gophish/models"
    18  	"github.com/gophish/gophish/util"
    19  	"github.com/gorilla/handlers"
    20  	"github.com/gorilla/mux"
    21  	"github.com/jordan-wright/unindexed"
    22  )
    23  
    24  // ErrInvalidRequest is thrown when a request with an invalid structure is
    25  // received
    26  var ErrInvalidRequest = errors.New("Invalid request")
    27  
    28  // ErrCampaignComplete is thrown when an event is received for a campaign that
    29  // has already been marked as complete.
    30  var ErrCampaignComplete = errors.New("Event received on completed campaign")
    31  
    32  // TransparencyResponse is the JSON response provided when a third-party
    33  // makes a request to the transparency handler.
    34  type TransparencyResponse struct {
    35  	Server         string    `json:"server"`
    36  	ContactAddress string    `json:"contact_address"`
    37  	SendDate       time.Time `json:"send_date"`
    38  }
    39  
    40  // TransparencySuffix (when appended to a valid result ID), will cause Gophish
    41  // to return a transparency response.
    42  const TransparencySuffix = "+"
    43  
    44  // PhishingServerOption is a functional option that is used to configure the
    45  // the phishing server
    46  type PhishingServerOption func(*PhishingServer)
    47  
    48  // PhishingServer is an HTTP server that implements the campaign event
    49  // handlers, such as email open tracking, click tracking, and more.
    50  type PhishingServer struct {
    51  	server         *http.Server
    52  	config         config.PhishServer
    53  	contactAddress string
    54  }
    55  
    56  // NewPhishingServer returns a new instance of the phishing server with
    57  // provided options applied.
    58  func NewPhishingServer(config config.PhishServer, options ...PhishingServerOption) *PhishingServer {
    59  	defaultServer := &http.Server{
    60  		ReadTimeout:  10 * time.Second,
    61  		WriteTimeout: 10 * time.Second,
    62  		Addr:         config.ListenURL,
    63  	}
    64  	ps := &PhishingServer{
    65  		server: defaultServer,
    66  		config: config,
    67  	}
    68  	for _, opt := range options {
    69  		opt(ps)
    70  	}
    71  	ps.registerRoutes()
    72  	return ps
    73  }
    74  
    75  // WithContactAddress sets the contact address used by the transparency
    76  // handlers
    77  func WithContactAddress(addr string) PhishingServerOption {
    78  	return func(ps *PhishingServer) {
    79  		ps.contactAddress = addr
    80  	}
    81  }
    82  
    83  // Start launches the phishing server, listening on the configured address.
    84  func (ps *PhishingServer) Start() error {
    85  	if ps.config.UseTLS {
    86  		err := util.CheckAndCreateSSL(ps.config.CertPath, ps.config.KeyPath)
    87  		if err != nil {
    88  			log.Fatal(err)
    89  			return err
    90  		}
    91  		log.Infof("Starting phishing server at https://%s", ps.config.ListenURL)
    92  		return ps.server.ListenAndServeTLS(ps.config.CertPath, ps.config.KeyPath)
    93  	}
    94  	// If TLS isn't configured, just listen on HTTP
    95  	log.Infof("Starting phishing server at http://%s", ps.config.ListenURL)
    96  	return ps.server.ListenAndServe()
    97  }
    98  
    99  // Shutdown attempts to gracefully shutdown the server.
   100  func (ps *PhishingServer) Shutdown() error {
   101  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
   102  	defer cancel()
   103  	return ps.server.Shutdown(ctx)
   104  }
   105  
   106  // CreatePhishingRouter creates the router that handles phishing connections.
   107  func (ps *PhishingServer) registerRoutes() {
   108  	router := mux.NewRouter()
   109  	fileServer := http.FileServer(unindexed.Dir("./static/endpoint/"))
   110  	router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
   111  	router.HandleFunc("/track", ps.TrackHandler)
   112  	router.HandleFunc("/robots.txt", ps.RobotsHandler)
   113  	router.HandleFunc("/{path:.*}/track", ps.TrackHandler)
   114  	router.HandleFunc("/{path:.*}/report", ps.ReportHandler)
   115  	router.HandleFunc("/report", ps.ReportHandler)
   116  	router.HandleFunc("/{path:.*}", ps.PhishHandler)
   117  
   118  	// Setup GZIP compression
   119  	gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
   120  	phishHandler := gzipWrapper(router)
   121  
   122  	// Setup logging
   123  	phishHandler = handlers.CombinedLoggingHandler(log.Writer(), phishHandler)
   124  	ps.server.Handler = phishHandler
   125  }
   126  
   127  // TrackHandler tracks emails as they are opened, updating the status for the given Result
   128  func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) {
   129  	r, err := setupContext(r)
   130  	if err != nil {
   131  		// Log the error if it wasn't something we can safely ignore
   132  		if err != ErrInvalidRequest && err != ErrCampaignComplete {
   133  			log.Error(err)
   134  		}
   135  		http.NotFound(w, r)
   136  		return
   137  	}
   138  	// Check for a preview
   139  	if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
   140  		http.ServeFile(w, r, "static/images/pixel.png")
   141  		return
   142  	}
   143  	rs := ctx.Get(r, "result").(models.Result)
   144  	rid := ctx.Get(r, "rid").(string)
   145  	d := ctx.Get(r, "details").(models.EventDetails)
   146  
   147  	// Check for a transparency request
   148  	if strings.HasSuffix(rid, TransparencySuffix) {
   149  		ps.TransparencyHandler(w, r)
   150  		return
   151  	}
   152  
   153  	err = rs.HandleEmailOpened(d)
   154  	if err != nil {
   155  		log.Error(err)
   156  	}
   157  	http.ServeFile(w, r, "static/images/pixel.png")
   158  }
   159  
   160  // ReportHandler tracks emails as they are reported, updating the status for the given Result
   161  func (ps *PhishingServer) ReportHandler(w http.ResponseWriter, r *http.Request) {
   162  	r, err := setupContext(r)
   163  	if err != nil {
   164  		// Log the error if it wasn't something we can safely ignore
   165  		if err != ErrInvalidRequest && err != ErrCampaignComplete {
   166  			log.Error(err)
   167  		}
   168  		http.NotFound(w, r)
   169  		return
   170  	}
   171  	// Check for a preview
   172  	if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
   173  		w.WriteHeader(http.StatusNoContent)
   174  		return
   175  	}
   176  	rs := ctx.Get(r, "result").(models.Result)
   177  	rid := ctx.Get(r, "rid").(string)
   178  	d := ctx.Get(r, "details").(models.EventDetails)
   179  
   180  	// Check for a transparency request
   181  	if strings.HasSuffix(rid, TransparencySuffix) {
   182  		ps.TransparencyHandler(w, r)
   183  		return
   184  	}
   185  
   186  	err = rs.HandleEmailReport(d)
   187  	if err != nil {
   188  		log.Error(err)
   189  	}
   190  	w.WriteHeader(http.StatusNoContent)
   191  }
   192  
   193  // PhishHandler handles incoming client connections and registers the associated actions performed
   194  // (such as clicked link, etc.)
   195  func (ps *PhishingServer) PhishHandler(w http.ResponseWriter, r *http.Request) {
   196  	r, err := setupContext(r)
   197  	if err != nil {
   198  		// Log the error if it wasn't something we can safely ignore
   199  		if err != ErrInvalidRequest && err != ErrCampaignComplete {
   200  			log.Error(err)
   201  		}
   202  		http.NotFound(w, r)
   203  		return
   204  	}
   205  	var ptx models.PhishingTemplateContext
   206  	// Check for a preview
   207  	if preview, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
   208  		ptx, err = models.NewPhishingTemplateContext(&preview, preview.BaseRecipient, preview.RId)
   209  		if err != nil {
   210  			log.Error(err)
   211  			http.NotFound(w, r)
   212  			return
   213  		}
   214  		p, err := models.GetPage(preview.PageId, preview.UserId)
   215  		if err != nil {
   216  			log.Error(err)
   217  			http.NotFound(w, r)
   218  			return
   219  		}
   220  		renderPhishResponse(w, r, ptx, p)
   221  		return
   222  	}
   223  	rs := ctx.Get(r, "result").(models.Result)
   224  	rid := ctx.Get(r, "rid").(string)
   225  	c := ctx.Get(r, "campaign").(models.Campaign)
   226  	d := ctx.Get(r, "details").(models.EventDetails)
   227  
   228  	// Check for a transparency request
   229  	if strings.HasSuffix(rid, TransparencySuffix) {
   230  		ps.TransparencyHandler(w, r)
   231  		return
   232  	}
   233  
   234  	p, err := models.GetPage(c.PageId, c.UserId)
   235  	if err != nil {
   236  		log.Error(err)
   237  		http.NotFound(w, r)
   238  		return
   239  	}
   240  	switch {
   241  	case r.Method == "GET":
   242  		err = rs.HandleClickedLink(d)
   243  		if err != nil {
   244  			log.Error(err)
   245  		}
   246  	case r.Method == "POST":
   247  		err = rs.HandleFormSubmit(d)
   248  		if err != nil {
   249  			log.Error(err)
   250  		}
   251  	}
   252  	ptx, err = models.NewPhishingTemplateContext(&c, rs.BaseRecipient, rs.RId)
   253  	if err != nil {
   254  		log.Error(err)
   255  		http.NotFound(w, r)
   256  	}
   257  	renderPhishResponse(w, r, ptx, p)
   258  }
   259  
   260  // renderPhishResponse handles rendering the correct response to the phishing
   261  // connection. This usually involves writing out the page HTML or redirecting
   262  // the user to the correct URL.
   263  func renderPhishResponse(w http.ResponseWriter, r *http.Request, ptx models.PhishingTemplateContext, p models.Page) {
   264  	// If the request was a form submit and a redirect URL was specified, we
   265  	// should send the user to that URL
   266  	if r.Method == "POST" {
   267  		if p.RedirectURL != "" {
   268  			redirectURL, err := models.ExecuteTemplate(p.RedirectURL, ptx)
   269  			if err != nil {
   270  				log.Error(err)
   271  				http.NotFound(w, r)
   272  				return
   273  			}
   274  			http.Redirect(w, r, redirectURL, http.StatusFound)
   275  			return
   276  		}
   277  	}
   278  	// Otherwise, we just need to write out the templated HTML
   279  	html, err := models.ExecuteTemplate(p.HTML, ptx)
   280  	if err != nil {
   281  		log.Error(err)
   282  		http.NotFound(w, r)
   283  		return
   284  	}
   285  	w.Write([]byte(html))
   286  }
   287  
   288  // RobotsHandler prevents search engines, etc. from indexing phishing materials
   289  func (ps *PhishingServer) RobotsHandler(w http.ResponseWriter, r *http.Request) {
   290  	fmt.Fprintln(w, "User-agent: *\nDisallow: /")
   291  }
   292  
   293  // TransparencyHandler returns a TransparencyResponse for the provided result
   294  // and campaign.
   295  func (ps *PhishingServer) TransparencyHandler(w http.ResponseWriter, r *http.Request) {
   296  	rs := ctx.Get(r, "result").(models.Result)
   297  	tr := &TransparencyResponse{
   298  		Server:         config.ServerName,
   299  		SendDate:       rs.SendDate,
   300  		ContactAddress: ps.contactAddress,
   301  	}
   302  	JSONResponse(w, tr, http.StatusOK)
   303  }
   304  
   305  // setupContext handles some of the administrative work around receiving a new
   306  // request, such as checking the result ID, the campaign, etc.
   307  func setupContext(r *http.Request) (*http.Request, error) {
   308  	err := r.ParseForm()
   309  	if err != nil {
   310  		log.Error(err)
   311  		return r, err
   312  	}
   313  	rid := r.Form.Get(models.RecipientParameter)
   314  	if rid == "" {
   315  		return r, ErrInvalidRequest
   316  	}
   317  	// Since we want to support the common case of adding a "+" to indicate a
   318  	// transparency request, we need to take care to handle the case where the
   319  	// request ends with a space, since a "+" is technically reserved for use
   320  	// as a URL encoding of a space.
   321  	if strings.HasSuffix(rid, " ") {
   322  		// We'll trim off the space
   323  		rid = strings.TrimRight(rid, " ")
   324  		// Then we'll add the transparency suffix
   325  		rid = fmt.Sprintf("%s%s", rid, TransparencySuffix)
   326  	}
   327  	// Finally, if this is a transparency request, we'll need to verify that
   328  	// a valid rid has been provided, so we'll look up the result with a
   329  	// trimmed parameter.
   330  	id := strings.TrimSuffix(rid, TransparencySuffix)
   331  	// Check to see if this is a preview or a real result
   332  	if strings.HasPrefix(id, models.PreviewPrefix) {
   333  		rs, err := models.GetEmailRequestByResultId(id)
   334  		if err != nil {
   335  			return r, err
   336  		}
   337  		r = ctx.Set(r, "result", rs)
   338  		return r, nil
   339  	}
   340  	rs, err := models.GetResult(id)
   341  	if err != nil {
   342  		return r, err
   343  	}
   344  	c, err := models.GetCampaign(rs.CampaignId, rs.UserId)
   345  	if err != nil {
   346  		log.Error(err)
   347  		return r, err
   348  	}
   349  	// Don't process events for completed campaigns
   350  	if c.Status == models.CampaignComplete {
   351  		return r, ErrCampaignComplete
   352  	}
   353  	ip, _, err := net.SplitHostPort(r.RemoteAddr)
   354  	if err != nil {
   355  		log.Error(err)
   356  		return r, err
   357  	}
   358  	// Respect X-Forwarded headers
   359  	if fips := r.Header.Get("X-Forwarded-For"); fips != "" {
   360  		ip = strings.Split(fips, ", ")[0]
   361  	}
   362  	// Handle post processing such as GeoIP
   363  	err = rs.UpdateGeo(ip)
   364  	if err != nil {
   365  		log.Error(err)
   366  	}
   367  	d := models.EventDetails{
   368  		Payload: r.Form,
   369  		Browser: make(map[string]string),
   370  	}
   371  	d.Browser["address"] = ip
   372  	d.Browser["user-agent"] = r.Header.Get("User-Agent")
   373  
   374  	r = ctx.Set(r, "rid", rid)
   375  	r = ctx.Set(r, "result", rs)
   376  	r = ctx.Set(r, "campaign", c)
   377  	r = ctx.Set(r, "details", d)
   378  	return r, nil
   379  }