github.com/dzsibi/gophish@v0.7.1-0.20190719042945-1f16c7237d0d/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 if err != nil { 165 // Log the error if it wasn't something we can safely ignore 166 if err != ErrInvalidRequest && err != ErrCampaignComplete { 167 log.Error(err) 168 } 169 http.NotFound(w, r) 170 return 171 } 172 // Check for a preview 173 if _, ok := ctx.Get(r, "result").(models.EmailRequest); ok { 174 w.WriteHeader(http.StatusNoContent) 175 return 176 } 177 rs := ctx.Get(r, "result").(models.Result) 178 rid := ctx.Get(r, "rid").(string) 179 d := ctx.Get(r, "details").(models.EventDetails) 180 181 // Check for a transparency request 182 if strings.HasSuffix(rid, TransparencySuffix) { 183 ps.TransparencyHandler(w, r) 184 return 185 } 186 187 err = rs.HandleEmailReport(d) 188 if err != nil { 189 log.Error(err) 190 } 191 w.WriteHeader(http.StatusNoContent) 192 } 193 194 // PhishHandler handles incoming client connections and registers the associated actions performed 195 // (such as clicked link, etc.) 196 func (ps *PhishingServer) PhishHandler(w http.ResponseWriter, r *http.Request) { 197 r, err := setupContext(r) 198 if err != nil { 199 // Log the error if it wasn't something we can safely ignore 200 if err != ErrInvalidRequest && err != ErrCampaignComplete { 201 log.Error(err) 202 } 203 http.NotFound(w, r) 204 return 205 } 206 var ptx models.PhishingTemplateContext 207 // Check for a preview 208 if preview, ok := ctx.Get(r, "result").(models.EmailRequest); ok { 209 ptx, err = models.NewPhishingTemplateContext(&preview, preview.BaseRecipient, preview.RId) 210 if err != nil { 211 log.Error(err) 212 http.NotFound(w, r) 213 return 214 } 215 p, err := models.GetPage(preview.PageId, preview.UserId) 216 if err != nil { 217 log.Error(err) 218 http.NotFound(w, r) 219 return 220 } 221 renderPhishResponse(w, r, ptx, p) 222 return 223 } 224 rs := ctx.Get(r, "result").(models.Result) 225 rid := ctx.Get(r, "rid").(string) 226 c := ctx.Get(r, "campaign").(models.Campaign) 227 d := ctx.Get(r, "details").(models.EventDetails) 228 229 // Check for a transparency request 230 if strings.HasSuffix(rid, TransparencySuffix) { 231 ps.TransparencyHandler(w, r) 232 return 233 } 234 235 p, err := models.GetPage(c.PageId, c.UserId) 236 if err != nil { 237 log.Error(err) 238 http.NotFound(w, r) 239 return 240 } 241 switch { 242 case r.Method == "GET": 243 err = rs.HandleClickedLink(d) 244 if err != nil { 245 log.Error(err) 246 } 247 case r.Method == "POST": 248 err = rs.HandleFormSubmit(d) 249 if err != nil { 250 log.Error(err) 251 } 252 } 253 ptx, err = models.NewPhishingTemplateContext(&c, rs.BaseRecipient, rs.RId) 254 if err != nil { 255 log.Error(err) 256 http.NotFound(w, r) 257 } 258 renderPhishResponse(w, r, ptx, p) 259 } 260 261 // renderPhishResponse handles rendering the correct response to the phishing 262 // connection. This usually involves writing out the page HTML or redirecting 263 // the user to the correct URL. 264 func renderPhishResponse(w http.ResponseWriter, r *http.Request, ptx models.PhishingTemplateContext, p models.Page) { 265 // If the request was a form submit and a redirect URL was specified, we 266 // should send the user to that URL 267 if r.Method == "POST" { 268 if p.RedirectURL != "" { 269 redirectURL, err := models.ExecuteTemplate(p.RedirectURL, ptx) 270 if err != nil { 271 log.Error(err) 272 http.NotFound(w, r) 273 return 274 } 275 http.Redirect(w, r, redirectURL, http.StatusFound) 276 return 277 } 278 } 279 // Otherwise, we just need to write out the templated HTML 280 html, err := models.ExecuteTemplate(p.HTML, ptx) 281 if err != nil { 282 log.Error(err) 283 http.NotFound(w, r) 284 return 285 } 286 w.Write([]byte(html)) 287 } 288 289 // RobotsHandler prevents search engines, etc. from indexing phishing materials 290 func (ps *PhishingServer) RobotsHandler(w http.ResponseWriter, r *http.Request) { 291 fmt.Fprintln(w, "User-agent: *\nDisallow: /") 292 } 293 294 // TransparencyHandler returns a TransparencyResponse for the provided result 295 // and campaign. 296 func (ps *PhishingServer) TransparencyHandler(w http.ResponseWriter, r *http.Request) { 297 rs := ctx.Get(r, "result").(models.Result) 298 tr := &TransparencyResponse{ 299 Server: config.ServerName, 300 SendDate: rs.SendDate, 301 ContactAddress: ps.contactAddress, 302 } 303 api.JSONResponse(w, tr, http.StatusOK) 304 } 305 306 // setupContext handles some of the administrative work around receiving a new 307 // request, such as checking the result ID, the campaign, etc. 308 func setupContext(r *http.Request) (*http.Request, error) { 309 err := r.ParseForm() 310 if err != nil { 311 log.Error(err) 312 return r, err 313 } 314 rid := r.Form.Get(models.RecipientParameter) 315 if rid == "" { 316 return r, ErrInvalidRequest 317 } 318 // Since we want to support the common case of adding a "+" to indicate a 319 // transparency request, we need to take care to handle the case where the 320 // request ends with a space, since a "+" is technically reserved for use 321 // as a URL encoding of a space. 322 if strings.HasSuffix(rid, " ") { 323 // We'll trim off the space 324 rid = strings.TrimRight(rid, " ") 325 // Then we'll add the transparency suffix 326 rid = fmt.Sprintf("%s%s", rid, TransparencySuffix) 327 } 328 // Finally, if this is a transparency request, we'll need to verify that 329 // a valid rid has been provided, so we'll look up the result with a 330 // trimmed parameter. 331 id := strings.TrimSuffix(rid, TransparencySuffix) 332 // Check to see if this is a preview or a real result 333 if strings.HasPrefix(id, models.PreviewPrefix) { 334 rs, err := models.GetEmailRequestByResultId(id) 335 if err != nil { 336 return r, err 337 } 338 r = ctx.Set(r, "result", rs) 339 return r, nil 340 } 341 rs, err := models.GetResult(id) 342 if err != nil { 343 return r, err 344 } 345 c, err := models.GetCampaign(rs.CampaignId, rs.UserId) 346 if err != nil { 347 log.Error(err) 348 return r, err 349 } 350 // Don't process events for completed campaigns 351 if c.Status == models.CampaignComplete { 352 return r, ErrCampaignComplete 353 } 354 ip, _, err := net.SplitHostPort(r.RemoteAddr) 355 if err != nil { 356 log.Error(err) 357 return r, err 358 } 359 // Respect X-Forwarded headers 360 if fips := r.Header.Get("X-Forwarded-For"); fips != "" { 361 ip = strings.Split(fips, ", ")[0] 362 } 363 // Handle post processing such as GeoIP 364 err = rs.UpdateGeo(ip) 365 if err != nil { 366 log.Error(err) 367 } 368 d := models.EventDetails{ 369 Payload: r.Form, 370 Browser: make(map[string]string), 371 } 372 d.Browser["address"] = ip 373 d.Browser["user-agent"] = r.Header.Get("User-Agent") 374 375 r = ctx.Set(r, "rid", rid) 376 r = ctx.Set(r, "result", rs) 377 r = ctx.Set(r, "campaign", c) 378 r = ctx.Set(r, "details", d) 379 return r, nil 380 }