github.com/merlinepedra/gopphish-attack@v0.9.0/controllers/phish.go (about)

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