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