github.com/glennzw/gophish@v0.8.1-0.20190824020715-24fe998a3aa0/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() error { 86 if ps.config.UseTLS { 87 err := util.CheckAndCreateSSL(ps.config.CertPath, ps.config.KeyPath) 88 if err != nil { 89 log.Fatal(err) 90 return err 91 } 92 log.Infof("Starting phishing server at https://%s", ps.config.ListenURL) 93 return ps.server.ListenAndServeTLS(ps.config.CertPath, ps.config.KeyPath) 94 } 95 // If TLS isn't configured, just listen on HTTP 96 log.Infof("Starting phishing server at http://%s", ps.config.ListenURL) 97 return ps.server.ListenAndServe() 98 } 99 100 // Shutdown attempts to gracefully shutdown the server. 101 func (ps *PhishingServer) Shutdown() error { 102 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 103 defer cancel() 104 return ps.server.Shutdown(ctx) 105 } 106 107 // CreatePhishingRouter creates the router that handles phishing connections. 108 func (ps *PhishingServer) registerRoutes() { 109 router := mux.NewRouter() 110 fileServer := http.FileServer(unindexed.Dir("./static/endpoint/")) 111 router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer)) 112 router.HandleFunc("/track", ps.TrackHandler) 113 router.HandleFunc("/robots.txt", ps.RobotsHandler) 114 router.HandleFunc("/{path:.*}/track", ps.TrackHandler) 115 router.HandleFunc("/{path:.*}/report", ps.ReportHandler) 116 router.HandleFunc("/report", ps.ReportHandler) 117 router.HandleFunc("/{path:.*}", ps.PhishHandler) 118 119 // Setup GZIP compression 120 gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression) 121 phishHandler := gzipWrapper(router) 122 123 // Setup logging 124 phishHandler = handlers.CombinedLoggingHandler(log.Writer(), phishHandler) 125 ps.server.Handler = phishHandler 126 } 127 128 // TrackHandler tracks emails as they are opened, updating the status for the given Result 129 func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) { 130 r, err := setupContext(r) 131 if err != nil { 132 // Log the error if it wasn't something we can safely ignore 133 if err != ErrInvalidRequest && err != ErrCampaignComplete { 134 log.Error(err) 135 } 136 http.NotFound(w, r) 137 return 138 } 139 // Check for a preview 140 if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok { 141 http.ServeFile(w, r, "static/images/pixel.png") 142 return 143 } 144 rs := ctx.Get(r, "result").(models.Result) 145 rid := ctx.Get(r, "rid").(string) 146 d := ctx.Get(r, "details").(models.EventDetails) 147 148 // Check for a transparency request 149 if strings.HasSuffix(rid, TransparencySuffix) { 150 ps.TransparencyHandler(w, r) 151 return 152 } 153 154 err = rs.HandleEmailOpened(d) 155 if err != nil { 156 log.Error(err) 157 } 158 http.ServeFile(w, r, "static/images/pixel.png") 159 } 160 161 // ReportHandler tracks emails as they are reported, updating the status for the given Result 162 func (ps *PhishingServer) ReportHandler(w http.ResponseWriter, r *http.Request) { 163 r, err := setupContext(r) 164 w.Header().Set("Access-Control-Allow-Origin", "*") // To allow Chrome extensions (or other pages) to report a campaign without violating CORS 165 if err != nil { 166 // Log the error if it wasn't something we can safely ignore 167 if err != ErrInvalidRequest && err != ErrCampaignComplete { 168 log.Error(err) 169 } 170 http.NotFound(w, r) 171 return 172 } 173 // Check for a preview 174 if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok { 175 w.WriteHeader(http.StatusNoContent) 176 return 177 } 178 rs := ctx.Get(r, "result").(models.Result) 179 rid := ctx.Get(r, "rid").(string) 180 d := ctx.Get(r, "details").(models.EventDetails) 181 182 // Check for a transparency request 183 if strings.HasSuffix(rid, TransparencySuffix) { 184 ps.TransparencyHandler(w, r) 185 return 186 } 187 188 err = rs.HandleEmailReport(d) 189 if err != nil { 190 log.Error(err) 191 } 192 w.WriteHeader(http.StatusNoContent) 193 } 194 195 // PhishHandler handles incoming client connections and registers the associated actions performed 196 // (such as clicked link, etc.) 197 func (ps *PhishingServer) PhishHandler(w http.ResponseWriter, r *http.Request) { 198 r, err := setupContext(r) 199 if err != nil { 200 // Log the error if it wasn't something we can safely ignore 201 if err != ErrInvalidRequest && err != ErrCampaignComplete { 202 log.Error(err) 203 } 204 http.NotFound(w, r) 205 return 206 } 207 w.Header().Set("X-Server", config.ServerName) // Useful for checking if this is a GoPhish server (e.g. for campaign reporting plugins) 208 var ptx models.PhishingTemplateContext 209 // Check for a preview 210 if preview, ok := ctx.Get(r, "result").(models.EmailRequest); ok { 211 ptx, err = models.NewPhishingTemplateContext(&preview, preview.BaseRecipient, preview.RId) 212 if err != nil { 213 log.Error(err) 214 http.NotFound(w, r) 215 return 216 } 217 p, err := models.GetPage(preview.PageId, preview.UserId) 218 if err != nil { 219 log.Error(err) 220 http.NotFound(w, r) 221 return 222 } 223 renderPhishResponse(w, r, ptx, p) 224 return 225 } 226 rs := ctx.Get(r, "result").(models.Result) 227 rid := ctx.Get(r, "rid").(string) 228 c := ctx.Get(r, "campaign").(models.Campaign) 229 d := ctx.Get(r, "details").(models.EventDetails) 230 231 // Check for a transparency request 232 if strings.HasSuffix(rid, TransparencySuffix) { 233 ps.TransparencyHandler(w, r) 234 return 235 } 236 237 p, err := models.GetPage(c.PageId, c.UserId) 238 if err != nil { 239 log.Error(err) 240 http.NotFound(w, r) 241 return 242 } 243 switch { 244 case r.Method == "GET": 245 err = rs.HandleClickedLink(d) 246 if err != nil { 247 log.Error(err) 248 } 249 case r.Method == "POST": 250 err = rs.HandleFormSubmit(d) 251 if err != nil { 252 log.Error(err) 253 } 254 } 255 ptx, err = models.NewPhishingTemplateContext(&c, rs.BaseRecipient, rs.RId) 256 if err != nil { 257 log.Error(err) 258 http.NotFound(w, r) 259 } 260 renderPhishResponse(w, r, ptx, p) 261 } 262 263 // renderPhishResponse handles rendering the correct response to the phishing 264 // connection. This usually involves writing out the page HTML or redirecting 265 // the user to the correct URL. 266 func renderPhishResponse(w http.ResponseWriter, r *http.Request, ptx models.PhishingTemplateContext, p models.Page) { 267 // If the request was a form submit and a redirect URL was specified, we 268 // should send the user to that URL 269 if r.Method == "POST" { 270 if p.RedirectURL != "" { 271 redirectURL, err := models.ExecuteTemplate(p.RedirectURL, ptx) 272 if err != nil { 273 log.Error(err) 274 http.NotFound(w, r) 275 return 276 } 277 http.Redirect(w, r, redirectURL, http.StatusFound) 278 return 279 } 280 } 281 // Otherwise, we just need to write out the templated HTML 282 html, err := models.ExecuteTemplate(p.HTML, ptx) 283 if err != nil { 284 log.Error(err) 285 http.NotFound(w, r) 286 return 287 } 288 w.Write([]byte(html)) 289 } 290 291 // RobotsHandler prevents search engines, etc. from indexing phishing materials 292 func (ps *PhishingServer) RobotsHandler(w http.ResponseWriter, r *http.Request) { 293 fmt.Fprintln(w, "User-agent: *\nDisallow: /") 294 } 295 296 // TransparencyHandler returns a TransparencyResponse for the provided result 297 // and campaign. 298 func (ps *PhishingServer) TransparencyHandler(w http.ResponseWriter, r *http.Request) { 299 rs := ctx.Get(r, "result").(models.Result) 300 tr := &TransparencyResponse{ 301 Server: config.ServerName, 302 SendDate: rs.SendDate, 303 ContactAddress: ps.contactAddress, 304 } 305 api.JSONResponse(w, tr, http.StatusOK) 306 } 307 308 // setupContext handles some of the administrative work around receiving a new 309 // request, such as checking the result ID, the campaign, etc. 310 func setupContext(r *http.Request) (*http.Request, error) { 311 err := r.ParseForm() 312 if err != nil { 313 log.Error(err) 314 return r, err 315 } 316 rid := r.Form.Get(models.RecipientParameter) 317 if rid == "" { 318 return r, ErrInvalidRequest 319 } 320 // Since we want to support the common case of adding a "+" to indicate a 321 // transparency request, we need to take care to handle the case where the 322 // request ends with a space, since a "+" is technically reserved for use 323 // as a URL encoding of a space. 324 if strings.HasSuffix(rid, " ") { 325 // We'll trim off the space 326 rid = strings.TrimRight(rid, " ") 327 // Then we'll add the transparency suffix 328 rid = fmt.Sprintf("%s%s", rid, TransparencySuffix) 329 } 330 // Finally, if this is a transparency request, we'll need to verify that 331 // a valid rid has been provided, so we'll look up the result with a 332 // trimmed parameter. 333 id := strings.TrimSuffix(rid, TransparencySuffix) 334 // Check to see if this is a preview or a real result 335 if strings.HasPrefix(id, models.PreviewPrefix) { 336 rs, err := models.GetEmailRequestByResultId(id) 337 if err != nil { 338 return r, err 339 } 340 r = ctx.Set(r, "result", rs) 341 return r, nil 342 } 343 rs, err := models.GetResult(id) 344 if err != nil { 345 return r, err 346 } 347 c, err := models.GetCampaign(rs.CampaignId, rs.UserId) 348 if err != nil { 349 log.Error(err) 350 return r, err 351 } 352 // Don't process events for completed campaigns 353 if c.Status == models.CampaignComplete { 354 return r, ErrCampaignComplete 355 } 356 ip, _, err := net.SplitHostPort(r.RemoteAddr) 357 if err != nil { 358 log.Error(err) 359 return r, err 360 } 361 // Respect X-Forwarded headers 362 if fips := r.Header.Get("X-Forwarded-For"); fips != "" { 363 ip = strings.Split(fips, ", ")[0] 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 }