github.com/gophish/gophish@v0.12.2-0.20230915144530-8e7929441393/controllers/route.go (about) 1 package controllers 2 3 import ( 4 "compress/gzip" 5 "context" 6 "crypto/tls" 7 "html/template" 8 "net/http" 9 "net/url" 10 "strings" 11 "time" 12 13 "github.com/NYTimes/gziphandler" 14 "github.com/gophish/gophish/auth" 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 mid "github.com/gophish/gophish/middleware" 20 "github.com/gophish/gophish/middleware/ratelimit" 21 "github.com/gophish/gophish/models" 22 "github.com/gophish/gophish/util" 23 "github.com/gophish/gophish/worker" 24 "github.com/gorilla/csrf" 25 "github.com/gorilla/handlers" 26 "github.com/gorilla/mux" 27 "github.com/gorilla/sessions" 28 "github.com/jordan-wright/unindexed" 29 ) 30 31 // AdminServerOption is a functional option that is used to configure the 32 // admin server 33 type AdminServerOption func(*AdminServer) 34 35 // AdminServer is an HTTP server that implements the administrative Gophish 36 // handlers, including the dashboard and REST API. 37 type AdminServer struct { 38 server *http.Server 39 worker worker.Worker 40 config config.AdminServer 41 limiter *ratelimit.PostLimiter 42 } 43 44 var defaultTLSConfig = &tls.Config{ 45 PreferServerCipherSuites: true, 46 CurvePreferences: []tls.CurveID{ 47 tls.X25519, 48 tls.CurveP256, 49 }, 50 MinVersion: tls.VersionTLS12, 51 CipherSuites: []uint16{ 52 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 53 tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 54 tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 55 tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 56 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 57 tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 58 59 // Kept for backwards compatibility with some clients 60 tls.TLS_RSA_WITH_AES_256_GCM_SHA384, 61 tls.TLS_RSA_WITH_AES_128_GCM_SHA256, 62 }, 63 } 64 65 // WithWorker is an option that sets the background worker. 66 func WithWorker(w worker.Worker) AdminServerOption { 67 return func(as *AdminServer) { 68 as.worker = w 69 } 70 } 71 72 // NewAdminServer returns a new instance of the AdminServer with the 73 // provided config and options applied. 74 func NewAdminServer(config config.AdminServer, options ...AdminServerOption) *AdminServer { 75 defaultWorker, _ := worker.New() 76 defaultServer := &http.Server{ 77 ReadTimeout: 10 * time.Second, 78 Addr: config.ListenURL, 79 } 80 defaultLimiter := ratelimit.NewPostLimiter() 81 as := &AdminServer{ 82 worker: defaultWorker, 83 server: defaultServer, 84 limiter: defaultLimiter, 85 config: config, 86 } 87 for _, opt := range options { 88 opt(as) 89 } 90 as.registerRoutes() 91 return as 92 } 93 94 // Start launches the admin server, listening on the configured address. 95 func (as *AdminServer) Start() { 96 if as.worker != nil { 97 go as.worker.Start() 98 } 99 if as.config.UseTLS { 100 // Only support TLS 1.2 and above - ref #1691, #1689 101 as.server.TLSConfig = defaultTLSConfig 102 err := util.CheckAndCreateSSL(as.config.CertPath, as.config.KeyPath) 103 if err != nil { 104 log.Fatal(err) 105 } 106 log.Infof("Starting admin server at https://%s", as.config.ListenURL) 107 log.Fatal(as.server.ListenAndServeTLS(as.config.CertPath, as.config.KeyPath)) 108 } 109 // If TLS isn't configured, just listen on HTTP 110 log.Infof("Starting admin server at http://%s", as.config.ListenURL) 111 log.Fatal(as.server.ListenAndServe()) 112 } 113 114 // Shutdown attempts to gracefully shutdown the server. 115 func (as *AdminServer) Shutdown() error { 116 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 117 defer cancel() 118 return as.server.Shutdown(ctx) 119 } 120 121 // SetupAdminRoutes creates the routes for handling requests to the web interface. 122 // This function returns an http.Handler to be used in http.ListenAndServe(). 123 func (as *AdminServer) registerRoutes() { 124 router := mux.NewRouter() 125 // Base Front-end routes 126 router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin)) 127 router.HandleFunc("/login", mid.Use(as.Login, as.limiter.Limit)) 128 router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin)) 129 router.HandleFunc("/reset_password", mid.Use(as.ResetPassword, mid.RequireLogin)) 130 router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin)) 131 router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin)) 132 router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin)) 133 router.HandleFunc("/groups", mid.Use(as.Groups, mid.RequireLogin)) 134 router.HandleFunc("/landing_pages", mid.Use(as.LandingPages, mid.RequireLogin)) 135 router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, mid.RequireLogin)) 136 router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin)) 137 router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin)) 138 router.HandleFunc("/webhooks", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin)) 139 router.HandleFunc("/impersonate", mid.Use(as.Impersonate, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin)) 140 // Create the API routes 141 api := api.NewServer( 142 api.WithWorker(as.worker), 143 api.WithLimiter(as.limiter), 144 ) 145 router.PathPrefix("/api/").Handler(api) 146 147 // Setup static file serving 148 router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/"))) 149 150 // Setup CSRF Protection 151 csrfKey := []byte(as.config.CSRFKey) 152 if len(csrfKey) == 0 { 153 csrfKey = []byte(auth.GenerateSecureKey(auth.APIKeyLength)) 154 } 155 csrfHandler := csrf.Protect(csrfKey, 156 csrf.FieldName("csrf_token"), 157 csrf.Secure(as.config.UseTLS), 158 csrf.TrustedOrigins(as.config.TrustedOrigins)) 159 adminHandler := csrfHandler(router) 160 adminHandler = mid.Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext, mid.ApplySecurityHeaders) 161 162 // Setup GZIP compression 163 gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression) 164 adminHandler = gzipWrapper(adminHandler) 165 166 // Respect X-Forwarded-For and X-Real-IP headers in case we're behind a 167 // reverse proxy. 168 adminHandler = handlers.ProxyHeaders(adminHandler) 169 170 // Setup logging 171 adminHandler = handlers.CombinedLoggingHandler(log.Writer(), adminHandler) 172 as.server.Handler = adminHandler 173 } 174 175 type templateParams struct { 176 Title string 177 Flashes []interface{} 178 User models.User 179 Token string 180 Version string 181 ModifySystem bool 182 } 183 184 // newTemplateParams returns the default template parameters for a user and 185 // the CSRF token. 186 func newTemplateParams(r *http.Request) templateParams { 187 user := ctx.Get(r, "user").(models.User) 188 session := ctx.Get(r, "session").(*sessions.Session) 189 modifySystem, _ := user.HasPermission(models.PermissionModifySystem) 190 return templateParams{ 191 Token: csrf.Token(r), 192 User: user, 193 ModifySystem: modifySystem, 194 Version: config.Version, 195 Flashes: session.Flashes(), 196 } 197 } 198 199 // Base handles the default path and template execution 200 func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) { 201 params := newTemplateParams(r) 202 params.Title = "Dashboard" 203 getTemplate(w, "dashboard").ExecuteTemplate(w, "base", params) 204 } 205 206 // Campaigns handles the default path and template execution 207 func (as *AdminServer) Campaigns(w http.ResponseWriter, r *http.Request) { 208 params := newTemplateParams(r) 209 params.Title = "Campaigns" 210 getTemplate(w, "campaigns").ExecuteTemplate(w, "base", params) 211 } 212 213 // CampaignID handles the default path and template execution 214 func (as *AdminServer) CampaignID(w http.ResponseWriter, r *http.Request) { 215 params := newTemplateParams(r) 216 params.Title = "Campaign Results" 217 getTemplate(w, "campaign_results").ExecuteTemplate(w, "base", params) 218 } 219 220 // Templates handles the default path and template execution 221 func (as *AdminServer) Templates(w http.ResponseWriter, r *http.Request) { 222 params := newTemplateParams(r) 223 params.Title = "Email Templates" 224 getTemplate(w, "templates").ExecuteTemplate(w, "base", params) 225 } 226 227 // Groups handles the default path and template execution 228 func (as *AdminServer) Groups(w http.ResponseWriter, r *http.Request) { 229 params := newTemplateParams(r) 230 params.Title = "Users & Groups" 231 getTemplate(w, "groups").ExecuteTemplate(w, "base", params) 232 } 233 234 // LandingPages handles the default path and template execution 235 func (as *AdminServer) LandingPages(w http.ResponseWriter, r *http.Request) { 236 params := newTemplateParams(r) 237 params.Title = "Landing Pages" 238 getTemplate(w, "landing_pages").ExecuteTemplate(w, "base", params) 239 } 240 241 // SendingProfiles handles the default path and template execution 242 func (as *AdminServer) SendingProfiles(w http.ResponseWriter, r *http.Request) { 243 params := newTemplateParams(r) 244 params.Title = "Sending Profiles" 245 getTemplate(w, "sending_profiles").ExecuteTemplate(w, "base", params) 246 } 247 248 // Settings handles the changing of settings 249 func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) { 250 switch { 251 case r.Method == "GET": 252 params := newTemplateParams(r) 253 params.Title = "Settings" 254 session := ctx.Get(r, "session").(*sessions.Session) 255 session.Save(r, w) 256 getTemplate(w, "settings").ExecuteTemplate(w, "base", params) 257 case r.Method == "POST": 258 u := ctx.Get(r, "user").(models.User) 259 currentPw := r.FormValue("current_password") 260 newPassword := r.FormValue("new_password") 261 confirmPassword := r.FormValue("confirm_new_password") 262 // Check the current password 263 err := auth.ValidatePassword(currentPw, u.Hash) 264 msg := models.Response{Success: true, Message: "Settings Updated Successfully"} 265 if err != nil { 266 msg.Message = err.Error() 267 msg.Success = false 268 api.JSONResponse(w, msg, http.StatusBadRequest) 269 return 270 } 271 newHash, err := auth.ValidatePasswordChange(u.Hash, newPassword, confirmPassword) 272 if err != nil { 273 msg.Message = err.Error() 274 msg.Success = false 275 api.JSONResponse(w, msg, http.StatusBadRequest) 276 return 277 } 278 u.Hash = string(newHash) 279 if err = models.PutUser(&u); err != nil { 280 msg.Message = err.Error() 281 msg.Success = false 282 api.JSONResponse(w, msg, http.StatusInternalServerError) 283 return 284 } 285 api.JSONResponse(w, msg, http.StatusOK) 286 } 287 } 288 289 // UserManagement is an admin-only handler that allows for the registration 290 // and management of user accounts within Gophish. 291 func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) { 292 params := newTemplateParams(r) 293 params.Title = "User Management" 294 getTemplate(w, "users").ExecuteTemplate(w, "base", params) 295 } 296 297 func (as *AdminServer) nextOrIndex(w http.ResponseWriter, r *http.Request) { 298 next := "/" 299 url, err := url.Parse(r.FormValue("next")) 300 if err == nil { 301 path := url.EscapedPath() 302 if path != "" { 303 next = "/" + strings.TrimLeft(path, "/") 304 } 305 } 306 http.Redirect(w, r, next, http.StatusFound) 307 } 308 309 func (as *AdminServer) handleInvalidLogin(w http.ResponseWriter, r *http.Request, message string) { 310 session := ctx.Get(r, "session").(*sessions.Session) 311 Flash(w, r, "danger", message) 312 params := struct { 313 User models.User 314 Title string 315 Flashes []interface{} 316 Token string 317 }{Title: "Login", Token: csrf.Token(r)} 318 params.Flashes = session.Flashes() 319 session.Save(r, w) 320 templates := template.New("template") 321 _, err := templates.ParseFiles("templates/login.html", "templates/flashes.html") 322 if err != nil { 323 log.Error(err) 324 } 325 // w.Header().Set("Content-Type", "text/html; charset=utf-8") 326 w.WriteHeader(http.StatusUnauthorized) 327 template.Must(templates, err).ExecuteTemplate(w, "base", params) 328 } 329 330 // Webhooks is an admin-only handler that handles webhooks 331 func (as *AdminServer) Webhooks(w http.ResponseWriter, r *http.Request) { 332 params := newTemplateParams(r) 333 params.Title = "Webhooks" 334 getTemplate(w, "webhooks").ExecuteTemplate(w, "base", params) 335 } 336 337 // Impersonate allows an admin to login to a user account without needing the password 338 func (as *AdminServer) Impersonate(w http.ResponseWriter, r *http.Request) { 339 340 if r.Method == "POST" { 341 username := r.FormValue("username") 342 u, err := models.GetUserByUsername(username) 343 if err != nil { 344 log.Error(err) 345 http.Error(w, err.Error(), http.StatusNotFound) 346 return 347 } 348 session := ctx.Get(r, "session").(*sessions.Session) 349 session.Values["id"] = u.Id 350 session.Save(r, w) 351 } 352 http.Redirect(w, r, "/", http.StatusFound) 353 } 354 355 // Login handles the authentication flow for a user. If credentials are valid, 356 // a session is created 357 func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) { 358 params := struct { 359 User models.User 360 Title string 361 Flashes []interface{} 362 Token string 363 }{Title: "Login", Token: csrf.Token(r)} 364 session := ctx.Get(r, "session").(*sessions.Session) 365 switch { 366 case r.Method == "GET": 367 params.Flashes = session.Flashes() 368 session.Save(r, w) 369 templates := template.New("template") 370 _, err := templates.ParseFiles("templates/login.html", "templates/flashes.html") 371 if err != nil { 372 log.Error(err) 373 } 374 template.Must(templates, err).ExecuteTemplate(w, "base", params) 375 case r.Method == "POST": 376 // Find the user with the provided username 377 username, password := r.FormValue("username"), r.FormValue("password") 378 u, err := models.GetUserByUsername(username) 379 if err != nil { 380 log.Error(err) 381 as.handleInvalidLogin(w, r, "Invalid Username/Password") 382 return 383 } 384 // Validate the user's password 385 err = auth.ValidatePassword(password, u.Hash) 386 if err != nil { 387 log.Error(err) 388 as.handleInvalidLogin(w, r, "Invalid Username/Password") 389 return 390 } 391 if u.AccountLocked { 392 as.handleInvalidLogin(w, r, "Account Locked") 393 return 394 } 395 u.LastLogin = time.Now().UTC() 396 err = models.PutUser(&u) 397 if err != nil { 398 log.Error(err) 399 } 400 // If we've logged in, save the session and redirect to the dashboard 401 session.Values["id"] = u.Id 402 session.Save(r, w) 403 as.nextOrIndex(w, r) 404 } 405 } 406 407 // Logout destroys the current user session 408 func (as *AdminServer) Logout(w http.ResponseWriter, r *http.Request) { 409 session := ctx.Get(r, "session").(*sessions.Session) 410 delete(session.Values, "id") 411 Flash(w, r, "success", "You have successfully logged out") 412 session.Save(r, w) 413 http.Redirect(w, r, "/login", http.StatusFound) 414 } 415 416 // ResetPassword handles the password reset flow when a password change is 417 // required either by the Gophish system or an administrator. 418 // 419 // This handler is meant to be used when a user is required to reset their 420 // password, not just when they want to. 421 // 422 // This is an important distinction since in this handler we don't require 423 // the user to re-enter their current password, as opposed to the flow 424 // through the settings handler. 425 // 426 // To that end, if the user doesn't require a password change, we will 427 // redirect them to the settings page. 428 func (as *AdminServer) ResetPassword(w http.ResponseWriter, r *http.Request) { 429 u := ctx.Get(r, "user").(models.User) 430 session := ctx.Get(r, "session").(*sessions.Session) 431 if !u.PasswordChangeRequired { 432 Flash(w, r, "info", "Please reset your password through the settings page") 433 session.Save(r, w) 434 http.Redirect(w, r, "/settings", http.StatusTemporaryRedirect) 435 return 436 } 437 params := newTemplateParams(r) 438 params.Title = "Reset Password" 439 switch { 440 case r.Method == http.MethodGet: 441 params.Flashes = session.Flashes() 442 session.Save(r, w) 443 getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params) 444 return 445 case r.Method == http.MethodPost: 446 newPassword := r.FormValue("password") 447 confirmPassword := r.FormValue("confirm_password") 448 newHash, err := auth.ValidatePasswordChange(u.Hash, newPassword, confirmPassword) 449 if err != nil { 450 Flash(w, r, "danger", err.Error()) 451 params.Flashes = session.Flashes() 452 session.Save(r, w) 453 w.WriteHeader(http.StatusBadRequest) 454 getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params) 455 return 456 } 457 u.PasswordChangeRequired = false 458 u.Hash = newHash 459 if err = models.PutUser(&u); err != nil { 460 Flash(w, r, "danger", err.Error()) 461 params.Flashes = session.Flashes() 462 session.Save(r, w) 463 w.WriteHeader(http.StatusInternalServerError) 464 getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params) 465 return 466 } 467 // TODO: We probably want to flash a message here that the password was 468 // changed successfully. The problem is that when the user resets their 469 // password on first use, they will see two flashes on the dashboard- 470 // one for their password reset, and one for the "no campaigns created". 471 // 472 // The solution to this is to revamp the empty page to be more useful, 473 // like a wizard or something. 474 as.nextOrIndex(w, r) 475 } 476 } 477 478 // TODO: Make this execute the template, too 479 func getTemplate(w http.ResponseWriter, tmpl string) *template.Template { 480 templates := template.New("template") 481 _, err := templates.ParseFiles("templates/base.html", "templates/nav.html", "templates/"+tmpl+".html", "templates/flashes.html") 482 if err != nil { 483 log.Error(err) 484 } 485 return template.Must(templates, err) 486 } 487 488 // Flash handles the rendering flash messages 489 func Flash(w http.ResponseWriter, r *http.Request, t string, m string) { 490 session := ctx.Get(r, "session").(*sessions.Session) 491 session.AddFlash(models.Flash{ 492 Type: t, 493 Message: m, 494 }) 495 }