Merged into master

pull/1894/head
Glenn Wilkinson 2020-07-13 13:36:38 +01:00
commit 74460f4d96
77 changed files with 1416 additions and 464 deletions

151
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,151 @@
name: Build Gophish Release
on:
release:
types: [created]
jobs:
build:
name: Build Binary
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
arch: ['386', amd64]
# We sometimes use different verbiage for things (e.g. "darwin"
# for the GOOS build flag and "osx" in the actual release ZIP).
# We need to specify those here.
include:
- os: windows-latest
goos: windows
bin: 'gophish.exe'
releaseos: windows
- os: ubuntu-latest
goos: linux
bin: 'gophish'
releaseos: linux
- os: macos-latest
goos: darwin
bin: 'gophish'
releaseos: osx
# Don't build windows-32bit due to missing MinGW dependencies
# Don't build osx-32bit due to eventual drop in Go support
exclude:
- os: windows-latest
arch: '386'
- os: macos-latest
arch: '386'
steps:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.14
- if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update && sudo apt-get install -y gcc-multilib
- if: matrix.arch == '386'
run: echo "::set-env name=RELEASE::gophish-${{ github.event.release.tag_name }}-${{ matrix.releaseos}}-32bit"
- if: matrix.arch == 'amd64'
run: echo "::set-env name=RELEASE::gophish-${{ github.event.release.tag_name}}-${{ matrix.releaseos}}-64bit"
- uses: actions/checkout@v2
- name: Build ${{ matrix.goos }}/${{ matrix.arch }}
run: go build -o ${{ matrix.bin }}
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.arch }}
CGO_ENABLED: 1
- name: Upload to artifacts
uses: actions/upload-artifact@v2
with:
name: ${{ env.RELEASE }}
path: ${{ matrix.bin }}
package:
name: Package Assets
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
path: bin
- name: Package Releases
run: |
mkdir releases;
for RELEASE_DIR in bin/*
do
echo "Creating release $RELEASE_DIR"
for BINARY in $RELEASE_DIR/*
do
cp $BINARY .;
zip -r releases/$(basename $RELEASE_DIR).zip \
$(basename ${BINARY}) \
static/js/dist \
static/js/src/vendor/ckeditor \
static/css/dist \
static/images \
static/font \
static/db \
db \
templates \
README.md \
VERSION \
LICENSE \
config.json;
rm $BINARY;
done
done
- name: Upload to artifacts
uses: actions/upload-artifact@v2
with:
name: releases
path: releases/*.zip
upload:
name: Upload to the Release
runs-on: ubuntu-latest
needs: package
steps:
- uses: actions/download-artifact@v2
with:
name: releases
path: releases/
# I would love to use @actions/upload-release-asset, but they don't
# support wildcards in the asset path. Ref #9, #24, and #47
- name: Upload Archives to Release
env:
UPLOAD_URL: ${{ github.event.release.upload_url }}
API_HEADER: "Accept: application/vnd.github.v3+json"
AUTH_HEADER: "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
run: |
UPLOAD_URL=$(echo -n $UPLOAD_URL | sed s/\{.*//g)
for FILE in releases/*
do
echo "Uploading ${FILE}";
curl \
-H "${API_HEADER}" \
-H "${AUTH_HEADER}" \
-H "Content-Type: $(file -b --mime-type ${FILE})" \
--data-binary "@${FILE}" \
"${UPLOAD_URL}?name=$(basename ${FILE})";
done
- name: Generate SHA256 Hashes
env:
API_HEADER: "Accept: application/vnd.github.v3+json"
AUTH_HEADER: "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
RELEASE_URL: ${{ github.event.release.url }}
run: |
HASH_TABLE="| SHA256 Hash | Filename |"
HASH_TABLE="${HASH_TABLE}\n|-----|-----|\n"
for FILE in releases/*
do
FILENAME=$(basename ${FILE})
HASH=$(sha256sum ${FILE} | cut -d ' ' -f 1)
HASH_TABLE="${HASH_TABLE}|${HASH}|${FILENAME}|\n"
done
echo "${HASH_TABLE}"
curl \
-XPATCH \
-H "${API_HEADER}" \
-H "${AUTH_HEADER}" \
-H "Content-Type: application/json" \
-d "{\"body\": \"${HASH_TABLE}\"}" \
"${RELEASE_URL}";

View File

@ -1 +1 @@
0.9.0 0.10.1

View File

@ -1,69 +1,103 @@
package auth package auth
import ( import (
"crypto/rand"
"errors" "errors"
"net/http" "fmt"
"io"
ctx "github.com/gophish/gophish/context"
"github.com/gophish/gophish/models"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// MinPasswordLength is the minimum number of characters required in a password
const MinPasswordLength = 8
// APIKeyLength is the length of Gophish API keys
const APIKeyLength = 32
// ErrInvalidPassword is thrown when a user provides an incorrect password. // ErrInvalidPassword is thrown when a user provides an incorrect password.
var ErrInvalidPassword = errors.New("Invalid Password") var ErrInvalidPassword = errors.New("Invalid Password")
// ErrPasswordMismatch is thrown when a user provides a blank password to the register // ErrPasswordMismatch is thrown when a user provides a mismatching password
// or change password functions // and confirmation password.
var ErrPasswordMismatch = errors.New("Password cannot be blank") var ErrPasswordMismatch = errors.New("Passwords do not match")
// ErrReusedPassword is thrown when a user attempts to change their password to
// the existing password
var ErrReusedPassword = errors.New("Cannot reuse existing password")
// ErrEmptyPassword is thrown when a user provides a blank password to the register // ErrEmptyPassword is thrown when a user provides a blank password to the register
// or change password functions // or change password functions
var ErrEmptyPassword = errors.New("No password provided") var ErrEmptyPassword = errors.New("No password provided")
// Login attempts to login the user given a request. // ErrPasswordTooShort is thrown when a user provides a password that is less
func Login(r *http.Request) (bool, models.User, error) { // than MinPasswordLength
username, password := r.FormValue("username"), r.FormValue("password") var ErrPasswordTooShort = fmt.Errorf("Password must be at least %d characters", MinPasswordLength)
u, err := models.GetUserByUsername(username)
if err != nil { // GenerateSecureKey returns the hex representation of key generated from n
return false, models.User{}, err // random bytes
} func GenerateSecureKey(n int) string {
//If we've made it here, we should have a valid user stored in u k := make([]byte, n)
//Let's check the password io.ReadFull(rand.Reader, k)
err = bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(password)) return fmt.Sprintf("%x", k)
if err != nil {
return false, models.User{}, ErrInvalidPassword
}
return true, u, nil
} }
// ChangePassword verifies the current password provided in the request and, // GeneratePasswordHash returns the bcrypt hash for the provided password using
// if it's valid, changes the password for the authenticated user. // the default bcrypt cost.
func ChangePassword(r *http.Request) error { func GeneratePasswordHash(password string) (string, error) {
u := ctx.Get(r, "user").(models.User) h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
currentPw := r.FormValue("current_password")
newPassword := r.FormValue("new_password")
confirmPassword := r.FormValue("confirm_new_password")
// Check the current password
err := bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(currentPw))
if err != nil { if err != nil {
return ErrInvalidPassword return "", err
} }
// Check that the new password isn't blank return string(h), nil
if newPassword == "" { }
// CheckPasswordPolicy ensures the provided password is valid according to our
// password policy.
//
// The current password policy is simply a minimum of 8 characters, though this
// may change in the future (see #1538).
func CheckPasswordPolicy(password string) error {
switch {
// Admittedly, empty passwords are a subset of too short passwords, but it
// helps to provide a more specific error message
case password == "":
return ErrEmptyPassword return ErrEmptyPassword
} case len(password) < MinPasswordLength:
// Check that new passwords match return ErrPasswordTooShort
if newPassword != confirmPassword {
return ErrPasswordMismatch
}
// Generate the new hash
h, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Hash = string(h)
if err = models.PutUser(&u); err != nil {
return err
} }
return nil return nil
} }
// ValidatePassword validates that the provided password matches the provided
// bcrypt hash.
func ValidatePassword(password string, hash string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
// ValidatePasswordChange validates that the new password matches the
// configured password policy, that the new password and confirmation
// password match.
//
// Note that this assumes the current password has been confirmed by the
// caller.
//
// If all of the provided data is valid, then the hash of the new password is
// returned.
func ValidatePasswordChange(currentHash, newPassword, confirmPassword string) (string, error) {
// Ensure the new password passes our password policy
if err := CheckPasswordPolicy(newPassword); err != nil {
return "", err
}
// Check that new passwords match
if newPassword != confirmPassword {
return "", ErrPasswordMismatch
}
// Make sure that the new password isn't the same as the old one
err := ValidatePassword(newPassword, currentHash)
if err == nil {
return "", ErrReusedPassword
}
// Generate the new hash
return GeneratePasswordHash(newPassword)
}

41
auth/auth_test.go Normal file
View File

@ -0,0 +1,41 @@
package auth
import (
"testing"
)
func TestPasswordPolicy(t *testing.T) {
candidate := "short"
got := CheckPasswordPolicy(candidate)
if got != ErrPasswordTooShort {
t.Fatalf("unexpected error received. expected %v got %v", ErrPasswordTooShort, got)
}
candidate = "valid password"
got = CheckPasswordPolicy(candidate)
if got != nil {
t.Fatalf("unexpected error received. expected %v got %v", nil, got)
}
}
func TestValidatePasswordChange(t *testing.T) {
newPassword := "valid password"
confirmPassword := "invalid"
currentPassword := "current password"
currentHash, err := GeneratePasswordHash(currentPassword)
if err != nil {
t.Fatalf("unexpected error generating password hash: %v", err)
}
_, got := ValidatePasswordChange(currentHash, newPassword, confirmPassword)
if got != ErrPasswordMismatch {
t.Fatalf("unexpected error received. expected %v got %v", ErrPasswordMismatch, got)
}
newPassword = currentPassword
confirmPassword = newPassword
_, got = ValidatePasswordChange(currentHash, newPassword, confirmPassword)
if got != ErrReusedPassword {
t.Fatalf("unexpected error received. expected %v got %v", ErrReusedPassword, got)
}
}

View File

@ -2,8 +2,9 @@ package config
import ( import (
"encoding/json" "encoding/json"
log "github.com/gophish/gophish/logger"
"io/ioutil" "io/ioutil"
log "github.com/gophish/gophish/logger"
) )
// AdminServer represents the Admin server configuration details // AdminServer represents the Admin server configuration details
@ -12,6 +13,7 @@ type AdminServer struct {
UseTLS bool `json:"use_tls"` UseTLS bool `json:"use_tls"`
CertPath string `json:"cert_path"` CertPath string `json:"cert_path"`
KeyPath string `json:"key_path"` KeyPath string `json:"key_path"`
CSRFKey string `json:"csrf_key"`
} }
// PhishServer represents the Phish server configuration details // PhishServer represents the Phish server configuration details

View File

@ -62,12 +62,13 @@ func TestLoadConfig(t *testing.T) {
} }
expectedConfig.MigrationsPath = expectedConfig.MigrationsPath + expectedConfig.DBName expectedConfig.MigrationsPath = expectedConfig.MigrationsPath + expectedConfig.DBName
expectedConfig.TestFlag = false expectedConfig.TestFlag = false
expectedConfig.AdminConf.CSRFKey = ""
if !reflect.DeepEqual(expectedConfig, conf) { if !reflect.DeepEqual(expectedConfig, conf) {
t.Fatalf("invalid config received. expected %#v got %#v", expectedConfig, conf) t.Fatalf("invalid config received. expected %#v got %#v", expectedConfig, conf)
} }
// Load an invalid config // Load an invalid config
conf, err = LoadConfig("bogusfile") _, err = LoadConfig("bogusfile")
if err == nil { if err == nil {
t.Fatalf("expected error when loading invalid config, but got %v", err) t.Fatalf("expected error when loading invalid config, but got %v", err)
} }

View File

@ -23,6 +23,4 @@ func Set(r *http.Request, key, val interface{}) *http.Request {
} }
// Clear is a null operation, since this is handled automatically in Go > 1.7 // Clear is a null operation, since this is handled automatically in Go > 1.7
func Clear(r *http.Request) { func Clear(r *http.Request) {}
return
}

View File

@ -42,18 +42,6 @@ func setupTest(t *testing.T) *testContext {
return ctx return ctx
} }
func tearDown(t *testing.T, ctx *testContext) {
// Cleanup all users except the original admin
// users, _ := models.GetUsers()
// for _, user := range users {
// if user.Id == 1 {
// continue
// }
// err := models.DeleteUser(user.Id)
// s.Nil(err)
// }
}
func createTestData(t *testing.T) { func createTestData(t *testing.T) {
// Add a group // Add a group
group := models.Group{Name: "Test Group"} group := models.Group{Name: "Test Group"}

View File

@ -87,6 +87,11 @@ func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
// Change this to get from URL and uid (don't bother with id in r.Body) // Change this to get from URL and uid (don't bother with id in r.Body)
g = models.Group{} g = models.Group{}
err = json.NewDecoder(r.Body).Decode(&g) err = json.NewDecoder(r.Body).Decode(&g)
if err != nil {
log.Errorf("error decoding group: %v", err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
if g.Id != id { if g.Id != id {
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError) JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
return return

View File

@ -46,7 +46,6 @@ func (as *Server) ImportGroup(w http.ResponseWriter, r *http.Request) {
return return
} }
JSONResponse(w, ts, http.StatusOK) JSONResponse(w, ts, http.StatusOK)
return
} }
// ImportEmail allows for the importing of email. // ImportEmail allows for the importing of email.
@ -94,7 +93,6 @@ func (as *Server) ImportEmail(w http.ResponseWriter, r *http.Request) {
HTML: string(e.HTML), HTML: string(e.HTML),
} }
JSONResponse(w, er, http.StatusOK) JSONResponse(w, er, http.StatusOK)
return
} }
// ImportSite allows for the importing of HTML from a website // ImportSite allows for the importing of HTML from a website
@ -153,5 +151,4 @@ func (as *Server) ImportSite(w http.ResponseWriter, r *http.Request) {
} }
cs := cloneResponse{HTML: h} cs := cloneResponse{HTML: h}
JSONResponse(w, cs, http.StatusOK) JSONResponse(w, cs, http.StatusOK)
return
} }

View File

@ -3,9 +3,9 @@ package api
import ( import (
"net/http" "net/http"
"github.com/gophish/gophish/auth"
ctx "github.com/gophish/gophish/context" ctx "github.com/gophish/gophish/context"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
) )
// Reset (/api/reset) resets the currently authenticated user's API key // Reset (/api/reset) resets the currently authenticated user's API key
@ -13,7 +13,7 @@ func (as *Server) Reset(w http.ResponseWriter, r *http.Request) {
switch { switch {
case r.Method == "POST": case r.Method == "POST":
u := ctx.Get(r, "user").(models.User) u := ctx.Get(r, "user").(models.User)
u.ApiKey = util.GenerateSecureKey() u.ApiKey = auth.GenerateSecureKey(auth.APIKeyLength)
err := models.PutUser(&u) err := models.PutUser(&u)
if err != nil { if err != nil {
http.Error(w, "Error setting API Key", http.StatusInternalServerError) http.Error(w, "Error setting API Key", http.StatusInternalServerError)

View File

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
mid "github.com/gophish/gophish/middleware" mid "github.com/gophish/gophish/middleware"
"github.com/gophish/gophish/middleware/ratelimit"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/gophish/gophish/worker" "github.com/gophish/gophish/worker"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -19,14 +20,17 @@ type ServerOption func(*Server)
type Server struct { type Server struct {
handler http.Handler handler http.Handler
worker worker.Worker worker worker.Worker
limiter *ratelimit.PostLimiter
} }
// NewServer returns a new instance of the API handler with the provided // NewServer returns a new instance of the API handler with the provided
// options applied. // options applied.
func NewServer(options ...ServerOption) *Server { func NewServer(options ...ServerOption) *Server {
defaultWorker, _ := worker.New() defaultWorker, _ := worker.New()
defaultLimiter := ratelimit.NewPostLimiter()
as := &Server{ as := &Server{
worker: defaultWorker, worker: defaultWorker,
limiter: defaultLimiter,
} }
for _, opt := range options { for _, opt := range options {
opt(as) opt(as)
@ -42,6 +46,12 @@ func WithWorker(w worker.Worker) ServerOption {
} }
} }
func WithLimiter(limiter *ratelimit.PostLimiter) ServerOption {
return func(as *Server) {
as.limiter = limiter
}
}
func (as *Server) registerRoutes() { func (as *Server) registerRoutes() {
root := mux.NewRouter() root := mux.NewRouter()
root = root.StrictSlash(true) root = root.StrictSlash(true)

View File

@ -6,18 +6,14 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/gophish/gophish/auth"
ctx "github.com/gophish/gophish/context" ctx "github.com/gophish/gophish/context"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
) )
// ErrEmptyPassword is thrown when a user provides a blank password to the register
// or change password functions
var ErrEmptyPassword = errors.New("No password provided")
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken. // ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
var ErrUsernameTaken = errors.New("Username already taken") var ErrUsernameTaken = errors.New("Username already taken")
@ -33,9 +29,10 @@ var ErrInsufficientPermission = errors.New("Permission denied")
// userRequest is the payload which represents the creation of a new user. // userRequest is the payload which represents the creation of a new user.
type userRequest struct { type userRequest struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Role string `json:"role"` Role string `json:"role"`
PasswordChangeRequired bool `json:"password_change_required"`
} }
func (ur *userRequest) Validate(existingUser *models.User) error { func (ur *userRequest) Validate(existingUser *models.User) error {
@ -89,11 +86,12 @@ func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return return
} }
if ur.Password == "" { err = auth.CheckPasswordPolicy(ur.Password)
JSONResponse(w, models.Response{Success: false, Message: ErrEmptyPassword.Error()}, http.StatusBadRequest) if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return return
} }
hash, err := util.NewHash(ur.Password) hash, err := auth.GeneratePasswordHash(ur.Password)
if err != nil { if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return return
@ -106,7 +104,7 @@ func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
user := models.User{ user := models.User{
Username: ur.Username, Username: ur.Username,
Hash: hash, Hash: hash,
ApiKey: util.GenerateSecureKey(), ApiKey: auth.GenerateSecureKey(auth.APIKeyLength),
Role: role, Role: role,
RoleID: role.ID, RoleID: role.ID,
} }
@ -195,13 +193,20 @@ func (as *Server) User(w http.ResponseWriter, r *http.Request) {
// We don't force the password to be provided, since it may be an admin // We don't force the password to be provided, since it may be an admin
// managing the user's account, and making a simple change like // managing the user's account, and making a simple change like
// updating the username or role. However, if it _is_ provided, we'll // updating the username or role. However, if it _is_ provided, we'll
// update the stored hash. // update the stored hash after validating the new password meets our
// password policy.
// //
// Note that we don't force the current password to be provided. The // Note that we don't force the current password to be provided. The
// assumption here is that the API key is a proper bearer token proving // assumption here is that the API key is a proper bearer token proving
// authenticated access to the account. // authenticated access to the account.
existingUser.PasswordChangeRequired = ur.PasswordChangeRequired
if ur.Password != "" { if ur.Password != "" {
hash, err := util.NewHash(ur.Password) err = auth.CheckPasswordPolicy(ur.Password)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
hash, err := auth.GeneratePasswordHash(ur.Password)
if err != nil { if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError) JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return return

View File

@ -66,7 +66,7 @@ func TestCreateUser(t *testing.T) {
testCtx := setupTest(t) testCtx := setupTest(t)
payload := &userRequest{ payload := &userRequest{
Username: "foo", Username: "foo",
Password: "bar", Password: "validpassword",
Role: models.RoleUser, Role: models.RoleUser,
} }
body, err := json.Marshal(payload) body, err := json.Marshal(payload)

View File

@ -118,5 +118,4 @@ func (as *Server) SendTestEmail(w http.ResponseWriter, r *http.Request) {
return return
} }
JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK) JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK)
return
} }

View File

@ -62,15 +62,20 @@ func (as *Server) Webhook(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: true, Message: "Webhook deleted Successfully!"}, http.StatusOK) JSONResponse(w, models.Response{Success: true, Message: "Webhook deleted Successfully!"}, http.StatusOK)
case r.Method == "PUT": case r.Method == "PUT":
wh2 := models.Webhook{} wh = models.Webhook{}
err = json.NewDecoder(r.Body).Decode(&wh2) err = json.NewDecoder(r.Body).Decode(&wh)
wh2.Id = id if err != nil {
err = models.PutWebhook(&wh2) log.Errorf("error decoding webhook: %v", err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
wh.Id = id
err = models.PutWebhook(&wh)
if err != nil { if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return return
} }
JSONResponse(w, wh2, http.StatusOK) JSONResponse(w, wh, http.StatusOK)
} }
} }

View File

@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/gophish/gophish/auth"
"github.com/gophish/gophish/config" "github.com/gophish/gophish/config"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
) )
@ -41,6 +42,10 @@ func setupTest(t *testing.T) *testContext {
ctx.adminServer.Start() ctx.adminServer.Start()
// Get the API key to use for these tests // Get the API key to use for these tests
u, err := models.GetUser(1) u, err := models.GetUser(1)
// Reset the temporary password for the admin user to a value we control
hash, err := auth.GeneratePasswordHash("gophish")
u.Hash = hash
models.PutUser(&u)
if err != nil { if err != nil {
t.Fatalf("error getting first user from database: %v", err) t.Fatalf("error getting first user from database: %v", err)
} }

View File

@ -3,7 +3,6 @@ package controllers
import ( import (
"compress/gzip" "compress/gzip"
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -86,9 +85,7 @@ func WithContactAddress(addr string) PhishingServerOption {
func (ps *PhishingServer) Start() { func (ps *PhishingServer) Start() {
if ps.config.UseTLS { if ps.config.UseTLS {
// Only support TLS 1.2 and above - ref #1691, #1689 // Only support TLS 1.2 and above - ref #1691, #1689
ps.server.TLSConfig = &tls.Config{ ps.server.TLSConfig = defaultTLSConfig
MinVersion: tls.VersionTLS12,
}
err := util.CheckAndCreateSSL(ps.config.CertPath, ps.config.KeyPath) err := util.CheckAndCreateSSL(ps.config.CertPath, ps.config.KeyPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -18,6 +18,7 @@ import (
"github.com/gophish/gophish/controllers/api" "github.com/gophish/gophish/controllers/api"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
mid "github.com/gophish/gophish/middleware" mid "github.com/gophish/gophish/middleware"
"github.com/gophish/gophish/middleware/ratelimit"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/gophish/gophish/util" "github.com/gophish/gophish/util"
"github.com/gophish/gophish/worker" "github.com/gophish/gophish/worker"
@ -35,9 +36,31 @@ type AdminServerOption func(*AdminServer)
// AdminServer is an HTTP server that implements the administrative Gophish // AdminServer is an HTTP server that implements the administrative Gophish
// handlers, including the dashboard and REST API. // handlers, including the dashboard and REST API.
type AdminServer struct { type AdminServer struct {
server *http.Server server *http.Server
worker worker.Worker worker worker.Worker
config config.AdminServer config config.AdminServer
limiter *ratelimit.PostLimiter
}
var defaultTLSConfig = &tls.Config{
PreferServerCipherSuites: true,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
// Kept for backwards compatibility with some clients
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
},
} }
// WithWorker is an option that sets the background worker. // WithWorker is an option that sets the background worker.
@ -55,10 +78,12 @@ func NewAdminServer(config config.AdminServer, options ...AdminServerOption) *Ad
ReadTimeout: 10 * time.Second, ReadTimeout: 10 * time.Second,
Addr: config.ListenURL, Addr: config.ListenURL,
} }
defaultLimiter := ratelimit.NewPostLimiter()
as := &AdminServer{ as := &AdminServer{
worker: defaultWorker, worker: defaultWorker,
server: defaultServer, server: defaultServer,
config: config, limiter: defaultLimiter,
config: config,
} }
for _, opt := range options { for _, opt := range options {
opt(as) opt(as)
@ -74,9 +99,7 @@ func (as *AdminServer) Start() {
} }
if as.config.UseTLS { if as.config.UseTLS {
// Only support TLS 1.2 and above - ref #1691, #1689 // Only support TLS 1.2 and above - ref #1691, #1689
as.server.TLSConfig = &tls.Config{ as.server.TLSConfig = defaultTLSConfig
MinVersion: tls.VersionTLS12,
}
err := util.CheckAndCreateSSL(as.config.CertPath, as.config.KeyPath) err := util.CheckAndCreateSSL(as.config.CertPath, as.config.KeyPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -102,8 +125,9 @@ func (as *AdminServer) registerRoutes() {
router := mux.NewRouter() router := mux.NewRouter()
// Base Front-end routes // Base Front-end routes
router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin)) router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin))
router.HandleFunc("/login", as.Login) router.HandleFunc("/login", mid.Use(as.Login, as.limiter.Limit))
router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin)) router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin))
router.HandleFunc("/reset_password", mid.Use(as.ResetPassword, mid.RequireLogin))
router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin)) router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin))
router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin)) router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin))
router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin)) router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin))
@ -113,18 +137,25 @@ func (as *AdminServer) registerRoutes() {
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin)) router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin)) router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
router.HandleFunc("/webhooks", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin)) router.HandleFunc("/webhooks", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
router.HandleFunc("/impersonate", mid.Use(as.Impersonate, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
router.HandleFunc("/reported", mid.Use(as.Reported, mid.RequireLogin)) router.HandleFunc("/reported", mid.Use(as.Reported, mid.RequireLogin))
router.HandleFunc("/reported/attachment/{id:[0-9]+}", mid.Use(as.ReportedEmailAttachment, mid.RequireLogin)) router.HandleFunc("/reported/attachment/{id:[0-9]+}", mid.Use(as.ReportedEmailAttachment, mid.RequireLogin))
// Create the API routes // Create the API routes
api := api.NewServer(api.WithWorker(as.worker)) api := api.NewServer(
api.WithWorker(as.worker),
api.WithLimiter(as.limiter),
)
router.PathPrefix("/api/").Handler(api) router.PathPrefix("/api/").Handler(api)
// Setup static file serving // Setup static file serving
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/"))) router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
// Setup CSRF Protection // Setup CSRF Protection
csrfHandler := csrf.Protect([]byte(util.GenerateSecureKey()), csrfKey := []byte(as.config.CSRFKey)
if len(csrfKey) == 0 {
csrfKey = []byte(auth.GenerateSecureKey(auth.APIKeyLength))
}
csrfHandler := csrf.Protect(csrfKey,
csrf.FieldName("csrf_token"), csrf.FieldName("csrf_token"),
csrf.Secure(as.config.UseTLS)) csrf.Secure(as.config.UseTLS))
adminHandler := csrfHandler(router) adminHandler := csrfHandler(router)
@ -152,12 +183,14 @@ type templateParams struct {
// the CSRF token. // the CSRF token.
func newTemplateParams(r *http.Request) templateParams { func newTemplateParams(r *http.Request) templateParams {
user := ctx.Get(r, "user").(models.User) user := ctx.Get(r, "user").(models.User)
session := ctx.Get(r, "session").(*sessions.Session)
modifySystem, _ := user.HasPermission(models.PermissionModifySystem) modifySystem, _ := user.HasPermission(models.PermissionModifySystem)
return templateParams{ return templateParams{
Token: csrf.Token(r), Token: csrf.Token(r),
User: user, User: user,
ModifySystem: modifySystem, ModifySystem: modifySystem,
Version: config.Version, Version: config.Version,
Flashes: session.Flashes(),
} }
} }
@ -216,22 +249,37 @@ func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) {
case r.Method == "GET": case r.Method == "GET":
params := newTemplateParams(r) params := newTemplateParams(r)
params.Title = "Settings" params.Title = "Settings"
session := ctx.Get(r, "session").(*sessions.Session)
session.Save(r, w)
getTemplate(w, "settings").ExecuteTemplate(w, "base", params) getTemplate(w, "settings").ExecuteTemplate(w, "base", params)
case r.Method == "POST": case r.Method == "POST":
err := auth.ChangePassword(r) u := ctx.Get(r, "user").(models.User)
currentPw := r.FormValue("current_password")
newPassword := r.FormValue("new_password")
confirmPassword := r.FormValue("confirm_new_password")
// Check the current password
err := auth.ValidatePassword(currentPw, u.Hash)
msg := models.Response{Success: true, Message: "Settings Updated Successfully"} msg := models.Response{Success: true, Message: "Settings Updated Successfully"}
if err == auth.ErrInvalidPassword {
msg.Message = "Invalid Password"
msg.Success = false
api.JSONResponse(w, msg, http.StatusBadRequest)
return
}
if err != nil { if err != nil {
msg.Message = err.Error() msg.Message = err.Error()
msg.Success = false msg.Success = false
api.JSONResponse(w, msg, http.StatusBadRequest) api.JSONResponse(w, msg, http.StatusBadRequest)
return return
} }
newHash, err := auth.ValidatePasswordChange(u.Hash, newPassword, confirmPassword)
if err != nil {
msg.Message = err.Error()
msg.Success = false
api.JSONResponse(w, msg, http.StatusBadRequest)
return
}
u.Hash = string(newHash)
if err = models.PutUser(&u); err != nil {
msg.Message = err.Error()
msg.Success = false
api.JSONResponse(w, msg, http.StatusInternalServerError)
return
}
api.JSONResponse(w, msg, http.StatusOK) api.JSONResponse(w, msg, http.StatusOK)
} }
} }
@ -244,6 +292,39 @@ func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
getTemplate(w, "users").ExecuteTemplate(w, "base", params) getTemplate(w, "users").ExecuteTemplate(w, "base", params)
} }
func (as *AdminServer) nextOrIndex(w http.ResponseWriter, r *http.Request) {
next := "/"
url, err := url.Parse(r.FormValue("next"))
if err == nil {
path := url.Path
if path != "" {
next = path
}
}
http.Redirect(w, r, next, 302)
}
func (as *AdminServer) handleInvalidLogin(w http.ResponseWriter, r *http.Request) {
session := ctx.Get(r, "session").(*sessions.Session)
Flash(w, r, "danger", "Invalid Username/Password")
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
}{Title: "Login", Token: csrf.Token(r)}
params.Flashes = session.Flashes()
session.Save(r, w)
templates := template.New("template")
_, err := templates.ParseFiles("templates/login.html", "templates/flashes.html")
if err != nil {
log.Error(err)
}
// w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusUnauthorized)
template.Must(templates, err).ExecuteTemplate(w, "base", params)
}
// Webhooks is an admin-only handler that handles webhooks // Webhooks is an admin-only handler that handles webhooks
func (as *AdminServer) Webhooks(w http.ResponseWriter, r *http.Request) { func (as *AdminServer) Webhooks(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r) params := newTemplateParams(r)
@ -281,6 +362,24 @@ func (as *AdminServer) ReportedEmailAttachment(w http.ResponseWriter, r *http.Re
} }
} }
// Impersonate allows an admin to login to a user account without needing the password
func (as *AdminServer) Impersonate(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
username := r.FormValue("username")
u, err := models.GetUserByUsername(username)
if err != nil {
log.Error(err)
http.Error(w, err.Error(), http.StatusNotFound)
return
}
session := ctx.Get(r, "session").(*sessions.Session)
session.Values["id"] = u.Id
session.Save(r, w)
}
http.Redirect(w, r, "/", http.StatusFound)
}
// Login handles the authentication flow for a user. If credentials are valid, // Login handles the authentication flow for a user. If credentials are valid,
// a session is created // a session is created
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) { func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
@ -302,37 +401,25 @@ func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
} }
template.Must(templates, err).ExecuteTemplate(w, "base", params) template.Must(templates, err).ExecuteTemplate(w, "base", params)
case r.Method == "POST": case r.Method == "POST":
//Attempt to login // Find the user with the provided username
succ, u, err := auth.Login(r) username, password := r.FormValue("username"), r.FormValue("password")
u, err := models.GetUserByUsername(username)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
as.handleInvalidLogin(w, r)
return
} }
//If we've logged in, save the session and redirect to the dashboard // Validate the user's password
if succ { err = auth.ValidatePassword(password, u.Hash)
session.Values["id"] = u.Id if err != nil {
session.Save(r, w) log.Error(err)
next := "/" as.handleInvalidLogin(w, r)
url, err := url.Parse(r.FormValue("next")) return
if err == nil {
path := url.Path
if path != "" {
next = path
}
}
http.Redirect(w, r, next, 302)
} else {
Flash(w, r, "danger", "Invalid Username/Password")
params.Flashes = session.Flashes()
session.Save(r, w)
templates := template.New("template")
_, err := templates.ParseFiles("templates/login.html", "templates/flashes.html")
if err != nil {
log.Error(err)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusUnauthorized)
template.Must(templates, err).ExecuteTemplate(w, "base", params)
} }
// If we've logged in, save the session and redirect to the dashboard
session.Values["id"] = u.Id
session.Save(r, w)
as.nextOrIndex(w, r)
} }
} }
@ -342,9 +429,72 @@ func (as *AdminServer) Logout(w http.ResponseWriter, r *http.Request) {
delete(session.Values, "id") delete(session.Values, "id")
Flash(w, r, "success", "You have successfully logged out") Flash(w, r, "success", "You have successfully logged out")
session.Save(r, w) session.Save(r, w)
http.Redirect(w, r, "/login", 302) http.Redirect(w, r, "/login", http.StatusFound)
} }
// ResetPassword handles the password reset flow when a password change is
// required either by the Gophish system or an administrator.
//
// This handler is meant to be used when a user is required to reset their
// password, not just when they want to.
//
// This is an important distinction since in this handler we don't require
// the user to re-enter their current password, as opposed to the flow
// through the settings handler.
//
// To that end, if the user doesn't require a password change, we will
// redirect them to the settings page.
func (as *AdminServer) ResetPassword(w http.ResponseWriter, r *http.Request) {
u := ctx.Get(r, "user").(models.User)
session := ctx.Get(r, "session").(*sessions.Session)
if !u.PasswordChangeRequired {
Flash(w, r, "info", "Please reset your password through the settings page")
session.Save(r, w)
http.Redirect(w, r, "/settings", http.StatusTemporaryRedirect)
return
}
params := newTemplateParams(r)
params.Title = "Reset Password"
switch {
case r.Method == http.MethodGet:
params.Flashes = session.Flashes()
session.Save(r, w)
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
return
case r.Method == http.MethodPost:
newPassword := r.FormValue("password")
confirmPassword := r.FormValue("confirm_password")
newHash, err := auth.ValidatePasswordChange(u.Hash, newPassword, confirmPassword)
if err != nil {
Flash(w, r, "danger", err.Error())
params.Flashes = session.Flashes()
session.Save(r, w)
w.WriteHeader(http.StatusBadRequest)
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
return
}
u.PasswordChangeRequired = false
u.Hash = newHash
if err = models.PutUser(&u); err != nil {
Flash(w, r, "danger", err.Error())
params.Flashes = session.Flashes()
session.Save(r, w)
w.WriteHeader(http.StatusInternalServerError)
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
return
}
// TODO: We probably want to flash a message here that the password was
// changed successfully. The problem is that when the user resets their
// password on first use, they will see two flashes on the dashboard-
// one for their password reset, and one for the "no campaigns created".
//
// The solution to this is to revamp the empty page to be more useful,
// like a wizard or something.
as.nextOrIndex(w, r)
}
}
// TODO: Make this execute the template, too
func getTemplate(w http.ResponseWriter, tmpl string) *template.Template { func getTemplate(w http.ResponseWriter, tmpl string) *template.Template {
templates := template.New("template") templates := template.New("template")
_, err := templates.ParseFiles("templates/base.html", "templates/nav.html", "templates/"+tmpl+".html", "templates/flashes.html") _, err := templates.ParseFiles("templates/base.html", "templates/nav.html", "templates/"+tmpl+".html", "templates/flashes.html")

View File

@ -0,0 +1,9 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE `users` ADD COLUMN password_change_required BOOLEAN;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View File

@ -0,0 +1,9 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE users ADD COLUMN password_change_required BOOLEAN;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

6
go.mod
View File

@ -8,6 +8,8 @@ require (
github.com/PuerkitoBio/goquery v1.5.0 github.com/PuerkitoBio/goquery v1.5.0
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/emersion/go-imap v1.0.4
github.com/emersion/go-message v0.12.0
github.com/go-sql-driver/mysql v1.5.0 github.com/go-sql-driver/mysql v1.5.0
github.com/gophish/gomail v0.0.0-20180314010319-cf7e1a5479be github.com/gophish/gomail v0.0.0-20180314010319-cf7e1a5479be
github.com/gorilla/context v1.1.1 github.com/gorilla/context v1.1.1
@ -17,15 +19,15 @@ require (
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0 github.com/gorilla/sessions v1.2.0
github.com/jinzhu/gorm v1.9.12 github.com/jinzhu/gorm v1.9.12
github.com/jordan-wright/email v0.0.0-20200121133829-a0b5c5b58bb6 github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 // indirect github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d
github.com/oschwald/maxminddb-golang v1.6.0 github.com/oschwald/maxminddb-golang v1.6.0
github.com/sirupsen/logrus v1.4.2 github.com/sirupsen/logrus v1.4.2
github.com/ziutek/mymysql v1.5.4 // indirect github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405

21
go.sum
View File

@ -15,6 +15,15 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/emersion/go-imap v1.0.4 h1:uiCAIHM6Z5Jwkma1zdNDWWXxSCqb+/xHBkHflD7XBro=
github.com/emersion/go-imap v1.0.4/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU=
github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-message v0.12.0 h1:mZnv35eZ6lB6EftTQBgYXspOH0FQdhpFhSUhA9i6/Zg=
github.com/emersion/go-message v0.12.0/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe h1:40SWqY0zE3qCi6ZrtTf5OUdNm5lDnGnjRSq9GgmeTrg=
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@ -43,8 +52,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jordan-wright/email v0.0.0-20200121133829-a0b5c5b58bb6 h1:gI29NnCaNU8N7rZT2svjtas5SrbL0XsutOPtInVvGIA= github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48=
github.com/jordan-wright/email v0.0.0-20200121133829-a0b5c5b58bb6/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f h1:bYVTBvVHcAYDkH8hyVMRUW7J2mYQNNSmQPXGadYd1nY= github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f h1:bYVTBvVHcAYDkH8hyVMRUW7J2mYQNNSmQPXGadYd1nY=
github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f/go.mod h1:eRt05O5haIXGKGodWjpQ2xdgBHTE7hg/pzsukNi9IRA= github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f/go.mod h1:eRt05O5haIXGKGodWjpQ2xdgBHTE7hg/pzsukNi9IRA=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
@ -53,11 +62,10 @@ github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 h1:mkl3tvPHIuP
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d h1:+DgqA2tuWi/8VU+gVgBAa7+WZrnFbPKhQWbKBB54cVs=
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d/go.mod h1:xacC5qXZnL/ooiitVoe3BtI1OotFTqi5zICBs9J5Fyk=
github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls= github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls=
github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
@ -90,6 +98,11 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=

View File

@ -26,6 +26,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
*/ */
import ( import (
"fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"os/signal" "os/signal"
@ -40,9 +41,17 @@ import (
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
) )
const (
modeAll string = "all"
modeAdmin string = "admin"
modePhish string = "phish"
)
var ( var (
configPath = kingpin.Flag("config", "Location of config.json.").Default("./config.json").String() configPath = kingpin.Flag("config", "Location of config.json.").Default("./config.json").String()
disableMailer = kingpin.Flag("disable-mailer", "Disable the mailer (for use with multi-system deployments)").Bool() disableMailer = kingpin.Flag("disable-mailer", "Disable the mailer (for use with multi-system deployments)").Bool()
mode = kingpin.Flag("mode", fmt.Sprintf("Run the binary in one of the modes (%s, %s or %s)", modeAll, modeAdmin, modePhish)).
Default("all").Enum(modeAll, modeAdmin, modePhish)
) )
func main() { func main() {
@ -102,18 +111,25 @@ func main() {
phishServer := controllers.NewPhishingServer(phishConfig) phishServer := controllers.NewPhishingServer(phishConfig)
imapMonitor := imap.NewMonitor() imapMonitor := imap.NewMonitor()
if *mode == "admin" || *mode == "all" {
go adminServer.Start() go adminServer.Start()
go phishServer.Start() go imapMonitor.Start()
go imapMonitor.Start() }
if *mode == "phish" || *mode == "all" {
go phishServer.Start()
}
// Handle graceful shutdown // Handle graceful shutdown
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt)
<-c <-c
log.Info("CTRL+C Received... Gracefully shutting down servers") log.Info("CTRL+C Received... Gracefully shutting down servers")
adminServer.Shutdown() if *mode == modeAdmin || *mode == modeAll {
phishServer.Shutdown() adminServer.Shutdown()
imapMonitor.Shutdown() imapMonitor.Shutdown()
}
if *mode == modePhish || *mode == modeAll {
phishServer.Shutdown()
}
} }

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io"
"regexp" "regexp"
"strconv" "strconv"
"time" "time"
@ -12,7 +11,6 @@ import (
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/emersion/go-imap/client" "github.com/emersion/go-imap/client"
"github.com/emersion/go-message/charset" "github.com/emersion/go-message/charset"
"github.com/emersion/go-message/mail"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
@ -48,7 +46,6 @@ type Mailbox struct {
// Validate validates supplied IMAP model by connecting to the server // Validate validates supplied IMAP model by connecting to the server
func Validate(s *models.IMAP) error { func Validate(s *models.IMAP) error {
err := s.Validate() err := s.Validate()
if err != nil { if err != nil {
log.Error(err) log.Error(err)
@ -117,7 +114,6 @@ func (mbox *Mailbox) DeleteEmails(seqs []uint32) error {
// GetUnread will find all unread emails in the folder and return them as a list. // GetUnread will find all unread emails in the folder and return them as a list.
func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) { func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
imap.CharsetReader = charset.Reader imap.CharsetReader = charset.Reader
var emails []Email var emails []Email
@ -130,80 +126,54 @@ func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
// Search for unread emails // Search for unread emails
criteria := imap.NewSearchCriteria() criteria := imap.NewSearchCriteria()
criteria.WithoutFlags = []string{"\\Seen"} criteria.WithoutFlags = []string{imap.SeenFlag}
seqs, err := imapClient.Search(criteria) seqs, err := imapClient.Search(criteria)
if err != nil { if err != nil {
return emails, err return emails, err
} }
if len(seqs) > 0 { if len(seqs) == 0 {
seqset := new(imap.SeqSet) return emails, nil
seqset.AddNum(seqs...) }
section := &imap.BodySectionName{}
items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchInternalDate, section.FetchItem()}
messages := make(chan *imap.Message)
go func() { seqset := new(imap.SeqSet)
if err := imapClient.Fetch(seqset, items, messages); err != nil { seqset.AddNum(seqs...)
log.Error("Error fetching emails: ", err.Error()) // TODO: How to handle this, need to propogate error out section := &imap.BodySectionName{}
} items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchInternalDate, section.FetchItem()}
}() messages := make(chan *imap.Message)
// Step through each email go func() {
for msg := range messages { if err := imapClient.Fetch(seqset, items, messages); err != nil {
// Extract raw message body. I can't find a better way to do this with the emersion library log.Error("Error fetching emails: ", err.Error()) // TODO: How to handle this, need to propogate error out
var em *email.Email }
var buf []byte }()
for _, value := range msg.Body {
buf = make([]byte, value.Len())
value.Read(buf)
break // There should only ever be one item in this map, but I'm not 100% sure
}
//Remove CR characters, see https://github.com/jordan-wright/email/issues/106 // Step through each email
tmp := string(buf) for msg := range messages {
re := regexp.MustCompile(`\r`) // Extract raw message body. I can't find a better way to do this with the emersion library
tmp = re.ReplaceAllString(tmp, "") var em *email.Email
buf = []byte(tmp) var buf []byte
for _, value := range msg.Body {
buf = make([]byte, value.Len())
value.Read(buf)
break // There should only ever be one item in this map, but I'm not 100% sure
}
rawBodyStream := bytes.NewReader(buf) //Remove CR characters, see https://github.com/jordan-wright/email/issues/106
em, err = email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library tmp := string(buf)
if err != nil { re := regexp.MustCompile(`\r`)
return emails, err tmp = re.ReplaceAllString(tmp, "")
} buf = []byte(tmp)
// Reload the reader rawBodyStream := bytes.NewReader(buf)
rawBodyStream = bytes.NewReader(buf) em, err = email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library
mr, err := mail.CreateReader(rawBodyStream) if err != nil {
if err != nil { return emails, err
return emails, err }
}
// Step over each part of the email, parsing attachments and attaching them to Jordan's email emtmp := Email{Email: em, SeqNum: msg.SeqNum} // Not sure why msg.Uid is always 0, so swapped to sequence numbers
for { emails = append(emails, emtmp)
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
return emails, err
}
h := p.Header
s, ok := h.(*mail.AttachmentHeader)
if ok {
filename, _ := s.Filename()
typ, _, _ := s.ContentType()
_, err := em.Attach(p.Body, filename, typ)
if err != nil {
return emails, err //Unable to attach file
}
}
}
emtmp := Email{Email: em, SeqNum: msg.SeqNum} // Not sure why msg.Uid is always 0, so swapped to sequence numbers
emails = append(emails, emtmp)
} // On to the next email
} }
return emails, nil return emails, nil
} }
@ -214,14 +184,11 @@ func (mbox *Mailbox) newClient() (*client.Client, error) {
var err error var err error
if mbox.TLS { if mbox.TLS {
imapClient, err = client.DialTLS(mbox.Host, new(tls.Config)) imapClient, err = client.DialTLS(mbox.Host, new(tls.Config))
if err != nil {
return imapClient, err
}
} else { } else {
imapClient, err = client.Dial(mbox.Host) imapClient, err = client.Dial(mbox.Host)
if err != nil { }
return imapClient, err if err != nil {
} return imapClient, err
} }
err = imapClient.Login(mbox.User, mbox.Pwd) err = imapClient.Login(mbox.User, mbox.Pwd)

View File

@ -11,6 +11,7 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"net/mail" "net/mail"
"path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@ -34,7 +35,6 @@ type Monitor struct {
// As each account can have its own polling frequency set we need to run one Go routine for // As each account can have its own polling frequency set we need to run one Go routine for
// each, as well as keeping an eye on newly created user accounts. // each, as well as keeping an eye on newly created user accounts.
func (im *Monitor) start(ctx context.Context) { func (im *Monitor) start(ctx context.Context) {
usermap := make(map[int64]int) // Keep track of running go routines, one per user. We assume incrementing non-repeating UIDs (for the case where users are deleted and re-added). usermap := make(map[int64]int) // Keep track of running go routines, one per user. We assume incrementing non-repeating UIDs (for the case where users are deleted and re-added).
for { for {
@ -62,7 +62,6 @@ func (im *Monitor) start(ctx context.Context) {
// monitor will continuously login to the IMAP settings associated to the supplied user id (if the user account has IMAP settings, and they're enabled.) // monitor will continuously login to the IMAP settings associated to the supplied user id (if the user account has IMAP settings, and they're enabled.)
// It also verifies the user account exists, and returns if not (for the case of a user being deleted). // It also verifies the user account exists, and returns if not (for the case of a user being deleted).
func monitor(uid int64, ctx context.Context) { func monitor(uid int64, ctx context.Context) {
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -96,7 +95,6 @@ func monitor(uid int64, ctx context.Context) {
// NewMonitor returns a new instance of imap.Monitor // NewMonitor returns a new instance of imap.Monitor
func NewMonitor() *Monitor { func NewMonitor() *Monitor {
im := &Monitor{} im := &Monitor{}
return im return im
} }
@ -120,7 +118,6 @@ func (im *Monitor) Shutdown() error {
// checkForNewEmails logs into an IMAP account and checks unread emails // checkForNewEmails logs into an IMAP account and checks unread emails
// for the rid campaign identifier. // for the rid campaign identifier.
func checkForNewEmails(im models.IMAP) { func checkForNewEmails(im models.IMAP) {
im.Host = im.Host + ":" + strconv.Itoa(int(im.Port)) // Append port im.Host = im.Host + ":" + strconv.Itoa(int(im.Port)) // Append port
mailServer := Mailbox{ mailServer := Mailbox{
Host: im.Host, Host: im.Host,
@ -140,7 +137,7 @@ func checkForNewEmails(im models.IMAP) {
if len(msgs) > 0 { if len(msgs) > 0 {
log.Debugf("%d new emails for %s", len(msgs), im.Username) log.Debugf("%d new emails for %s", len(msgs), im.Username)
var reportingFailed []uint32 // SeqNums of emails that were unable to be reported to phishing server, mark as unread var reportingFailed []uint32 // SeqNums of emails that were unable to be reported to phishing server, mark as unread
var campaignEmails []uint32 // SeqNums of campaign emails. If DeleteReportedCampaignEmail is true, we will delete these var deleteEmails []uint32 // SeqNums of campaign emails. If DeleteReportedCampaignEmail is true, we will delete these
for _, m := range msgs { for _, m := range msgs {
// Check if sender is from company's domain, if enabled. TODO: Make this an IMAP filter // Check if sender is from company's domain, if enabled. TODO: Make this an IMAP filter
if im.RestrictDomain != "" { // e.g domainResitct = widgets.com if im.RestrictDomain != "" { // e.g domainResitct = widgets.com
@ -152,118 +149,113 @@ func checkForNewEmails(im models.IMAP) {
} }
} }
rids, err := checkRIDs(m.Email) // Search email Text, HTML, and each attachment for rid parameters rids, err := matchEmail(m.Email) // Search email Text, HTML, and each attachment for rid parameters
if err != nil { if err != nil {
log.Errorf("Error searching email for rids from user '%s': %s", m.Email.From, err.Error()) log.Errorf("Error searching email for rids from user '%s': %s", m.Email.From, err.Error())
} else { continue
if len(rids) < 1 { }
log.Infof("User '%s' reported email with subject '%s'. This is not a GoPhish campaign; you should investigate it.", m.Email.From, m.Email.Subject) if len(rids) < 1 {
// In the future this should be an alert in Gophish
log.Infof("User '%s' reported email with subject '%s'. This is not a GoPhish campaign; you should investigate it.", m.Email.From, m.Email.Subject)
// Save reported email to the database // Save reported email to the database
atts := []*models.ReportedAttachment{} atts := []*models.ReportedAttachment{}
for _, a := range m.Attachments { for _, a := range m.Attachments {
na := &models.ReportedAttachment{Filename: a.Filename, Header: a.Header.Get("Content-Type"), Size: len(a.Content), Content: base64.StdEncoding.EncodeToString(a.Content)} na := &models.ReportedAttachment{Filename: a.Filename, Header: a.Header.Get("Content-Type"), Size: len(a.Content), Content: base64.StdEncoding.EncodeToString(a.Content)}
atts = append(atts, na) atts = append(atts, na)
}
e, err := mail.ParseAddress(m.Email.From)
if err != nil {
log.Error(err)
}
em := &models.ReportedEmail{
UserId: im.UserId,
ReportedByName: e.Name,
ReportedByEmail: e.Address,
ReportedHTML: string(m.HTML),
ReportedText: string(m.Text),
ReportedSubject: string(m.Subject),
IMAPUID: -1, // https://github.com/emersion/go-imap/issues/353
ReportedTime: time.Now().UTC(),
Attachments: atts,
Status: "Unknown"}
models.SaveReportedEmail(em)
} }
for rid := range rids {
log.Infof("User '%s' reported email with rid %s", m.Email.From, rid)
result, err := models.GetResult(rid)
if err != nil {
log.Error("Error reporting GoPhish email with rid ", rid, ": ", err.Error())
reportingFailed = append(reportingFailed, m.SeqNum)
} else {
err = result.HandleEmailReport(models.EventDetails{})
if err != nil {
log.Error("Error updating GoPhish email with rid ", rid, ": ", err.Error())
} else {
if im.DeleteReportedCampaignEmail == true {
campaignEmails = append(campaignEmails, m.SeqNum)
}
}
}
e, err := mail.ParseAddress(m.Email.From)
if err != nil {
log.Error(err)
}
em := &models.ReportedEmail{
UserId: im.UserId,
ReportedByName: e.Name,
ReportedByEmail: e.Address,
ReportedHTML: string(m.HTML),
ReportedText: string(m.Text),
ReportedSubject: string(m.Subject),
IMAPUID: -1, // https://github.com/emersion/go-imap/issues/353
ReportedTime: time.Now().UTC(),
Attachments: atts,
Status: "Unknown"}
models.SaveReportedEmail(em)
}
for rid := range rids {
log.Infof("User '%s' reported email with rid %s", m.Email.From, rid)
result, err := models.GetResult(rid)
if err != nil {
log.Error("Error reporting GoPhish email with rid ", rid, ": ", err.Error())
reportingFailed = append(reportingFailed, m.SeqNum)
continue
}
err = result.HandleEmailReport(models.EventDetails{})
if err != nil {
log.Error("Error updating GoPhish email with rid ", rid, ": ", err.Error())
continue
}
if im.DeleteReportedCampaignEmail == true {
deleteEmails = append(deleteEmails, m.SeqNum)
} }
} }
// Check if any emails were unable to be reported, so we can mark them as unread }
if len(reportingFailed) > 0 { // Check if any emails were unable to be reported, so we can mark them as unread
log.Debugf("Marking %d emails as unread as failed to report", len(reportingFailed)) if len(reportingFailed) > 0 {
err := mailServer.MarkAsUnread(reportingFailed) // Set emails as unread that we failed to report to GoPhish log.Debugf("Marking %d emails as unread as failed to report", len(reportingFailed))
if err != nil { err := mailServer.MarkAsUnread(reportingFailed) // Set emails as unread that we failed to report to GoPhish
log.Error("Unable to mark emails as unread: ", err.Error()) if err != nil {
} log.Error("Unable to mark emails as unread: ", err.Error())
}
// If the DeleteReportedCampaignEmail flag is set, delete reported Gophish campaign emails
if im.DeleteReportedCampaignEmail == true && len(campaignEmails) > 0 {
log.Debugf("Deleting %d campaign emails", len(campaignEmails))
err := mailServer.DeleteEmails(campaignEmails) // Delete GoPhish campaign emails.
if err != nil {
log.Error("Failed to delete emails: ", err.Error())
}
} }
} }
// If the DeleteReportedCampaignEmail flag is set, delete reported Gophish campaign emails
if len(deleteEmails) > 0 {
log.Debugf("Deleting %d campaign emails", len(deleteEmails))
err := mailServer.DeleteEmails(deleteEmails) // Delete GoPhish campaign emails.
if err != nil {
log.Error("Failed to delete emails: ", err.Error())
}
}
} else { } else {
log.Debug("No new emails for ", im.Username) log.Debug("No new emails for ", im.Username)
} }
} }
// returns a slice of gophish rid paramters found in the email HTML, Text, and attachments func checkRIDs(em *email.Email, rids map[string]bool) {
func checkRIDs(em *email.Email) (map[string]int, error) {
rids := make(map[string]int)
// Check Text and HTML // Check Text and HTML
emailContent := string(em.Text) + string(em.HTML) emailContent := string(em.Text) + string(em.HTML)
for _, r := range goPhishRegex.FindAllStringSubmatch(emailContent, -1) { for _, r := range goPhishRegex.FindAllStringSubmatch(emailContent, -1) {
newrid := r[len(r)-1] newrid := r[len(r)-1]
if _, ok := rids[newrid]; ok { if !rids[newrid] {
rids[newrid]++ rids[newrid] = true
} else {
rids[newrid] = 1
} }
} }
}
//Next check each attachment // returns a slice of gophish rid paramters found in the email HTML, Text, and attachments
func matchEmail(em *email.Email) (map[string]bool, error) {
rids := make(map[string]bool)
checkRIDs(em, rids)
// Next check each attachment
for _, a := range em.Attachments { for _, a := range em.Attachments {
if a.Header.Get("Content-Type") == "message/rfc822" || (len(a.Filename) > 3 && a.Filename[len(a.Filename)-4:] == ".eml") { ext := filepath.Ext(a.Filename)
if a.Header.Get("Content-Type") == "message/rfc822" || ext == ".eml" {
//Let's decode the email // Let's decode the email
rawBodyStream := bytes.NewReader(a.Content) rawBodyStream := bytes.NewReader(a.Content)
attachementEmail, err := email.NewEmailFromReader(rawBodyStream) attachmentEmail, err := email.NewEmailFromReader(rawBodyStream)
if err != nil { if err != nil {
return rids, err return rids, err
} }
emailContent := string(attachementEmail.Text) + string(attachementEmail.HTML) checkRIDs(attachmentEmail, rids)
for _, r := range goPhishRegex.FindAllStringSubmatch(emailContent, -1) {
newrid := r[len(r)-1]
if _, ok := rids[newrid]; ok {
rids[newrid]++
} else {
rids[newrid] = 1
}
}
} }
} }

View File

@ -13,11 +13,6 @@ import (
// being unreachable // being unreachable
var errHostUnreachable = errors.New("host unreachable") var errHostUnreachable = errors.New("host unreachable")
// errDialerUnavailable is a mock error to represent a dialer
// being unavailable (perhaps an error getting the dialer config
// or a database error)
var errDialerUnavailable = errors.New("dialer unavailable")
// mockDialer keeps track of calls to Dial // mockDialer keeps track of calls to Dial
type mockDialer struct { type mockDialer struct {
dialCount int dialCount int
@ -137,10 +132,6 @@ func (mm *mockMessage) defaultDialer() (Dialer, error) {
return newMockDialer(), nil return newMockDialer(), nil
} }
func (mm *mockMessage) errorDialer() (Dialer, error) {
return nil, errDialerUnavailable
}
func (mm *mockMessage) GetDialer() (Dialer, error) { func (mm *mockMessage) GetDialer() (Dialer, error) {
return mm.getdialer() return mm.getdialer()
} }

View File

@ -114,13 +114,21 @@ func RequireAPIKey(handler http.Handler) http.Handler {
func RequireLogin(handler http.Handler) http.HandlerFunc { func RequireLogin(handler http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if u := ctx.Get(r, "user"); u != nil { if u := ctx.Get(r, "user"); u != nil {
// If a password change is required for the user, then redirect them
// to the login page
currentUser := u.(models.User)
if currentUser.PasswordChangeRequired && r.URL.Path != "/reset_password" {
q := r.URL.Query()
q.Set("next", r.URL.Path)
http.Redirect(w, r, fmt.Sprintf("/reset_password?%s", q.Encode()), http.StatusTemporaryRedirect)
return
}
handler.ServeHTTP(w, r) handler.ServeHTTP(w, r)
return return
} }
q := r.URL.Query() q := r.URL.Query()
q.Set("next", r.URL.Path) q.Set("next", r.URL.Path)
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect) http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
return
} }
} }

View File

@ -162,3 +162,22 @@ func TestBearerToken(t *testing.T) {
t.Fatalf("incorrect status code received. expected %d got %d", expected, got) t.Fatalf("incorrect status code received. expected %d got %d", expected, got)
} }
} }
func TestPasswordResetRequired(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req = ctx.Set(req, "user", models.User{
PasswordChangeRequired: true,
})
response := httptest.NewRecorder()
RequireLogin(successHandler).ServeHTTP(response, req)
gotStatus := response.Code
expectedStatus := http.StatusTemporaryRedirect
if gotStatus != expectedStatus {
t.Fatalf("incorrect status code received. expected %d got %d", expectedStatus, gotStatus)
}
expectedLocation := "/reset_password?next=%2F"
gotLocation := response.Header().Get("Location")
if gotLocation != expectedLocation {
t.Fatalf("incorrect location header received. expected %s got %s", expectedLocation, gotLocation)
}
}

View File

@ -0,0 +1,15 @@
// Package ratelimit provides a simple token-bucket rate limiting middleware
// which only allows n POST requests every minute. This is meant to be used on
// login handlers or other sensitive transactions which should be throttled to
// prevent abuse.
//
// Tracked clients are stored in a locked map, with a goroutine that runs at a
// configurable interval to clean up stale entries.
//
// Note that there is no enforcement for GET requests. This is an effort to be
// opinionated in order to hit the most common use-cases. For more advanced
// use-cases, you may consider the `github.com/didip/tollbooth` package.
//
// The enforcement mechanism is based on the blog post here:
// https://www.alexedwards.net/blog/how-to-rate-limit-http-requests
package ratelimit

View File

@ -0,0 +1,145 @@
package ratelimit
import (
"net"
"net/http"
"sync"
"time"
log "github.com/gophish/gophish/logger"
"golang.org/x/time/rate"
)
// DefaultRequestsPerMinute is the number of requests to allow per minute.
// Any requests over this interval will return a HTTP 429 error.
const DefaultRequestsPerMinute = 5
// DefaultCleanupInterval determines how frequently the cleanup routine
// executes.
const DefaultCleanupInterval = 1 * time.Minute
// DefaultExpiry is the amount of time to track a bucket for a particular
// visitor.
const DefaultExpiry = 10 * time.Minute
type bucket struct {
limiter *rate.Limiter
lastSeen time.Time
}
// PostLimiter is a simple rate limiting middleware which only allows n POST
// requests per minute.
type PostLimiter struct {
visitors map[string]*bucket
requestLimit int
cleanupInterval time.Duration
expiry time.Duration
sync.RWMutex
}
// PostLimiterOption is a functional option that allows callers to configure
// the rate limiter.
type PostLimiterOption func(*PostLimiter)
// WithRequestsPerMinute sets the number of requests to allow per minute.
func WithRequestsPerMinute(requestLimit int) PostLimiterOption {
return func(p *PostLimiter) {
p.requestLimit = requestLimit
}
}
// WithCleanupInterval sets the interval between cleaning up stale entries in
// the rate limit client list
func WithCleanupInterval(interval time.Duration) PostLimiterOption {
return func(p *PostLimiter) {
p.cleanupInterval = interval
}
}
// WithExpiry sets the amount of time to store client entries before they are
// considered stale.
func WithExpiry(expiry time.Duration) PostLimiterOption {
return func(p *PostLimiter) {
p.expiry = expiry
}
}
// NewPostLimiter returns a new instance of a PostLimiter
func NewPostLimiter(opts ...PostLimiterOption) *PostLimiter {
limiter := &PostLimiter{
visitors: make(map[string]*bucket),
requestLimit: DefaultRequestsPerMinute,
cleanupInterval: DefaultCleanupInterval,
expiry: DefaultExpiry,
}
for _, opt := range opts {
opt(limiter)
}
go limiter.pollCleanup()
return limiter
}
func (limiter *PostLimiter) pollCleanup() {
ticker := time.NewTicker(time.Duration(limiter.cleanupInterval) * time.Second)
for range ticker.C {
limiter.Cleanup()
}
}
// Cleanup removes any buckets that were last seen past the configured expiry.
func (limiter *PostLimiter) Cleanup() {
limiter.Lock()
defer limiter.Unlock()
for ip, bucket := range limiter.visitors {
if time.Now().Sub(bucket.lastSeen) >= limiter.expiry {
delete(limiter.visitors, ip)
}
}
}
func (limiter *PostLimiter) addBucket(ip string) *bucket {
limiter.Lock()
defer limiter.Unlock()
limit := rate.NewLimiter(rate.Every(time.Minute/time.Duration(limiter.requestLimit)), limiter.requestLimit)
b := &bucket{
limiter: limit,
}
limiter.visitors[ip] = b
return b
}
func (limiter *PostLimiter) allow(ip string) bool {
// Check if we have a limiter already active for this clientIP
limiter.RLock()
bucket, exists := limiter.visitors[ip]
limiter.RUnlock()
if !exists {
bucket = limiter.addBucket(ip)
}
// Update the lastSeen for this bucket to assist with cleanup
limiter.Lock()
defer limiter.Unlock()
bucket.lastSeen = time.Now()
return bucket.limiter.Allow()
}
// Limit enforces the configured rate limit for POST requests.
//
// TODO: Change the return value to an http.Handler when we clean up the
// way Gophish routing is done.
func (limiter *PostLimiter) Limit(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
log.Errorf("Unable to determine client IP address: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if r.Method == http.MethodPost && !limiter.allow(clientIP) {
log.Error("")
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,59 @@
package ratelimit
import (
"net/http"
"net/http/httptest"
"testing"
)
var successHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
func reachLimit(t *testing.T, handler http.Handler, limit int) {
// Make `expected` requests and ensure that each return a successful
// response.
r := httptest.NewRequest(http.MethodPost, "/", nil)
r.RemoteAddr = "127.0.0.1:"
for i := 0; i < limit; i++ {
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("no 200 on req %d got %d", i, w.Code)
}
}
// Then, makes another request to ensure it returns the 429
// status.
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Code != http.StatusTooManyRequests {
t.Fatalf("no 429")
}
}
func TestRateLimitEnforcement(t *testing.T) {
expectedLimit := 3
limiter := NewPostLimiter(WithRequestsPerMinute(expectedLimit))
handler := limiter.Limit(successHandler)
reachLimit(t, handler, expectedLimit)
}
func TestRateLimitCleanup(t *testing.T) {
expectedLimit := 3
limiter := NewPostLimiter(WithRequestsPerMinute(expectedLimit))
handler := limiter.Limit(successHandler)
reachLimit(t, handler, expectedLimit)
// Set the timeout to be
bucket, exists := limiter.visitors["127.0.0.1"]
if !exists {
t.Fatalf("doesn't exist for some reason")
}
bucket.lastSeen = bucket.lastSeen.Add(-limiter.expiry)
limiter.Cleanup()
_, exists = limiter.visitors["127.0.0.1"]
if exists {
t.Fatalf("exists for some reason")
}
reachLimit(t, handler, expectedLimit)
}

View File

@ -27,7 +27,7 @@ type Campaign struct {
Status string `json:"status"` Status string `json:"status"`
Results []Result `json:"results,omitempty"` Results []Result `json:"results,omitempty"`
Groups []Group `json:"groups,omitempty"` Groups []Group `json:"groups,omitempty"`
Events []Event `json:"timeline,omitemtpy"` Events []Event `json:"timeline,omitempty"`
SMTPId int64 `json:"-"` SMTPId int64 `json:"-"`
SMTP SMTP `json:"smtp"` SMTP SMTP `json:"smtp"`
URL string `json:"url"` URL string `json:"url"`

View File

@ -103,10 +103,6 @@ func (s *EmailRequest) Generate(msg *gomail.Message) error {
if err != nil { if err != nil {
return err return err
} }
fn := f.Name
if fn == "" {
fn = f.Address
}
msg.SetAddressHeader("From", f.Address, f.Name) msg.SetAddressHeader("From", f.Address, f.Name)
ptx, err := NewPhishingTemplateContext(s, s.BaseRecipient, s.RId) ptx, err := NewPhishingTemplateContext(s, s.BaseRecipient, s.RId)

View File

@ -226,7 +226,6 @@ func PutGroup(g *Group) error {
return err return err
} }
// Fetch group's existing targets from database. // Fetch group's existing targets from database.
ts := []Target{}
ts, err := GetTargets(g.Id) ts, err := GetTargets(g.Id)
if err != nil { if err != nil {
log.WithFields(logrus.Fields{ log.WithFields(logrus.Fields{

View File

@ -53,7 +53,7 @@ var ErrIMAPPasswordNotSpecified = errors.New("No Password specified")
// ErrInvalidIMAPFreq is thrown when the frequency for polling the // ErrInvalidIMAPFreq is thrown when the frequency for polling the
// IMAP server is invalid // IMAP server is invalid
var ErrInvalidIMAPFreq = errors.New("Invalid polling frequency.") var ErrInvalidIMAPFreq = errors.New("Invalid polling frequency")
// TableName specifies the database tablename for Gorm to use // TableName specifies the database tablename for Gorm to use
func (im IMAP) TableName() string { func (im IMAP) TableName() string {

View File

@ -125,7 +125,7 @@ func (m *MailLog) Success() error {
return err return err
} }
err = db.Delete(m).Error err = db.Delete(m).Error
return nil return err
} }
// GetDialer returns a dialer based on the maillog campaign's SMTP configuration // GetDialer returns a dialer based on the maillog campaign's SMTP configuration

View File

@ -7,13 +7,15 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"path/filepath" "os"
"time" "time"
"bitbucket.org/liamstask/goose/lib/goose" "bitbucket.org/liamstask/goose/lib/goose"
mysql "github.com/go-sql-driver/mysql" mysql "github.com/go-sql-driver/mysql"
"github.com/gophish/gophish/auth"
"github.com/gophish/gophish/config" "github.com/gophish/gophish/config"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
_ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3 _ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3
@ -24,6 +26,19 @@ var conf *config.Config
const MaxDatabaseConnectionAttempts int = 10 const MaxDatabaseConnectionAttempts int = 10
// DefaultAdminUsername is the default username for the administrative user
const DefaultAdminUsername = "admin"
// InitialAdminPassword is the environment variable that specifies which
// password to use for the initial root login instead of generating one
// randomly
const InitialAdminPassword = "GOPHISH_INITIAL_ADMIN_PASSWORD"
// InitialAdminApiToken is the environment variable that specifies the
// API token to seed the initial root login instead of generating one
// randomly
const InitialAdminApiToken = "GOPHISH_INITIAL_ADMIN_API_TOKEN"
const ( const (
CampaignInProgress string = "In progress" CampaignInProgress string = "In progress"
CampaignQueued string = "Queued" CampaignQueued string = "Queued"
@ -83,8 +98,38 @@ func chooseDBDriver(name, openStr string) goose.DBDriver {
return d return d
} }
// Setup initializes the Conn object func createTemporaryPassword(u *User) error {
// It also populates the Gophish Config object var temporaryPassword string
if envPassword := os.Getenv(InitialAdminPassword); envPassword != "" {
temporaryPassword = envPassword
} else {
// This will result in a 16 character password which could be viewed as an
// inconvenience, but it should be ok for now.
temporaryPassword = auth.GenerateSecureKey(auth.MinPasswordLength)
}
hash, err := auth.GeneratePasswordHash(temporaryPassword)
if err != nil {
return err
}
u.Hash = hash
// Anytime a temporary password is created, we will force the user
// to change their password
u.PasswordChangeRequired = true
err = db.Save(u).Error
if err != nil {
return err
}
log.Infof("Please login with the username admin and the password %s", temporaryPassword)
return nil
}
// Setup initializes the database and runs any needed migrations.
//
// First, it establishes a connection to the database, then runs any migrations
// newer than the version the database is on.
//
// Once the database is up-to-date, we create an admin user (if needed) that
// has a randomly generated API key and password.
func Setup(c *config.Config) error { func Setup(c *config.Config) error {
// Setup the package-scoped config // Setup the package-scoped config
conf = c conf = c
@ -94,8 +139,6 @@ func Setup(c *config.Config) error {
Env: "production", Env: "production",
Driver: chooseDBDriver(conf.DBName, conf.DBPath), Driver: chooseDBDriver(conf.DBName, conf.DBPath),
} }
abs, _ := filepath.Abs(migrateConf.MigrationsDir)
fmt.Println(abs)
// Get the latest possible migration // Get the latest possible migration
latest, err := goose.GetMostRecentDBVersion(migrateConf.MigrationsDir) latest, err := goose.GetMostRecentDBVersion(migrateConf.MigrationsDir)
if err != nil { if err != nil {
@ -156,6 +199,7 @@ func Setup(c *config.Config) error {
} }
// Create the admin user if it doesn't exist // Create the admin user if it doesn't exist
var userCount int64 var userCount int64
var adminUser User
db.Model(&User{}).Count(&userCount) db.Model(&User{}).Count(&userCount)
adminRole, err := GetRoleBySlug(RoleAdmin) adminRole, err := GetRoleBySlug(RoleAdmin)
if err != nil { if err != nil {
@ -163,14 +207,44 @@ func Setup(c *config.Config) error {
return err return err
} }
if userCount == 0 { if userCount == 0 {
initUser := User{ adminUser := User{
Username: "admin", Username: DefaultAdminUsername,
Hash: "$2a$10$IYkPp0.QsM81lYYPrQx6W.U6oQGw7wMpozrKhKAHUBVL4mkm/EvAS", //gophish Role: adminRole,
Role: adminRole, RoleID: adminRole.ID,
RoleID: adminRole.ID, PasswordChangeRequired: true,
} }
initUser.ApiKey = generateSecureKey()
err = db.Save(&initUser).Error if envToken := os.Getenv(InitialAdminApiToken); envToken != "" {
adminUser.ApiKey = envToken
} else {
adminUser.ApiKey = auth.GenerateSecureKey(auth.APIKeyLength)
}
err = db.Save(&adminUser).Error
if err != nil {
log.Error(err)
return err
}
}
// If this is the first time the user is installing Gophish, then we will
// generate a temporary password for the admin user.
//
// We do this here instead of in the block above where the admin is created
// since there's the chance the user executes Gophish and has some kind of
// error, then tries restarting it. If they didn't grab the password out of
// the logs, then they would have lost it.
//
// By doing the temporary password here, we will regenerate that temporary
// password until the user is able to reset the admin password.
if adminUser.Username == "" {
adminUser, err = GetUserByUsername(DefaultAdminUsername)
if err != nil {
log.Error(err)
return err
}
}
if adminUser.PasswordChangeRequired {
err = createTemporaryPassword(&adminUser)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return err return err

View File

@ -138,6 +138,9 @@ func PostPage(p *Page) error {
// Per the PUT Method RFC, it presumes all data for a page is provided. // Per the PUT Method RFC, it presumes all data for a page is provided.
func PutPage(p *Page) error { func PutPage(p *Page) error {
err := p.Validate() err := p.Validate()
if err != nil {
return err
}
err = db.Where("id=?", p.Id).Save(p).Error err = db.Where("id=?", p.Id).Save(p).Error
if err != nil { if err != nil {
log.Error(err) log.Error(err)

View File

@ -13,12 +13,13 @@ var ErrModifyingOnlyAdmin = errors.New("Cannot remove the only administrator")
// User represents the user model for gophish. // User represents the user model for gophish.
type User struct { type User struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Username string `json:"username" sql:"not null;unique"` Username string `json:"username" sql:"not null;unique"`
Hash string `json:"-"` Hash string `json:"-"`
ApiKey string `json:"api_key" sql:"not null;unique"` ApiKey string `json:"api_key" sql:"not null;unique"`
Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"` Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"`
RoleID int64 `json:"-"` RoleID int64 `json:"-"`
PasswordChangeRequired bool `json:"password_change_required"`
} }
// GetUser returns the user that the given id corresponds to. If no user is found, an // GetUser returns the user that the given id corresponds to. If no user is found, an

View File

@ -27,7 +27,9 @@ func (s *ModelsSuite) TestGetUserByAPIKeyWithExistingAPIKey(c *check.C) {
u, err := GetUser(1) u, err := GetUser(1)
c.Assert(err, check.Equals, nil) c.Assert(err, check.Equals, nil)
u, err = GetUserByAPIKey(u.ApiKey) got, err := GetUserByAPIKey(u.ApiKey)
c.Assert(err, check.Equals, nil)
c.Assert(got.Id, check.Equals, u.Id)
} }
func (s *ModelsSuite) TestGetUserByAPIKeyWithNotExistingAPIKey(c *check.C) { func (s *ModelsSuite) TestGetUserByAPIKeyWithNotExistingAPIKey(c *check.C) {
@ -46,11 +48,12 @@ func (s *ModelsSuite) TestGetUserByUsernameWithNotExistingUser(c *check.C) {
} }
func (s *ModelsSuite) TestPutUser(c *check.C) { func (s *ModelsSuite) TestPutUser(c *check.C) {
u, err := GetUser(1) u, _ := GetUser(1)
u.Username = "admin_changed" u.Username = "admin_changed"
err = PutUser(&u) err := PutUser(&u)
c.Assert(err, check.Equals, nil) c.Assert(err, check.Equals, nil)
u, err = GetUser(1) u, err = GetUser(1)
c.Assert(err, check.Equals, nil)
c.Assert(u.Username, check.Equals, "admin_changed") c.Assert(u.Username, check.Equals, "admin_changed")
} }

View File

@ -29,5 +29,8 @@
"jshint-stylish": "^2.2.1", "jshint-stylish": "^2.2.1",
"webpack": "^4.32.2", "webpack": "^4.32.2",
"webpack-cli": "^3.3.2" "webpack-cli": "^3.3.2"
},
"dependencies": {
"zxcvbn": "^4.4.2"
} }
} }

File diff suppressed because one or more lines are too long

12
static/css/main.css vendored
View File

@ -730,3 +730,15 @@ table.dataTable {
.cke_autocomplete_panel>li { .cke_autocomplete_panel>li {
padding: 10px 5px !important; padding: 10px 5px !important;
} }
#password-strength {
margin-top: 20px;
margin-bottom: 0px;
height: 8px;
}
#password-strength-description {
font-size: 12px;
}
#password-strength-container {
height: 40px;
}

BIN
static/images/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),$.each(e.targets,function(e,a){targets.DataTable().row.add([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.position),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>']).draw()})}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var o=window.URL.createObjectURL(t),s=document.createElement("a");s.href=o,s.setAttribute("download",e),document.body.appendChild(s),s.click(),document.body.removeChild(s)}},deleteGroup=function(o){var e=groups.find(function(e){return e.id===o});e&&Swal.fire({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(o).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(e){e.value&&Swal.fire("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,o){var s=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),s,escapeHtml(o),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(s);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var t=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(groups,function(e,a){t.row.add([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}$(document).ready(function(){load(),$("#targetForm").submit(function(){var e=document.getElementById("targetForm");if(e.checkValidity())return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1;e.reportValidity()}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)}); var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),targetRows=[],$.each(e.targets,function(e,a){targetRows.push([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.position),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'])}),targets.DataTable().rows.add(targetRows).draw()}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var o=window.URL.createObjectURL(t),s=document.createElement("a");s.href=o,s.setAttribute("download",e),document.body.appendChild(s),s.click(),document.body.removeChild(s)}},deleteGroup=function(o){var e=groups.find(function(e){return e.id===o});e&&Swal.fire({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(o).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(e){e.value&&Swal.fire("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,o){var s=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),s,escapeHtml(o),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(s);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var a=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});a.clear(),groupRows=[],$.each(groups,function(e,a){groupRows.push([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"])}),a.rows.add(groupRows).draw()}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}$(document).ready(function(){load(),$("#targetForm").submit(function(){var e=document.getElementById("targetForm");if(e.checkValidity())return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1;e.reportValidity()}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)});

File diff suppressed because one or more lines are too long

1
static/js/dist/app/passwords.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
!function(e){var o={};function n(t){if(o[t])return o[t].exports;var a=o[t]={i:t,l:!1,exports:{}};return e[t].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=o,n.d=function(e,o,t){n.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,o){if(1&o&&(e=n(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var a in e)n.d(t,a,function(o){return e[o]}.bind(null,a));return t},n.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(o,"a",o),o},n.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},n.p="",n(n.s=1)}([,function(e,o){var n=[],t=function(){$("#name").val(""),$("#url").val(""),$("#secret").val(""),$("#is_active").prop("checked",!1),$("#flashes").empty()},a=function(){$("#webhookTable").hide(),$("#loading").show(),api.webhooks.get().success(function(e){n=e,$("#loading").hide(),$("#webhookTable").show();var o=$("#webhookTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});o.clear(),$.each(n,function(e,n){o.row.add([escapeHtml(n.name),escapeHtml(n.url),escapeHtml(n.is_active),'\n <div class="pull-right">\n <button class="btn btn-primary ping_button" data-webhook-id="'.concat(n.id,'">\n Ping\n </button>\n <button class="btn btn-primary edit_button" data-toggle="modal" data-backdrop="static" data-target="#modal" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-pencil"></i>\n </button>\n <button class="btn btn-danger delete_button" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-trash-o"></i>\n </button>\n </div>\n ')]).draw()})}).error(function(){errorFlash("Error fetching webhooks")})},c=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){var o={name:$("#name").val(),url:$("#url").val(),secret:$("#secret").val(),is_active:$("#is_active").is(":checked")};-1!=e?(o.id=e,api.webhookId.put(o).success(function(e){t(),a(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escape(o.name),'" has been updated successfully!'))}).error(function(e){modalError(e.responseJSON.message)})):api.webhooks.post(o).success(function(e){a(),t(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escape(o.name),'" has been created successfully!'))}).error(function(e){modalError(e.responseJSON.message)})}(e)}),-1!==e&&api.webhookId.get(e).success(function(e){$("#name").val(e.name),$("#url").val(e.url),$("#secret").val(e.secret),$("#is_active").prop("checked",e.is_active)}).error(function(){errorFlash("Error fetching webhook")})};$(document).ready(function(){a(),$("#modal").on("hide.bs.modal",function(){t()}),$("#new_button").on("click",function(){c(-1)}),$("#webhookTable").on("click",".edit_button",function(e){c($(this).attr("data-webhook-id"))}),$("#webhookTable").on("click",".delete_button",function(e){var o,t;o=$(this).attr("data-webhook-id"),(t=n.find(function(e){return e.id==o}))&&Swal.fire({title:"Are you sure?",text:"This will delete the webhook '".concat(escape(t.name),"'"),type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,n){api.webhookId.delete(o).success(function(o){e()}).error(function(e){n(e.responseJSON.message)})}).catch(function(e){Swal.showValidationMessage(e)})}}).then(function(e){e.value&&Swal.fire("Webhook Deleted!","The webhook has been deleted!","success"),$("button:contains('OK')").on("click",function(){location.reload()})})}),$("#webhookTable").on("click",".ping_button",function(e){var o,a;o=e.currentTarget,a=e.currentTarget.dataset.webhookId,t(),o.disabled=!0,api.webhookId.ping(a).success(function(e){o.disabled=!1,successFlash('Ping of "'.concat(escape(e.name),'" webhook succeeded.'))}).error(function(e){o.disabled=!1;var t=n.find(function(e){return e.id==a});t&&errorFlash('Ping of "'.concat(escape(t.name),'" webhook failed: "').concat(e.responseJSON.message,'"'))})})})}]); !function(e){var t={};function n(o){if(t[o])return t[o].exports;var a=t[o]={i:o,l:!1,exports:{}};return e[o].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(o,a,function(t){return e[t]}.bind(null,a));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=9)}({9:function(e,t){var n=[],o=function(){$("#name").val(""),$("#url").val(""),$("#secret").val(""),$("#is_active").prop("checked",!1),$("#flashes").empty()},a=function(){$("#webhookTable").hide(),$("#loading").show(),api.webhooks.get().success(function(e){n=e,$("#loading").hide(),$("#webhookTable").show();var t=$("#webhookTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(n,function(e,n){t.row.add([escapeHtml(n.name),escapeHtml(n.url),escapeHtml(n.is_active),'\n <div class="pull-right">\n <button class="btn btn-primary ping_button" data-webhook-id="'.concat(n.id,'">\n Ping\n </button>\n <button class="btn btn-primary edit_button" data-toggle="modal" data-backdrop="static" data-target="#modal" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-pencil"></i>\n </button>\n <button class="btn btn-danger delete_button" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-trash-o"></i>\n </button>\n </div>\n ')]).draw()})}).error(function(){errorFlash("Error fetching webhooks")})},c=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){var t={name:$("#name").val(),url:$("#url").val(),secret:$("#secret").val(),is_active:$("#is_active").is(":checked")};-1!=e?(t.id=parseInt(e),api.webhookId.put(t).success(function(e){o(),a(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escapeHtml(t.name),'" has been updated successfully!'))}).error(function(e){modalError(e.responseJSON.message)})):api.webhooks.post(t).success(function(e){a(),o(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escapeHtml(t.name),'" has been created successfully!'))}).error(function(e){modalError(e.responseJSON.message)})}(e)}),-1!==e&&api.webhookId.get(e).success(function(e){$("#name").val(e.name),$("#url").val(e.url),$("#secret").val(e.secret),$("#is_active").prop("checked",e.is_active)}).error(function(){errorFlash("Error fetching webhook")})};$(document).ready(function(){a(),$("#modal").on("hide.bs.modal",function(){o()}),$("#new_button").on("click",function(){c(-1)}),$("#webhookTable").on("click",".edit_button",function(e){c($(this).attr("data-webhook-id"))}),$("#webhookTable").on("click",".delete_button",function(e){var t,o;t=$(this).attr("data-webhook-id"),(o=n.find(function(e){return e.id==t}))&&Swal.fire({title:"Are you sure?",text:"This will delete the webhook '".concat(escapeHtml(o.name),"'"),type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,n){api.webhookId.delete(t).success(function(t){e()}).error(function(e){n(e.responseJSON.message)})}).catch(function(e){Swal.showValidationMessage(e)})}}).then(function(e){e.value&&Swal.fire("Webhook Deleted!","The webhook has been deleted!","success"),$("button:contains('OK')").on("click",function(){location.reload()})})}),$("#webhookTable").on("click",".ping_button",function(e){var t,a;t=e.currentTarget,a=e.currentTarget.dataset.webhookId,o(),t.disabled=!0,api.webhookId.ping(a).success(function(e){t.disabled=!1,successFlash('Ping of "'.concat(escapeHtml(e.name),'" webhook succeeded.'))}).error(function(e){t.disabled=!1;var o=n.find(function(e){return e.id==a});o&&errorFlash('Ping of "'.concat(escapeHtml(o.name),'" webhook failed: "').concat(e.responseJSON.message,'"'))})})})}});

View File

@ -162,15 +162,16 @@ function deleteCampaign(idx) {
} }
function setupOptions() { function setupOptions() {
api.groups.get() api.groups.summary()
.success(function (groups) { .success(function (summaries) {
groups = summaries.groups
if (groups.length == 0) { if (groups.length == 0) {
modalError("No groups found!") modalError("No groups found!")
return false; return false;
} else { } else {
var group_s2 = $.map(groups, function (obj) { var group_s2 = $.map(groups, function (obj) {
obj.text = obj.name obj.text = obj.name
obj.title = obj.targets.length + " targets" obj.title = obj.num_targets + " targets"
return obj return obj
}); });
console.log(group_s2) console.log(group_s2)
@ -360,6 +361,7 @@ $(document).ready(function () {
[1, "desc"] [1, "desc"]
] ]
}); });
campaignRows = []
$.each(campaigns, function (i, campaign) { $.each(campaigns, function (i, campaign) {
campaignTable = campaignTableOriginal campaignTable = campaignTableOriginal
if (campaign.status === "Completed") { if (campaign.status === "Completed") {
@ -378,7 +380,7 @@ $(document).ready(function () {
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "Reported : " + campaign.stats.reported var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "Reported : " + campaign.stats.reported
} }
campaignTable.row.add([ campaignRows.push([
escapeHtml(campaign.name), escapeHtml(campaign.name),
moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'), moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a'),
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>", "<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
@ -391,9 +393,10 @@ $(document).ready(function () {
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\ <button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\
<i class='fa fa-trash-o'></i>\ <i class='fa fa-trash-o'></i>\
</button></div>" </button></div>"
]).draw() ])
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
}) })
campaignTable.rows.add(campaignRows).draw()
} else { } else {
$("#emptyMessage").show() $("#emptyMessage").show()
} }

View File

@ -326,6 +326,7 @@ $(document).ready(function () {
[1, "desc"] [1, "desc"]
] ]
}); });
campaignRows = []
$.each(campaigns, function (i, campaign) { $.each(campaigns, function (i, campaign) {
var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a') var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a')
var label = statuses[campaign.status].label || "label-default"; var label = statuses[campaign.status].label || "label-default";
@ -338,8 +339,8 @@ $(document).ready(function () {
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a') launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "<br><br>" + "Reported : " + campaign.stats.email_reported var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error + "<br><br>" + "Reported : " + campaign.stats.email_reported
} }
// Add it to the table // Add it to the list
campaignTable.row.add([ campaignRows.push([
escapeHtml(campaign.name), escapeHtml(campaign.name),
campaign_date, campaign_date,
campaign.stats.sent, campaign.stats.sent,
@ -354,9 +355,10 @@ $(document).ready(function () {
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\ <button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\
<i class='fa fa-trash-o'></i>\ <i class='fa fa-trash-o'></i>\
</button></div>" </button></div>"
]).draw() ])
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
}) })
campaignTable.rows.add(campaignRows).draw()
// Build the charts // Build the charts
generateStatsPieCharts(campaigns) generateStatsPieCharts(campaigns)
generateTimelineChart(campaigns) generateTimelineChart(campaigns)

View File

@ -69,17 +69,17 @@ function edit(id) {
api.groupId.get(id) api.groupId.get(id)
.success(function (group) { .success(function (group) {
$("#name").val(group.name) $("#name").val(group.name)
targetRows = []
$.each(group.targets, function (i, record) { $.each(group.targets, function (i, record) {
targets.DataTable() targetRows.push([
.row.add([ escapeHtml(record.first_name),
escapeHtml(record.first_name), escapeHtml(record.last_name),
escapeHtml(record.last_name), escapeHtml(record.email),
escapeHtml(record.email), escapeHtml(record.position),
escapeHtml(record.position), '<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>' ])
]).draw()
}); });
targets.DataTable().rows.add(targetRows).draw()
}) })
.error(function () { .error(function () {
errorFlash("Error fetching group") errorFlash("Error fetching group")
@ -233,8 +233,9 @@ function load() {
}] }]
}); });
groupTable.clear(); groupTable.clear();
groupRows = []
$.each(groups, function (i, group) { $.each(groups, function (i, group) {
groupTable.row.add([ groupRows.push([
escapeHtml(group.name), escapeHtml(group.name),
escapeHtml(group.num_targets), escapeHtml(group.num_targets),
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'), moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
@ -244,8 +245,9 @@ function load() {
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\ <button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
<i class='fa fa-trash-o'></i>\ <i class='fa fa-trash-o'></i>\
</button></div>" </button></div>"
]).draw() ])
}) })
groupTable.rows.add(groupRows).draw()
} else { } else {
$("#emptyMessage").show() $("#emptyMessage").show()
} }

View File

@ -157,8 +157,9 @@ function load() {
}] }]
}); });
pagesTable.clear() pagesTable.clear()
pageRows = []
$.each(pages, function (i, page) { $.each(pages, function (i, page) {
pagesTable.row.add([ pageRows.push([
escapeHtml(page.name), escapeHtml(page.name),
moment(page.modified_date).format('MMMM Do YYYY, h:mm:ss a'), moment(page.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><span data-toggle='modal' data-backdrop='static' data-target='#modal'><button class='btn btn-primary' data-toggle='tooltip' data-placement='left' title='Edit Page' onclick='edit(" + i + ")'>\ "<div class='pull-right'><span data-toggle='modal' data-backdrop='static' data-target='#modal'><button class='btn btn-primary' data-toggle='tooltip' data-placement='left' title='Edit Page' onclick='edit(" + i + ")'>\
@ -170,8 +171,9 @@ function load() {
<button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Page' onclick='deletePage(" + i + ")'>\ <button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Page' onclick='deletePage(" + i + ")'>\
<i class='fa fa-trash-o'></i>\ <i class='fa fa-trash-o'></i>\
</button></div>" </button></div>"
]).draw() ])
}) })
pagesTable.rows.add(pageRows).draw()
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
} else { } else {
$("#emptyMessage").show() $("#emptyMessage").show()

View File

@ -0,0 +1,54 @@
import zxcvbn from 'zxcvbn';
const StrengthMapping = {
0: {
class: 'danger',
width: '10%',
status: 'Very Weak'
},
1: {
class: 'danger',
width: '25%',
status: 'Very Weak'
},
2: {
class: 'warning',
width: '50%',
status: 'Weak'
},
3: {
class: 'success',
width: '75%',
status: 'Good'
},
4: {
class: 'success',
width: '100%',
status: 'Very Good'
}
}
const Progress = document.getElementById("password-strength-container")
const ProgressBar = document.getElementById("password-strength-bar")
const StrengthDescription = document.getElementById("password-strength-description")
const updatePasswordStrength = (e) => {
const candidate = e.target.value
// If there is no password, clear out the progress bar
if (!candidate) {
ProgressBar.style.width = 0
StrengthDescription.textContent = ""
Progress.classList.add("hidden")
return
}
const score = zxcvbn(candidate).score
const evaluation = StrengthMapping[score]
// Update the progress bar
ProgressBar.classList = `progress-bar progress-bar-${evaluation.class}`
ProgressBar.style.width = evaluation.width
StrengthDescription.textContent = evaluation.status
StrengthDescription.classList = `text-${evaluation.class}`
Progress.classList.remove("hidden")
}
document.getElementById("password").addEventListener("input", updatePasswordStrength)

View File

@ -200,8 +200,9 @@ function load() {
}] }]
}); });
profileTable.clear() profileTable.clear()
profileRows = []
$.each(profiles, function (i, profile) { $.each(profiles, function (i, profile) {
profileTable.row.add([ profileRows.push([
escapeHtml(profile.name), escapeHtml(profile.name),
profile.interface_type, profile.interface_type,
moment(profile.modified_date).format('MMMM Do YYYY, h:mm:ss a'), moment(profile.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
@ -214,8 +215,9 @@ function load() {
<button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Profile' onclick='deleteProfile(" + i + ")'>\ <button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Profile' onclick='deleteProfile(" + i + ")'>\
<i class='fa fa-trash-o'></i>\ <i class='fa fa-trash-o'></i>\
</button></div>" </button></div>"
]).draw() ])
}) })
profileTable.rows.add(profileRows).draw()
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
} else { } else {
$("#emptyMessage").show() $("#emptyMessage").show()

View File

@ -191,17 +191,19 @@ function edit(idx) {
$("#subject").val(template.subject) $("#subject").val(template.subject)
$("#html_editor").val(template.html) $("#html_editor").val(template.html)
$("#text_editor").val(template.text) $("#text_editor").val(template.text)
attachmentRows = []
$.each(template.attachments, function (i, file) { $.each(template.attachments, function (i, file) {
var icon = icons[file.type] || "fa-file-o" var icon = icons[file.type] || "fa-file-o"
// Add the record to the modal // Add the record to the modal
attachmentsTable.row.add([ attachmentRows.push([
'<i class="fa ' + icon + '"></i>', '<i class="fa ' + icon + '"></i>',
escapeHtml(file.name), escapeHtml(file.name),
'<span class="remove-row"><i class="fa fa-trash-o"></i></span>', '<span class="remove-row"><i class="fa fa-trash-o"></i></span>',
file.content, file.content,
file.type || "application/octet-stream" file.type || "application/octet-stream"
]).draw() ])
}) })
attachmentsTable.rows.add(attachmentRows).draw()
if (template.html.indexOf("{{.Tracker}}") != -1) { if (template.html.indexOf("{{.Tracker}}") != -1) {
$("#use_tracker_checkbox").prop("checked", true) $("#use_tracker_checkbox").prop("checked", true)
} else { } else {
@ -316,8 +318,9 @@ function load() {
}] }]
}); });
templateTable.clear() templateTable.clear()
templateRows = []
$.each(templates, function (i, template) { $.each(templates, function (i, template) {
templateTable.row.add([ templateRows.push([
escapeHtml(template.name), escapeHtml(template.name),
moment(template.modified_date).format('MMMM Do YYYY, h:mm:ss a'), moment(template.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
"<div class='pull-right'><span data-toggle='modal' data-backdrop='static' data-target='#modal'><button class='btn btn-primary' data-toggle='tooltip' data-placement='left' title='Edit Template' onclick='edit(" + i + ")'>\ "<div class='pull-right'><span data-toggle='modal' data-backdrop='static' data-target='#modal'><button class='btn btn-primary' data-toggle='tooltip' data-placement='left' title='Edit Template' onclick='edit(" + i + ")'>\
@ -329,8 +332,9 @@ function load() {
<button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Template' onclick='deleteTemplate(" + i + ")'>\ <button class='btn btn-danger' data-toggle='tooltip' data-placement='left' title='Delete Template' onclick='deleteTemplate(" + i + ")'>\
<i class='fa fa-trash-o'></i>\ <i class='fa fa-trash-o'></i>\
</button></div>" </button></div>"
]).draw() ])
}) })
templateTable.rows.add(templateRows).draw()
$('[data-toggle="tooltip"]').tooltip() $('[data-toggle="tooltip"]').tooltip()
} else { } else {
$("#emptyMessage").show() $("#emptyMessage").show()

View File

@ -10,7 +10,8 @@ const save = (id) => {
let user = { let user = {
username: $("#username").val(), username: $("#username").val(),
password: $("#password").val(), password: $("#password").val(),
role: $("#role").val() role: $("#role").val(),
password_change_required: $("#force_password_change_checkbox").prop('checked')
} }
// Submit the user // Submit the user
if (id != -1) { if (id != -1) {
@ -18,26 +19,26 @@ const save = (id) => {
// we need to PUT /user/:id // we need to PUT /user/:id
user.id = id user.id = id
api.userId.put(user) api.userId.put(user)
.success(function (data) { .success((data) => {
successFlash("User " + escapeHtml(user.username) + " updated successfully!") successFlash("User " + escapeHtml(user.username) + " updated successfully!")
load() load()
dismiss() dismiss()
$("#modal").modal('hide') $("#modal").modal('hide')
}) })
.error(function (data) { .error((data) => {
modalError(data.responseJSON.message) modalError(data.responseJSON.message)
}) })
} else { } else {
// Else, if this is a new user, POST it // Else, if this is a new user, POST it
// to /user // to /user
api.users.post(user) api.users.post(user)
.success(function (data) { .success((data) => {
successFlash("User " + escapeHtml(user.username) + " registered successfully!") successFlash("User " + escapeHtml(user.username) + " registered successfully!")
load() load()
dismiss() dismiss()
$("#modal").modal('hide') $("#modal").modal('hide')
}) })
.error(function (data) { .error((data) => {
modalError(data.responseJSON.message) modalError(data.responseJSON.message)
}) })
} }
@ -61,10 +62,11 @@ const edit = (id) => {
$("#role").trigger("change") $("#role").trigger("change")
} else { } else {
api.userId.get(id) api.userId.get(id)
.success(function (user) { .success((user) => {
$("#username").val(user.username) $("#username").val(user.username)
$("#role").val(user.role.slug) $("#role").val(user.role.slug)
$("#role").trigger("change") $("#role").trigger("change")
$("#force_password_change_checkbox").prop('checked', false)
}) })
.error(function () { .error(function () {
errorFlash("Error fetching user") errorFlash("Error fetching user")
@ -115,6 +117,55 @@ const deleteUser = (id) => {
}) })
} }
const impersonate = (id) => {
var user = users.find(x => x.id == id)
if (!user) {
return
}
Swal.fire({
title: "Are you sure?",
html: "You will be logged out of your account and logged in as <strong>" + escapeHtml(user.username) + "</strong>",
type: "warning",
animation: false,
showCancelButton: true,
confirmButtonText: "Swap User",
confirmButtonColor: "#428bca",
reverseButtons: true,
allowOutsideClick: false,
}).then((result) => {
if (result.value) {
fetch('/impersonate', {
method: 'post',
body: "username=" + user.username + "&csrf_token=" + encodeURIComponent(csrf_token),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}).then((response) => {
if (response.status == 200) {
Swal.fire({
title: "Success!",
html: "Successfully changed to user <strong>" + escapeHtml(user.username) + "</strong>.",
type: "success",
showCancelButton: false,
confirmButtonText: "Home",
allowOutsideClick: false,
}).then((result) => {
if (result.value) {
window.location.href = "/"
}});
} else {
Swal.fire({
title: "Error!",
type: "error",
html: "Failed to change to user <strong>" + escapeHtml(user.username) + "</strong>.",
showCancelButton: false,
})
}
})
}
})
}
const load = () => { const load = () => {
$("#userTable").hide() $("#userTable").hide()
@ -132,18 +183,24 @@ const load = () => {
}] }]
}); });
userTable.clear(); userTable.clear();
userRows = []
$.each(users, (i, user) => { $.each(users, (i, user) => {
userTable.row.add([ userRows.push([
escapeHtml(user.username), escapeHtml(user.username),
escapeHtml(user.role.name), escapeHtml(user.role.name),
"<div class='pull-right'><button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='" + user.id + "'>\ "<div class='pull-right'>\
<button class='btn btn-warning impersonate_button' data-user-id='" + user.id + "'>\
<i class='fa fa-retweet'></i>\
</button>\
<button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='" + user.id + "'>\
<i class='fa fa-pencil'></i>\ <i class='fa fa-pencil'></i>\
</button>\ </button>\
<button class='btn btn-danger delete_button' data-user-id='" + user.id + "'>\ <button class='btn btn-danger delete_button' data-user-id='" + user.id + "'>\
<i class='fa fa-trash-o'></i>\ <i class='fa fa-trash-o'></i>\
</button></div>" </button></div>"
]).draw() ])
}) })
userTable.rows.add(userRows).draw();
}) })
.error(() => { .error(() => {
errorFlash("Error fetching users") errorFlash("Error fetching users")
@ -180,4 +237,7 @@ $(document).ready(function () {
$("#userTable").on('click', '.delete_button', function (e) { $("#userTable").on('click', '.delete_button', function (e) {
deleteUser($(this).attr('data-user-id')) deleteUser($(this).attr('data-user-id'))
}) })
$("#userTable").on('click', '.impersonate_button', function (e) {
impersonate($(this).attr('data-user-id'))
})
}); });

View File

@ -16,13 +16,13 @@ const saveWebhook = (id) => {
is_active: $("#is_active").is(":checked"), is_active: $("#is_active").is(":checked"),
}; };
if (id != -1) { if (id != -1) {
wh.id = id; wh.id = parseInt(id);
api.webhookId.put(wh) api.webhookId.put(wh)
.success(function(data) { .success(function(data) {
dismiss(); dismiss();
load(); load();
$("#modal").modal("hide"); $("#modal").modal("hide");
successFlash(`Webhook "${escape(wh.name)}" has been updated successfully!`); successFlash(`Webhook "${escapeHtml(wh.name)}" has been updated successfully!`);
}) })
.error(function(data) { .error(function(data) {
modalError(data.responseJSON.message) modalError(data.responseJSON.message)
@ -33,7 +33,7 @@ const saveWebhook = (id) => {
load(); load();
dismiss(); dismiss();
$("#modal").modal("hide"); $("#modal").modal("hide");
successFlash(`Webhook "${escape(wh.name)}" has been created successfully!`); successFlash(`Webhook "${escapeHtml(wh.name)}" has been created successfully!`);
}) })
.error(function(data) { .error(function(data) {
modalError(data.responseJSON.message) modalError(data.responseJSON.message)
@ -108,7 +108,7 @@ const deleteWebhook = (id) => {
} }
Swal.fire({ Swal.fire({
title: "Are you sure?", title: "Are you sure?",
text: `This will delete the webhook '${escape(wh.name)}'`, text: `This will delete the webhook '${escapeHtml(wh.name)}'`,
type: "warning", type: "warning",
animation: false, animation: false,
showCancelButton: true, showCancelButton: true,
@ -150,7 +150,7 @@ const pingUrl = (btn, whId) => {
api.webhookId.ping(whId) api.webhookId.ping(whId)
.success(function(wh) { .success(function(wh) {
btn.disabled = false; btn.disabled = false;
successFlash(`Ping of "${escape(wh.name)}" webhook succeeded.`); successFlash(`Ping of "${escapeHtml(wh.name)}" webhook succeeded.`);
}) })
.error(function(data) { .error(function(data) {
btn.disabled = false; btn.disabled = false;
@ -158,7 +158,7 @@ const pingUrl = (btn, whId) => {
if (!wh) { if (!wh) {
return return
} }
errorFlash(`Ping of "${escape(wh.name)}" webhook failed: "${data.responseJSON.message}"`) errorFlash(`Ping of "${escapeHtml(wh.name)}" webhook failed: "${data.responseJSON.message}"`)
}); });
}; };

View File

@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Gophish - Phishing Toolkit"> <meta name="description" content="Gophish - Phishing Toolkit">
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)"> <meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
<link rel="shortcut icon" href="../../docs-assets/ico/favicon.png"> <link rel="shortcut icon" href="/images/favicon.ico">
<title>{{ .Title }} - Gophish</title> <title>{{ .Title }} - Gophish</title>

View File

@ -6,7 +6,8 @@
<div id="loading"> <div id="loading">
<i class="fa fa-spinner fa-spin fa-4x"></i> <i class="fa fa-spinner fa-spin fa-4x"></i>
</div> </div>
<div id="emptyMessage" class="row" style="display:none;"> {{template "flashes" .Flashes}}
<div id="emptyMessage" style="display:none;">
<div class="alert alert-info"> <div class="alert alert-info">
No campaigns created yet. Let's create one! No campaigns created yet. Let's create one!
</div> </div>

View File

@ -8,6 +8,8 @@
fa-exclamation-triangle fa-exclamation-triangle
{{else if eq .Type "success"}} {{else if eq .Type "success"}}
fa-check-circle fa-check-circle
{{else if eq .Type "info"}}
fa-info-circle
{{end}}"></i> {{end}}"></i>
{{.Message}} {{.Message}}
</div> </div>

View File

@ -8,7 +8,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Gophish - Open-Source Phishing Toolkit"> <meta name="description" content="Gophish - Open-Source Phishing Toolkit">
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)"> <meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
<link rel="shortcut icon" href="../../docs-assets/ico/favicon.png"> <link rel="shortcut icon" href="/images/favicon.ico">
<title>Gophish - {{ .Title }}</title> <title>Gophish - {{ .Title }}</title>

View File

@ -0,0 +1,75 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Gophish - Open-Source Phishing Toolkit">
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
<link rel="shortcut icon" href="../../docs-assets/ico/favicon.png">
<title>Gophish - {{ .Title }}</title>
<link href="/css/dist/gophish.css" rel="stylesheet">
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet'
type='text/css'>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<img class="navbar-logo" src="/images/logo_inv_small.png" />
<a class="navbar-brand" href="/">&nbsp;gophish</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<div class="btn-group" id="navbar-dropdown">
<a class="btn btn-primary" href="/logout"><i class="fa fa-user"></i> {{.User.Username}}</a>
<a class="btn btn-primary dropdown-toggle" href="/logout">
<i class="fa fa-sign-out"></i>
</a>
</div>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container">
<form class="form-signin" action="" method="POST">
<img id="logo" src="/images/logo_purple.png" />
<h2 class="form-signin-heading">Reset Your Password</h2>
{{template "flashes" .Flashes}}
<input type="password" id="password" name="password" class="form-control" placeholder="Password"
autocomplete="off" minlength="8" required autofocus>
<div class="" id="password-strength-container">
<div class="progress" id="password-strength">
<div id="password-strength-bar" class="progress-bar" role="progressbar" aria-valuenow="0"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
<span id="password-strength-description"></span>
</div>
<input type="password" name="confirm_password" class="form-control" placeholder="Confirm Password"
autocomplete="off" minlength="8" required>
<input type="hidden" name="csrf_token" value="{{.Token}}" />
<br />
<button class="btn btn-lg btn-primary btn-block" type="submit">Save Password</button>
</form>
</div>
<!-- Placed at the end of the document so the pages load faster -->
<script src="/js/dist/app/passwords.min.js"></script>
<script src="/js/dist/vendor.min.js"></script>
</body>
</html>
{{ end }}

View File

@ -4,6 +4,7 @@
<h1 class="page-header">Settings</h1> <h1 class="page-header">Settings</h1>
</div> </div>
<div id="flashes" class="row"></div> <div id="flashes" class="row"></div>
{{template "flashes" .Flashes}}
<!-- Nav tabs --> <!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab" <li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
@ -58,8 +59,15 @@
<div class="row"> <div class="row">
<label for="new_password" class="col-sm-2 control-label form-label">New Password:</label> <label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
<div class="col-md-6"> <div class="col-md-6">
<input type="password" id="new_password" name="new_password" autocomplete="off" <input type="password" id="password" name="new_password" autocomplete="new-password"
class="form-control" /> class="form-control" />
<div class="hidden" id="password-strength-container">
<div class="progress" id="password-strength">
<div id="password-strength-bar" class="progress-bar" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<span id="password-strength-description"></span>
</div>
</div> </div>
</div> </div>
<br /> <br />
@ -225,5 +233,6 @@
</div> </div>
</div> </div>
{{end}} {{define "scripts"}} {{end}} {{define "scripts"}}
<script src="/js/dist/app/passwords.min.js"></script>
<script src="/js/dist/app/settings.min.js"></script> <script src="/js/dist/app/settings.min.js"></script>
{{end}} {{end}}

View File

@ -5,7 +5,9 @@
{{.Title}} {{.Title}}
</h1> </h1>
</div> </div>
<div id="flashes" class="row"></div> <div id="flashes" class="row">
{{template "flashes" .Flashes}}
</div>
<div class="row"> <div class="row">
<button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static" <button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
data-user-id="-1" data-target="#modal"> data-user-id="-1" data-target="#modal">
@ -47,13 +49,23 @@
</div> </div>
<label class="control-label" for="password">Password:</label> <label class="control-label" for="password">Password:</label>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" placeholder="Password" id="password" required /> <input type="password" class="form-control" autocomplete="new-password" placeholder="Password" id="password" required />
<div class="hidden" id="password-strength-container">
<div class="progress" id="password-strength">
<div id="password-strength-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<span id="password-strength-description"></span>
</div>
</div> </div>
<label class="control-label" for="confirm_password">Confirm Password:</label> <label class="control-label" for="confirm_password">Confirm Password:</label>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password" <input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password"
required /> required />
</div> </div>
<div class="checkbox checkbox-primary">
<input id="force_password_change_checkbox" type="checkbox" checked>
<label for="force_password_change_checkbox">Require the user to set a new password</label>
</div>
<label class="control-label" for="role">Role:</label> <label class="control-label" for="role">Role:</label>
<div class="form-group" id="role-select"> <div class="form-group" id="role-select">
<select class="form-control" placeholder="" id="role" /> <select class="form-control" placeholder="" id="role" />
@ -70,5 +82,6 @@
</div> </div>
</div> </div>
{{end}} {{define "scripts"}} {{end}} {{define "scripts"}}
<script src="/js/dist/app/passwords.min.js"></script>
<script src="/js/dist/app/users.min.js"></script> <script src="/js/dist/app/users.min.js"></script>
{{end}} {{end}}

View File

@ -21,7 +21,6 @@ import (
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"github.com/jordan-wright/email" "github.com/jordan-wright/email"
"golang.org/x/crypto/bcrypt"
) )
var ( var (
@ -138,6 +137,9 @@ func CheckAndCreateSSL(cp string, kp string) error {
log.Infof("Creating new self-signed certificates for administration interface") log.Infof("Creating new self-signed certificates for administration interface")
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return fmt.Errorf("error generating tls private key: %v", err)
}
notBefore := time.Now() notBefore := time.Now()
// Generate a certificate that lasts for 10 years // Generate a certificate that lasts for 10 years
@ -147,7 +149,7 @@ func CheckAndCreateSSL(cp string, kp string) error {
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil { if err != nil {
return fmt.Errorf("TLS Certificate Generation: Failed to generate a random serial number: %s", err) return fmt.Errorf("tls certificate generation: failed to generate a random serial number: %s", err)
} }
template := x509.Certificate{ template := x509.Certificate{
@ -165,24 +167,24 @@ func CheckAndCreateSSL(cp string, kp string) error {
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv) derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
if err != nil { if err != nil {
return fmt.Errorf("TLS Certificate Generation: Failed to create certificate: %s", err) return fmt.Errorf("tls certificate generation: failed to create certificate: %s", err)
} }
certOut, err := os.Create(cp) certOut, err := os.Create(cp)
if err != nil { if err != nil {
return fmt.Errorf("TLS Certificate Generation: Failed to open %s for writing: %s", cp, err) return fmt.Errorf("tls certificate generation: failed to open %s for writing: %s", cp, err)
} }
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close() certOut.Close()
keyOut, err := os.OpenFile(kp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) keyOut, err := os.OpenFile(kp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil { if err != nil {
return fmt.Errorf("TLS Certificate Generation: Failed to open %s for writing", kp) return fmt.Errorf("tls certificate generation: failed to open %s for writing", kp)
} }
b, err := x509.MarshalECPrivateKey(priv) b, err := x509.MarshalECPrivateKey(priv)
if err != nil { if err != nil {
return fmt.Errorf("TLS Certificate Generation: Unable to marshal ECDSA private key: %v", err) return fmt.Errorf("tls certificate generation: unable to marshal ECDSA private key: %v", err)
} }
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}) pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
@ -191,21 +193,3 @@ func CheckAndCreateSSL(cp string, kp string) error {
log.Info("TLS Certificate Generation complete") log.Info("TLS Certificate Generation complete")
return nil return nil
} }
// GenerateSecureKey creates a secure key to use as an API key
func GenerateSecureKey() string {
// Inspired from gorilla/securecookie
k := make([]byte, 32)
io.ReadFull(rand.Reader, k)
return fmt.Sprintf("%x", k)
}
// NewHash hashes the provided password and returns the bcrypt hash (using the
// default 10 rounds) as a string.
func NewHash(pass string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}

View File

@ -16,20 +16,24 @@ import (
const ( const (
// DefaultTimeoutSeconds is amount of seconds of timeout used by HTTP sender // DefaultTimeoutSeconds is the number of seconds before a timeout occurs
// when sending a webhook
DefaultTimeoutSeconds = 10 DefaultTimeoutSeconds = 10
// MinHTTPStatusErrorCode is the lowest number of an HTTP response which indicates an error // MinHTTPStatusErrorCode is the lower bound of HTTP status codes which
// indicate an error occurred
MinHTTPStatusErrorCode = 400 MinHTTPStatusErrorCode = 400
// SignatureHeader is the name of an HTTP header used to which contains signature of a webhook // SignatureHeader is the name of the HTTP header which contains the
// webhook signature
SignatureHeader = "X-Gophish-Signature" SignatureHeader = "X-Gophish-Signature"
// Sha256Prefix is the prefix that specifies the hashing algorithm used for signature // Sha256Prefix is the prefix that specifies the hashing algorithm used
// for the signature
Sha256Prefix = "sha256" Sha256Prefix = "sha256"
) )
// Sender defines behaviour of an entity by which webhook is sent // Sender represents a type which can send webhooks to an EndPoint
type Sender interface { type Sender interface {
Send(endPoint EndPoint, data interface{}) error Send(endPoint EndPoint, data interface{}) error
} }
@ -47,7 +51,8 @@ var senderInstance = &defaultSender{
}, },
} }
// EndPoint represents and end point to send a webhook to: url and secret by which payload is signed // EndPoint represents a URL to send the webhook to, as well as a secret used
// to sign the event
type EndPoint struct { type EndPoint struct {
URL string URL string
Secret string Secret string
@ -58,12 +63,12 @@ func Send(endPoint EndPoint, data interface{}) error {
return senderInstance.Send(endPoint, data) return senderInstance.Send(endPoint, data)
} }
// SendAll sends data to each of the EndPoints // SendAll sends data to multiple EndPoints
func SendAll(endPoints []EndPoint, data interface{}) { func SendAll(endPoints []EndPoint, data interface{}) {
for _, ept := range endPoints { for _, e := range endPoints {
go func(ept1 EndPoint) { go func(e EndPoint) {
senderInstance.Send(ept1, data) senderInstance.Send(e, data)
}(EndPoint{URL: ept.URL, Secret: ept.Secret}) }(e)
} }
} }
@ -76,7 +81,15 @@ func (ds defaultSender) Send(endPoint EndPoint, data interface{}) error {
} }
req, err := http.NewRequest("POST", endPoint.URL, bytes.NewBuffer(jsonData)) req, err := http.NewRequest("POST", endPoint.URL, bytes.NewBuffer(jsonData))
if err != nil {
log.Error(err)
return err
}
signat, err := sign(endPoint.Secret, jsonData) signat, err := sign(endPoint.Secret, jsonData)
if err != nil {
log.Error(err)
return err
}
req.Header.Set(SignatureHeader, fmt.Sprintf("%s=%s", Sha256Prefix, signat)) req.Header.Set(SignatureHeader, fmt.Sprintf("%s=%s", Sha256Prefix, signat))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := ds.client.Do(req) resp, err := ds.client.Do(req)

View File

@ -3,6 +3,7 @@ const path = require('path');
module.exports = { module.exports = {
context: path.resolve(__dirname, 'static', 'js', 'src', 'app'), context: path.resolve(__dirname, 'static', 'js', 'src', 'app'),
entry: { entry: {
passwords: './passwords',
users: './users', users: './users',
webhooks: './webhooks', webhooks: './webhooks',
}, },

View File

@ -125,6 +125,11 @@ func (w *DefaultWorker) LaunchCampaign(c models.Campaign) {
// that implements an interface as a slice of that interface. // that implements an interface as a slice of that interface.
mailEntries := []mailer.Mail{} mailEntries := []mailer.Mail{}
currentTime := time.Now().UTC() currentTime := time.Now().UTC()
campaignMailCtx, err := models.GetCampaignMailContext(c.Id, c.UserId)
if err != nil {
log.Error(err)
return
}
for _, m := range ms { for _, m := range ms {
// Only send the emails scheduled to be sent for the past minute to // Only send the emails scheduled to be sent for the past minute to
// respect the campaign scheduling options // respect the campaign scheduling options
@ -132,7 +137,7 @@ func (w *DefaultWorker) LaunchCampaign(c models.Campaign) {
m.Unlock() m.Unlock()
continue continue
} }
err = m.CacheCampaign(&c) err = m.CacheCampaign(&campaignMailCtx)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return return
@ -150,11 +155,3 @@ func (w *DefaultWorker) SendTestEmail(s *models.EmailRequest) error {
}() }()
return <-s.ErrorChan return <-s.ErrorChan
} }
// errorMail is a helper to handle erroring out a slice of Mail instances
// in the case that an unrecoverable error occurs.
func errorMail(err error, ms []mailer.Mail) {
for _, m := range ms {
m.Error(err)
}
}

View File

@ -15,9 +15,7 @@ type logMailer struct {
queue chan []mailer.Mail queue chan []mailer.Mail
} }
func (m *logMailer) Start(ctx context.Context) { func (m *logMailer) Start(ctx context.Context) {}
return
}
func (m *logMailer) Queue(ms []mailer.Mail) { func (m *logMailer) Queue(ms []mailer.Mail) {
m.queue <- ms m.queue <- ms

View File

@ -5242,3 +5242,8 @@ yargs@^7.1.0:
which-module "^1.0.0" which-module "^1.0.0"
y18n "^3.2.1" y18n "^3.2.1"
yargs-parser "^5.0.0" yargs-parser "^5.0.0"
zxcvbn@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"
integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=