github.com/lunarobliq/gophish@v0.8.1-0.20230523153303-93511002234d/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 }