github.com/ArminBerberovic/gophish@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 }