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