mirror of https://github.com/gophish/gophish
Merge branch 'master' into 1451-template-dates
commit
f8a85014e6
|
@ -0,0 +1,34 @@
|
|||
name: CI
|
||||
on: [push]
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goVer: [1.16, 1.17, 1.18]
|
||||
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.goVer }}
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.goVer }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
go get gopkg.in/check.v1
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
|
||||
- name: Format
|
||||
run: diff -u <(echo -n) <(gofmt -d .)
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
|
@ -0,0 +1,153 @@
|
|||
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 "RELEASE=gophish-${{ github.event.release.tag_name }}-${{ matrix.releaseos }}-32bit" >> $GITHUB_ENV
|
||||
- if: matrix.arch == 'amd64'
|
||||
run: echo "RELEASE=gophish-${{ github.event.release.tag_name }}-${{ matrix.releaseos }}-64bit" >> $GITHUB_ENV
|
||||
- if: matrix.os == 'windows-latest'
|
||||
run: echo "RELEASE=gophish-${{ github.event.release.tag_name }}-${{ matrix.releaseos }}-64bit" | Out-File -FilePath $env:GITHUB_ENV -Append # https://github.com/actions/runner/issues/1636
|
||||
- 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}";
|
|
@ -13,6 +13,7 @@ node_modules
|
|||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
.DS_Store
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
|
@ -27,4 +28,4 @@ gophish_admin.key
|
|||
|
||||
*.exe
|
||||
gophish.db*
|
||||
gophish
|
||||
gophish
|
||||
|
|
14
.travis.yml
14
.travis.yml
|
@ -1,14 +0,0 @@
|
|||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.9
|
||||
- "1.10"
|
||||
- 1.11
|
||||
- 1.12
|
||||
- tip
|
||||
|
||||
install:
|
||||
- go get -d -v ./... && go build -v ./...
|
||||
- go get gopkg.in/check.v1
|
||||
- go get github.com/stretchr/testify
|
49
Dockerfile
49
Dockerfile
|
@ -1,30 +1,45 @@
|
|||
# setup build image
|
||||
FROM golang:1.11 AS build
|
||||
# Minify client side assets (JavaScript)
|
||||
FROM node:latest AS build-js
|
||||
|
||||
# build Gophish binary
|
||||
WORKDIR /build/gophish
|
||||
RUN npm install gulp gulp-cli -g
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN go get -d -v ./...
|
||||
RUN go build
|
||||
RUN npm install --only=dev
|
||||
RUN gulp
|
||||
|
||||
|
||||
# setup run image
|
||||
# Build Golang binary
|
||||
FROM golang:1.15.2 AS build-golang
|
||||
|
||||
WORKDIR /go/src/github.com/gophish/gophish
|
||||
COPY . .
|
||||
RUN go get -v && go build -v
|
||||
|
||||
|
||||
# Runtime container
|
||||
FROM debian:stable-slim
|
||||
|
||||
RUN useradd -m -d /opt/gophish -s /bin/bash app
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
jq && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
apt-get install --no-install-recommends -y jq libcap2-bin ca-certificates && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# copy Gophish assets from the build image
|
||||
WORKDIR /gophish
|
||||
COPY --from=build /build/gophish/ /gophish/
|
||||
RUN chmod +x gophish
|
||||
WORKDIR /opt/gophish
|
||||
COPY --from=build-golang /go/src/github.com/gophish/gophish/ ./
|
||||
COPY --from=build-js /build/static/js/dist/ ./static/js/dist/
|
||||
COPY --from=build-js /build/static/css/dist/ ./static/css/dist/
|
||||
COPY --from=build-golang /go/src/github.com/gophish/gophish/config.json ./
|
||||
RUN chown app. config.json
|
||||
|
||||
# expose the admin port to the host
|
||||
RUN setcap 'cap_net_bind_service=+ep' /opt/gophish/gophish
|
||||
|
||||
USER app
|
||||
RUN sed -i 's/127.0.0.1/0.0.0.0/g' config.json
|
||||
RUN touch config.json.tmp
|
||||
|
||||
# expose default ports
|
||||
EXPOSE 80 443 3333
|
||||
EXPOSE 3333 8080 8443 80
|
||||
|
||||
CMD ["./docker/run.sh"]
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2017 Jordan Wright
|
||||
Copyright (c) 2013-2020 Jordan Wright
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software ("Gophish Community Edition") and associated documentation files (the "Software"), to deal in
|
||||
|
|
18
README.md
18
README.md
|
@ -3,7 +3,7 @@
|
|||
Gophish
|
||||
=======
|
||||
|
||||
[![Build Status](https://travis-ci.org/gophish/gophish.svg?branch=master)](https://travis-ci.org/gophish/gophish) [![GoDoc](https://godoc.org/github.com/gophish/gophish?status.svg)](https://godoc.org/github.com/gophish/gophish)
|
||||
![Build Status](https://github.com/gophish/gophish/workflows/CI/badge.svg) [![GoDoc](https://godoc.org/github.com/gophish/gophish?status.svg)](https://godoc.org/github.com/gophish/gophish)
|
||||
|
||||
Gophish: Open-Source Phishing Toolkit
|
||||
|
||||
|
@ -14,15 +14,21 @@ Gophish: Open-Source Phishing Toolkit
|
|||
Installation of Gophish is dead-simple - just download and extract the zip containing the [release for your system](https://github.com/gophish/gophish/releases/), and run the binary. Gophish has binary releases for Windows, Mac, and Linux platforms.
|
||||
|
||||
### Building From Source
|
||||
**If you are building from source, please note that Gophish requires Go v1.9 or above!**
|
||||
**If you are building from source, please note that Gophish requires Go v1.10 or above!**
|
||||
|
||||
To build Gophish from source, simply run ```go get github.com/gophish/gophish``` and ```cd``` into the project source directory. Then, run ```go build```. After this, you should have a binary called ```gophish``` in the current directory.
|
||||
To build Gophish from source, simply run ```git clone https://github.com/gophish/gophish.git``` and ```cd``` into the project source directory. Then, run ```go build```. After this, you should have a binary called ```gophish``` in the current directory.
|
||||
|
||||
### Docker
|
||||
You can also use Gophish via an unofficial Docker container [here](https://hub.docker.com/r/matteoggl/gophish/).
|
||||
You can also use Gophish via the official Docker container [here](https://hub.docker.com/r/gophish/gophish/).
|
||||
|
||||
### Setup
|
||||
After running the Gophish binary, open an Internet browser to https://localhost:3333 and login with the default username (admin) and password (gophish).
|
||||
After running the Gophish binary, open an Internet browser to https://localhost:3333 and login with the default username and password listed in the log output.
|
||||
e.g.
|
||||
```
|
||||
time="2020-07-29T01:24:08Z" level=info msg="Please login with the username admin and the password 4304d5255378177d"
|
||||
```
|
||||
|
||||
Releases of Gophish prior to v0.10.1 have a default username of `admin` and password of `gophish`.
|
||||
|
||||
### Documentation
|
||||
|
||||
|
@ -38,7 +44,7 @@ Gophish - Open-Source Phishing Framework
|
|||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 - 2018 Jordan Wright
|
||||
Copyright (c) 2013 - 2020 Jordan Wright
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software ("Gophish Community Edition") and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Thank you for taking the time to find and report a security vulnerability in Gophish!
|
||||
|
||||
I'd ask that you please send me an email with the details at hi@getgophish.com rather than posting any details in the public issue tracker.
|
||||
|
||||
I'll happily work with you to get the vulnerability resolved as quickly as possible, and will be sure to credit you (if you'd like!) in the release notes for the following release.
|
|
@ -1,4 +1,4 @@
|
|||
Tested on Ubuntu 16.04.4.
|
||||
Tested on Ubuntu 20.04 LTS.
|
||||
|
||||
Installs Postfix (to listen on localhost only) and the latest Linux gophish binary. setcap is used to allow the gophish binary to listen on privileged ports without running as root.
|
||||
|
||||
|
@ -17,7 +17,7 @@ ansible-playbook site.yml -i hosts -u root --private-key=private.key
|
|||
ansible-playbook site.yml -i hosts -u root --ask-pass
|
||||
|
||||
# Log in as non-root user with SSH key (if root login has been disabled)
|
||||
ansible-playbook site.yml -i hosts --private-key=private.key -u user --become --ask-sudo-pass
|
||||
ansible-playbook site.yml -i hosts --private-key=private.key -u user --become --ask-become-pass
|
||||
|
||||
# Logging in as non-root user without SSH keys
|
||||
ansible-playbook site.yml -i hosts -u ubuntu --ask-pass --become --ask-sudo-pass
|
||||
ansible-playbook site.yml -i hosts -u ubuntu --ask-pass --become --ask-become-pass
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
{
|
||||
"admin_server" : {
|
||||
"listen_url" : "127.0.0.1:3333",
|
||||
"use_tls" : true,
|
||||
"cert_path" : "gophish_admin.crt",
|
||||
"key_path" : "gophish_admin.key"
|
||||
},
|
||||
"phish_server" : {
|
||||
"listen_url" : "0.0.0.0:80",
|
||||
"use_tls" : false,
|
||||
"cert_path" : "example.crt",
|
||||
"key_path": "example.key"
|
||||
},
|
||||
"db_name" : "sqlite3",
|
||||
"db_path" : "gophish.db",
|
||||
"migrations_prefix" : "db/db_"
|
||||
"admin_server": {
|
||||
"listen_url": "127.0.0.1:3333",
|
||||
"use_tls": true,
|
||||
"cert_path": "/etc/ssl/crt/gophish.crt",
|
||||
"key_path": "/etc/ssl/private/gophish.pem"
|
||||
},
|
||||
"phish_server": {
|
||||
"listen_url": "127.0.0.1:8080",
|
||||
"use_tls": true,
|
||||
"cert_path": "/etc/ssl/crt/gophish.crt",
|
||||
"key_path": "/etc/ssl/private/gophish.pem"
|
||||
},
|
||||
"db_name": "sqlite3",
|
||||
"db_path": "gophish.db",
|
||||
"migrations_prefix": "db/db_",
|
||||
"contact_address": "",
|
||||
"logging": {
|
||||
"filename": "gophish.log",
|
||||
"level": ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,22 +2,27 @@
|
|||
hostname:
|
||||
name: "{{ hostname }}"
|
||||
|
||||
- name: Ensure ufw is installed on the machine
|
||||
package:
|
||||
name: ufw
|
||||
state: present
|
||||
|
||||
- name: Allow TCP 22 for SSH.
|
||||
ufw:
|
||||
rule: allow
|
||||
port: 22
|
||||
port: "22"
|
||||
proto: tcp
|
||||
|
||||
- name: Allow TCP 80 for Gophish.
|
||||
ufw:
|
||||
rule: allow
|
||||
port: 80
|
||||
port: "80"
|
||||
proto: tcp
|
||||
|
||||
- name: Allow TCP 443 for Gophish.
|
||||
ufw:
|
||||
rule: allow
|
||||
port: 443
|
||||
port: "443"
|
||||
proto: tcp
|
||||
|
||||
- name: Enable ufw.
|
||||
|
@ -34,11 +39,55 @@
|
|||
apt:
|
||||
upgrade: safe
|
||||
|
||||
- name: Ensure /etc/ssl/csr folder exists
|
||||
file:
|
||||
path: /etc/ssl/csr
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure /etc/ssl/private folder exists
|
||||
file:
|
||||
path: /etc/ssl/private
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure /etc/ssl/crt folder exists
|
||||
file:
|
||||
path: /etc/ssl/crt
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Install specified packages.
|
||||
apt:
|
||||
pkg: "{{ item }}"
|
||||
pkg: "{{ install_packages }}"
|
||||
state: latest
|
||||
with_items: "{{ install_packages }}"
|
||||
|
||||
- name: adding existing user '{{ gophish_user }}' to group ssl-cert
|
||||
user:
|
||||
name: "{{ gophish_user }}"
|
||||
groups: ssl-cert
|
||||
append: yes
|
||||
|
||||
- name: Ensure the cryptography Python package is installed
|
||||
pip:
|
||||
name: cryptography
|
||||
|
||||
- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
|
||||
openssl_privatekey:
|
||||
path: "{{ gophish_ssl_cert_path }}"
|
||||
|
||||
- name: Generate an OpenSSL Certificate Signing Request
|
||||
openssl_csr:
|
||||
path: "{{ gophish_csr_path }}"
|
||||
privatekey_path: "{{ gophish_ssl_cert_path }}"
|
||||
common_name: "{{ gophish_domain }}"
|
||||
|
||||
- name: Generate a Self Signed OpenSSL certificate
|
||||
openssl_certificate:
|
||||
path: "{{ gophish_crt_path }}"
|
||||
privatekey_path: "{{ gophish_ssl_cert_path }}"
|
||||
csr_path: "{{ gophish_csr_path }}"
|
||||
provider: selfsigned
|
||||
|
||||
- name: Update postfix main.cf configuration file.
|
||||
template:
|
||||
|
@ -60,18 +109,38 @@
|
|||
state: started
|
||||
enabled: yes
|
||||
|
||||
- name: get latest release info
|
||||
uri:
|
||||
url: "https://api.github.com/repos/gophish/gophish/releases/latest"
|
||||
return_content: true
|
||||
register: latest_json_reponse
|
||||
|
||||
- name: Download latest Gophish .zip file.
|
||||
get_url:
|
||||
validate_certs: True
|
||||
url: https://getgophish.com/releases/latest/linux/64
|
||||
url: "https://github.com/gophish/gophish/releases/download/{{ latest_json_reponse.json.tag_name }}/gophish-{{ latest_json_reponse.json.tag_name }}-linux-64bit.zip"
|
||||
dest: "/home/{{ gophish_user }}/gophish.zip"
|
||||
mode: 0755
|
||||
owner: "{{ gophish_user }}"
|
||||
group: "{{ gophish_user }}"
|
||||
|
||||
- name: Ensure gophish user has permission for CRT file.
|
||||
file:
|
||||
path: "{{ gophish_crt_path }}"
|
||||
mode: 0755
|
||||
owner: "{{ gophish_user }}"
|
||||
group: "{{ gophish_user }}"
|
||||
|
||||
- name: Ensure gophish user has permission for SSL certificate.
|
||||
file:
|
||||
path: "{{ gophish_ssl_cert_path }}"
|
||||
mode: 0755
|
||||
owner: "{{ gophish_user }}"
|
||||
group: "{{ gophish_user }}"
|
||||
|
||||
- name: Create directory for gophish.
|
||||
file:
|
||||
path: "/home/{{ gophish_user }}/gophish"
|
||||
path: "/home/{{ gophish_user }}/gophish_deploy"
|
||||
state: directory
|
||||
mode: 0755
|
||||
owner: "{{ gophish_user }}"
|
||||
|
@ -80,29 +149,78 @@
|
|||
- name: Unzip gophish file.
|
||||
unarchive:
|
||||
src: "/home/{{ gophish_user }}/gophish.zip"
|
||||
dest: "/home/{{ gophish_user }}/gophish"
|
||||
remote_src: True # File is on target server and not locally.
|
||||
dest: "/home/{{ gophish_user }}/gophish_deploy"
|
||||
remote_src: True # File is on target server and not locally.
|
||||
owner: "{{ gophish_user }}"
|
||||
group: "{{ gophish_user }}"
|
||||
|
||||
- name: Change ownership of Gophish folder and files.
|
||||
file:
|
||||
path: /home/{{ gophish_user }}/gophish
|
||||
path: /home/{{ gophish_user }}/gophish_deploy
|
||||
owner: "{{ gophish_user }}"
|
||||
group: "{{ gophish_user }}"
|
||||
recurse: True
|
||||
|
||||
- name: Allow gophish binary to bind to privileged ports using setcap.
|
||||
shell: setcap CAP_NET_BIND_SERVICE=+eip /home/{{ gophish_user }}/gophish/gophish
|
||||
- name: Ensure gophish binary is executable
|
||||
file:
|
||||
path: /home/{{ gophish_user }}/gophish_deploy/gophish
|
||||
mode: 744
|
||||
|
||||
- name: Ensure gophish binary is allowed to bind to privileged ports using setcap
|
||||
capabilities:
|
||||
path: /home/{{ gophish_user }}/gophish_deploy/gophish
|
||||
capability: cap_net_bind_service+eip
|
||||
state: present
|
||||
|
||||
- name: Copy config.json file.
|
||||
copy:
|
||||
src: files/config.json
|
||||
dest: "/home/{{ gophish_user }}/gophish/config.json"
|
||||
dest: "/home/{{ gophish_user }}/gophish_deploy/config.json"
|
||||
owner: "{{ gophish_user }}"
|
||||
group: "{{ gophish_user }}"
|
||||
mode: 0644
|
||||
|
||||
- name: Ensure gophish service file is properly set
|
||||
template:
|
||||
src: gophish.service.j2
|
||||
dest: /etc/systemd/system/gophish.service
|
||||
mode: 644
|
||||
|
||||
- name: Ensure systemd to reread configs
|
||||
systemd:
|
||||
daemon_reload: yes
|
||||
|
||||
- name: Ensure gophish is properly started
|
||||
service:
|
||||
name: gophish.service
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
- name: Ensure nginx is installed
|
||||
package:
|
||||
name: nginx
|
||||
state: present
|
||||
|
||||
- name: Ensure nginx service file is properly set
|
||||
template:
|
||||
src: nginx.conf.j2
|
||||
dest: /etc/nginx/nginx.conf
|
||||
mode: 644
|
||||
|
||||
- name: Ensure nginx service is restarted
|
||||
service:
|
||||
name: nginx
|
||||
state: reloaded
|
||||
enabled: yes
|
||||
|
||||
- name: get Gophish log file which contain initial password
|
||||
command: cat /home/{{ gophish_user }}/gophish_deploy/gophish.log
|
||||
register: gophish_log
|
||||
|
||||
- name: display log file
|
||||
debug:
|
||||
msg: "{{ gophish_log }}"
|
||||
|
||||
- name: Reboot the box in 1 minute.
|
||||
command: shutdown -r 1
|
||||
when: reboot_box
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[Unit]
|
||||
Description=gophish
|
||||
After=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/{{ gophish_user }}/gophish_deploy/
|
||||
ExecStart="/home/{{ gophish_user }}/gophish_deploy/gophish"
|
||||
User={{ gophish_user }}
|
||||
PIDFile="/home/{{ gophish_user }}/gophish_deploy/gophish.pid"
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1,26 @@
|
|||
events {
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
server_name {{gophish_domain}};
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
ssl_certificate {{ gophish_crt_path }};
|
||||
ssl_certificate_key {{ gophish_ssl_cert_path }};
|
||||
server_name {{gophish_domain}};
|
||||
location / {
|
||||
proxy_pass https://127.0.0.1:8080;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,11 +3,17 @@ enable_ufw_firewall: true
|
|||
install_packages:
|
||||
- postfix
|
||||
- unzip
|
||||
- libcap2-bin
|
||||
- python-is-python3
|
||||
- python3-pip
|
||||
|
||||
hostname: gophish
|
||||
gophish_user: ubuntu
|
||||
postfix_hostname: gophish
|
||||
postfix_inet_interfaces: 127.0.0.1
|
||||
|
||||
gophish_domain: gophish.local
|
||||
gophish_ssl_cert_path: /etc/ssl/private/gophish.pem
|
||||
gophish_csr_path: /etc/ssl/csr/gophish.csr
|
||||
gophish_crt_path: /etc/ssl/crt/gophish.crt
|
||||
# Required if changing /etc/hostname to something different.
|
||||
reboot_box: true
|
||||
|
|
124
auth/auth.go
124
auth/auth.go
|
@ -1,69 +1,103 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"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.
|
||||
var ErrInvalidPassword = errors.New("Invalid Password")
|
||||
|
||||
// ErrPasswordMismatch is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrPasswordMismatch = errors.New("Password cannot be blank")
|
||||
// ErrPasswordMismatch is thrown when a user provides a mismatching password
|
||||
// and confirmation password.
|
||||
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
|
||||
// or change password functions
|
||||
var ErrEmptyPassword = errors.New("No password provided")
|
||||
|
||||
// Login attempts to login the user given a request.
|
||||
func Login(r *http.Request) (bool, models.User, error) {
|
||||
username, password := r.FormValue("username"), r.FormValue("password")
|
||||
u, err := models.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
return false, models.User{}, err
|
||||
}
|
||||
//If we've made it here, we should have a valid user stored in u
|
||||
//Let's check the password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(password))
|
||||
if err != nil {
|
||||
return false, models.User{}, ErrInvalidPassword
|
||||
}
|
||||
return true, u, nil
|
||||
// ErrPasswordTooShort is thrown when a user provides a password that is less
|
||||
// than MinPasswordLength
|
||||
var ErrPasswordTooShort = fmt.Errorf("Password must be at least %d characters", MinPasswordLength)
|
||||
|
||||
// GenerateSecureKey returns the hex representation of key generated from n
|
||||
// random bytes
|
||||
func GenerateSecureKey(n int) string {
|
||||
k := make([]byte, n)
|
||||
io.ReadFull(rand.Reader, k)
|
||||
return fmt.Sprintf("%x", k)
|
||||
}
|
||||
|
||||
// ChangePassword verifies the current password provided in the request and,
|
||||
// if it's valid, changes the password for the authenticated user.
|
||||
func ChangePassword(r *http.Request) error {
|
||||
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 := bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(currentPw))
|
||||
// GeneratePasswordHash returns the bcrypt hash for the provided password using
|
||||
// the default bcrypt cost.
|
||||
func GeneratePasswordHash(password string) (string, error) {
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return ErrInvalidPassword
|
||||
return "", err
|
||||
}
|
||||
// Check that the new password isn't blank
|
||||
if newPassword == "" {
|
||||
return string(h), nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// Check that new passwords match
|
||||
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
|
||||
case len(password) < MinPasswordLength:
|
||||
return ErrPasswordTooShort
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -3,7 +3,8 @@
|
|||
"listen_url": "127.0.0.1:3333",
|
||||
"use_tls": true,
|
||||
"cert_path": "gophish_admin.crt",
|
||||
"key_path": "gophish_admin.key"
|
||||
"key_path": "gophish_admin.key",
|
||||
"trusted_origins": []
|
||||
},
|
||||
"phish_server": {
|
||||
"listen_url": "0.0.0.0:80",
|
||||
|
@ -16,6 +17,7 @@
|
|||
"migrations_prefix": "db/db_",
|
||||
"contact_address": "",
|
||||
"logging": {
|
||||
"filename": ""
|
||||
"filename": "",
|
||||
"level": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,19 @@ package config
|
|||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
)
|
||||
|
||||
// AdminServer represents the Admin server configuration details
|
||||
type AdminServer struct {
|
||||
ListenURL string `json:"listen_url"`
|
||||
UseTLS bool `json:"use_tls"`
|
||||
CertPath string `json:"cert_path"`
|
||||
KeyPath string `json:"key_path"`
|
||||
ListenURL string `json:"listen_url"`
|
||||
UseTLS bool `json:"use_tls"`
|
||||
CertPath string `json:"cert_path"`
|
||||
KeyPath string `json:"key_path"`
|
||||
CSRFKey string `json:"csrf_key"`
|
||||
AllowedInternalHosts []string `json:"allowed_internal_hosts"`
|
||||
TrustedOrigins []string `json:"trusted_origins"`
|
||||
}
|
||||
|
||||
// PhishServer represents the Phish server configuration details
|
||||
|
@ -21,21 +26,17 @@ type PhishServer struct {
|
|||
KeyPath string `json:"key_path"`
|
||||
}
|
||||
|
||||
// LoggingConfig represents configuration details for Gophish logging.
|
||||
type LoggingConfig struct {
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
|
||||
// Config represents the configuration information.
|
||||
type Config struct {
|
||||
AdminConf AdminServer `json:"admin_server"`
|
||||
PhishConf PhishServer `json:"phish_server"`
|
||||
DBName string `json:"db_name"`
|
||||
DBPath string `json:"db_path"`
|
||||
MigrationsPath string `json:"migrations_prefix"`
|
||||
TestFlag bool `json:"test_flag"`
|
||||
ContactAddress string `json:"contact_address"`
|
||||
Logging LoggingConfig `json:"logging"`
|
||||
AdminConf AdminServer `json:"admin_server"`
|
||||
PhishConf PhishServer `json:"phish_server"`
|
||||
DBName string `json:"db_name"`
|
||||
DBPath string `json:"db_path"`
|
||||
DBSSLCaPath string `json:"db_sslca_path"`
|
||||
MigrationsPath string `json:"migrations_prefix"`
|
||||
TestFlag bool `json:"test_flag"`
|
||||
ContactAddress string `json:"contact_address"`
|
||||
Logging *log.Config `json:"logging"`
|
||||
}
|
||||
|
||||
// Version contains the current gophish version
|
||||
|
@ -56,6 +57,9 @@ func LoadConfig(filepath string) (*Config, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config.Logging == nil {
|
||||
config.Logging = &log.Config{}
|
||||
}
|
||||
// Choosing the migrations directory based on the database used.
|
||||
config.MigrationsPath = config.MigrationsPath + config.DBName
|
||||
// Explicitly set the TestFlag to false to prevent config.json overrides
|
||||
|
|
|
@ -4,16 +4,12 @@ import (
|
|||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
)
|
||||
|
||||
type ConfigSuite struct {
|
||||
suite.Suite
|
||||
ConfigFile *os.File
|
||||
}
|
||||
|
||||
var validConfig = []byte(`{
|
||||
"admin_server": {
|
||||
"listen_url": "127.0.0.1:3333",
|
||||
|
@ -33,36 +29,50 @@ var validConfig = []byte(`{
|
|||
"contact_address": ""
|
||||
}`)
|
||||
|
||||
func (s *ConfigSuite) SetupTest() {
|
||||
func createTemporaryConfig(t *testing.T) *os.File {
|
||||
f, err := ioutil.TempFile("", "gophish-config")
|
||||
s.Nil(err)
|
||||
s.ConfigFile = f
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create temporary config: %v", err)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func (s *ConfigSuite) TearDownTest() {
|
||||
err := s.ConfigFile.Close()
|
||||
s.Nil(err)
|
||||
func removeTemporaryConfig(t *testing.T, f *os.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to remove temporary config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigSuite) TestLoadConfig() {
|
||||
_, err := s.ConfigFile.Write(validConfig)
|
||||
s.Nil(err)
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
f := createTemporaryConfig(t)
|
||||
defer removeTemporaryConfig(t, f)
|
||||
_, err := f.Write(validConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("error writing config to temporary file: %v", err)
|
||||
}
|
||||
// Load the valid config
|
||||
conf, err := LoadConfig(s.ConfigFile.Name())
|
||||
s.Nil(err)
|
||||
conf, err := LoadConfig(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("error loading config from temporary file: %v", err)
|
||||
}
|
||||
|
||||
expectedConfig := &Config{}
|
||||
err = json.Unmarshal(validConfig, &expectedConfig)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error unmarshaling config: %v", err)
|
||||
}
|
||||
expectedConfig.MigrationsPath = expectedConfig.MigrationsPath + expectedConfig.DBName
|
||||
expectedConfig.TestFlag = false
|
||||
s.Equal(expectedConfig, conf)
|
||||
expectedConfig.AdminConf.CSRFKey = ""
|
||||
expectedConfig.Logging = &log.Config{}
|
||||
if !reflect.DeepEqual(expectedConfig, conf) {
|
||||
t.Fatalf("invalid config received. expected %#v got %#v", expectedConfig, conf)
|
||||
}
|
||||
|
||||
// Load an invalid config
|
||||
conf, err = LoadConfig("bogusfile")
|
||||
s.NotNil(err)
|
||||
}
|
||||
|
||||
func TestConfigSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConfigSuite))
|
||||
_, err = LoadConfig("bogusfile")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when loading invalid config, but got %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !go1.7
|
||||
// +build !go1.7
|
||||
|
||||
package context
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//go:build go1.7
|
||||
// +build go1.7
|
||||
|
||||
package context
|
||||
|
@ -23,6 +24,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
|
||||
func Clear(r *http.Request) {
|
||||
return
|
||||
}
|
||||
func Clear(r *http.Request) {}
|
||||
|
|
|
@ -6,23 +6,20 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type APISuite struct {
|
||||
suite.Suite
|
||||
type testContext struct {
|
||||
apiKey string
|
||||
config *config.Config
|
||||
apiServer *Server
|
||||
admin models.User
|
||||
}
|
||||
|
||||
func (s *APISuite) SetupSuite() {
|
||||
func setupTest(t *testing.T) *testContext {
|
||||
conf := &config.Config{
|
||||
DBName: "sqlite3",
|
||||
DBPath: ":memory:",
|
||||
|
@ -30,39 +27,22 @@ func (s *APISuite) SetupSuite() {
|
|||
}
|
||||
err := models.Setup(conf)
|
||||
if err != nil {
|
||||
s.T().Fatalf("Failed creating database: %v", err)
|
||||
t.Fatalf("Failed creating database: %v", err)
|
||||
}
|
||||
s.config = conf
|
||||
s.Nil(err)
|
||||
ctx := &testContext{}
|
||||
ctx.config = conf
|
||||
// Get the API key to use for these tests
|
||||
u, err := models.GetUser(1)
|
||||
s.Nil(err)
|
||||
s.apiKey = u.ApiKey
|
||||
s.admin = u
|
||||
// Move our cwd up to the project root for help with resolving
|
||||
// static assets
|
||||
err = os.Chdir("../")
|
||||
s.Nil(err)
|
||||
s.apiServer = NewServer()
|
||||
if err != nil {
|
||||
t.Fatalf("error getting admin user: %v", err)
|
||||
}
|
||||
ctx.apiKey = u.ApiKey
|
||||
ctx.admin = u
|
||||
ctx.apiServer = NewServer()
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (s *APISuite) TearDownTest() {
|
||||
campaigns, _ := models.GetCampaigns(1)
|
||||
for _, campaign := range campaigns {
|
||||
models.DeleteCampaign(campaign.Id)
|
||||
}
|
||||
// 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 (s *APISuite) SetupTest() {
|
||||
func createTestData(t *testing.T) {
|
||||
// Add a group
|
||||
group := models.Group{Name: "Test Group"}
|
||||
group.Targets = []models.Target{
|
||||
|
@ -73,12 +53,12 @@ func (s *APISuite) SetupTest() {
|
|||
models.PostGroup(&group)
|
||||
|
||||
// Add a template
|
||||
t := models.Template{Name: "Test Template"}
|
||||
t.Subject = "Test subject"
|
||||
t.Text = "Text text"
|
||||
t.HTML = "<html>Test</html>"
|
||||
t.UserId = 1
|
||||
models.PostTemplate(&t)
|
||||
template := models.Template{Name: "Test Template"}
|
||||
template.Subject = "Test subject"
|
||||
template.Text = "Text text"
|
||||
template.HTML = "<html>Test</html>"
|
||||
template.UserId = 1
|
||||
models.PostTemplate(&template)
|
||||
|
||||
// Add a landing page
|
||||
p := models.Page{Name: "Test Page"}
|
||||
|
@ -97,7 +77,7 @@ func (s *APISuite) SetupTest() {
|
|||
// Set the status such that no emails are attempted
|
||||
c := models.Campaign{Name: "Test campaign"}
|
||||
c.UserId = 1
|
||||
c.Template = t
|
||||
c.Template = template
|
||||
c.Page = p
|
||||
c.SMTP = smtp
|
||||
c.Groups = []models.Group{group}
|
||||
|
@ -105,12 +85,13 @@ func (s *APISuite) SetupTest() {
|
|||
c.UpdateStatus(models.CampaignEmailsSent)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestSiteImportBaseHref() {
|
||||
func TestSiteImportBaseHref(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
h := "<html><head></head><body><img src=\"/test.png\"/></body></html>"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, h)
|
||||
}))
|
||||
hr := fmt.Sprintf("<html><head><base href=\"%s\"/></head><body><img src=\"/test.png\"/>\n</body></html>", ts.URL)
|
||||
expected := fmt.Sprintf("<html><head><base href=\"%s\"/></head><body><img src=\"/test.png\"/>\n</body></html>", ts.URL)
|
||||
defer ts.Close()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/import/site",
|
||||
bytes.NewBuffer([]byte(fmt.Sprintf(`
|
||||
|
@ -121,13 +102,13 @@ func (s *APISuite) TestSiteImportBaseHref() {
|
|||
`, ts.URL))))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
s.apiServer.ImportSite(response, req)
|
||||
ctx.apiServer.ImportSite(response, req)
|
||||
cs := cloneResponse{}
|
||||
err := json.NewDecoder(response.Body).Decode(&cs)
|
||||
s.Nil(err)
|
||||
s.Equal(cs.HTML, hr)
|
||||
}
|
||||
|
||||
func TestAPISuite(t *testing.T) {
|
||||
suite.Run(t, new(APISuite))
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding response: %v", err)
|
||||
}
|
||||
if cs.HTML != expected {
|
||||
t.Fatalf("unexpected response received. expected %s got %s", expected, cs.HTML)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
g = models.Group{}
|
||||
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 {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/imap"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
// IMAPServerValidate handles requests for the /api/imapserver/validate endpoint
|
||||
func (as *Server) IMAPServerValidate(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Only POSTs allowed"}, http.StatusBadRequest)
|
||||
case r.Method == "POST":
|
||||
im := models.IMAP{}
|
||||
err := json.NewDecoder(r.Body).Decode(&im)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = imap.Validate(&im)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusOK)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Successful login."}, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// IMAPServer handles requests for the /api/imapserver/ endpoint
|
||||
func (as *Server) IMAPServer(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ss, err := models.GetIMAP(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, ss, http.StatusOK)
|
||||
|
||||
// POST: Update database
|
||||
case r.Method == "POST":
|
||||
im := models.IMAP{}
|
||||
err := json.NewDecoder(r.Body).Decode(&im)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid data. Please check your IMAP settings."}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
im.ModifiedDate = time.Now().UTC()
|
||||
im.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostIMAP(&im, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Successfully saved IMAP settings."}, http.StatusCreated)
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gophish/gophish/dialer"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
|
@ -46,7 +47,6 @@ func (as *Server) ImportGroup(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// ImportEmail allows for the importing of email.
|
||||
|
@ -94,7 +94,6 @@ func (as *Server) ImportEmail(w http.ResponseWriter, r *http.Request) {
|
|||
HTML: string(e.HTML),
|
||||
}
|
||||
JSONResponse(w, er, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// ImportSite allows for the importing of HTML from a website
|
||||
|
@ -115,7 +114,9 @@ func (as *Server) ImportSite(w http.ResponseWriter, r *http.Request) {
|
|||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
restrictedDialer := dialer.Dialer()
|
||||
tr := &http.Transport{
|
||||
DialContext: restrictedDialer.DialContext,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
|
@ -153,5 +154,4 @@ func (as *Server) ImportSite(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
cs := cloneResponse{HTML: h}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gophish/gophish/dialer"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
func makeImportRequest(ctx *testContext, allowedHosts []string, url string) *httptest.ResponseRecorder {
|
||||
orig := dialer.DefaultDialer.AllowedHosts()
|
||||
dialer.SetAllowedHosts(allowedHosts)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/import/site",
|
||||
bytes.NewBuffer([]byte(fmt.Sprintf(`
|
||||
{
|
||||
"url" : "%s"
|
||||
}
|
||||
`, url))))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
ctx.apiServer.ImportSite(response, req)
|
||||
dialer.SetAllowedHosts(orig)
|
||||
return response
|
||||
}
|
||||
|
||||
func TestDefaultDeniedImport(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
metadataURL := "http://169.254.169.254/latest/meta-data/"
|
||||
response := makeImportRequest(ctx, []string{}, metadataURL)
|
||||
expectedCode := http.StatusBadRequest
|
||||
if response.Code != expectedCode {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expectedCode, response.Code)
|
||||
}
|
||||
got := &models.Response{}
|
||||
err := json.NewDecoder(response.Body).Decode(got)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding body: %v", err)
|
||||
}
|
||||
if !strings.Contains(got.Message, "upstream connection denied") {
|
||||
t.Fatalf("incorrect response error provided: %s", got.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAllowedImport(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
h := "<html><head></head><body><img src=\"/test.png\"/></body></html>"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, h)
|
||||
}))
|
||||
defer ts.Close()
|
||||
response := makeImportRequest(ctx, []string{}, ts.URL)
|
||||
expectedCode := http.StatusOK
|
||||
if response.Code != expectedCode {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expectedCode, response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomDeniedImport(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
h := "<html><head></head><body><img src=\"/test.png\"/></body></html>"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, h)
|
||||
}))
|
||||
defer ts.Close()
|
||||
response := makeImportRequest(ctx, []string{"192.168.1.1"}, ts.URL)
|
||||
expectedCode := http.StatusBadRequest
|
||||
if response.Code != expectedCode {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expectedCode, response.Code)
|
||||
}
|
||||
got := &models.Response{}
|
||||
err := json.NewDecoder(response.Body).Decode(got)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding body: %v", err)
|
||||
}
|
||||
if !strings.Contains(got.Message, "upstream connection denied") {
|
||||
t.Fatalf("incorrect response error provided: %s", got.Message)
|
||||
}
|
||||
}
|
|
@ -3,9 +3,9 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
case r.Method == "POST":
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
u.ApiKey = util.GenerateSecureKey()
|
||||
u.ApiKey = auth.GenerateSecureKey(auth.APIKeyLength)
|
||||
err := models.PutUser(&u)
|
||||
if err != nil {
|
||||
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/middleware/ratelimit"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/worker"
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -19,14 +20,17 @@ type ServerOption func(*Server)
|
|||
type Server struct {
|
||||
handler http.Handler
|
||||
worker worker.Worker
|
||||
limiter *ratelimit.PostLimiter
|
||||
}
|
||||
|
||||
// NewServer returns a new instance of the API handler with the provided
|
||||
// options applied.
|
||||
func NewServer(options ...ServerOption) *Server {
|
||||
defaultWorker, _ := worker.New()
|
||||
defaultLimiter := ratelimit.NewPostLimiter()
|
||||
as := &Server{
|
||||
worker: defaultWorker,
|
||||
worker: defaultWorker,
|
||||
limiter: defaultLimiter,
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(as)
|
||||
|
@ -42,12 +46,20 @@ func WithWorker(w worker.Worker) ServerOption {
|
|||
}
|
||||
}
|
||||
|
||||
func WithLimiter(limiter *ratelimit.PostLimiter) ServerOption {
|
||||
return func(as *Server) {
|
||||
as.limiter = limiter
|
||||
}
|
||||
}
|
||||
|
||||
func (as *Server) registerRoutes() {
|
||||
root := mux.NewRouter()
|
||||
root = root.StrictSlash(true)
|
||||
router := root.PathPrefix("/api/").Subrouter()
|
||||
router.Use(mid.RequireAPIKey)
|
||||
router.Use(mid.EnforceViewOnly)
|
||||
router.HandleFunc("/imap/", as.IMAPServer)
|
||||
router.HandleFunc("/imap/validate", as.IMAPServerValidate)
|
||||
router.HandleFunc("/reset", as.Reset)
|
||||
router.HandleFunc("/campaigns/", as.Campaigns)
|
||||
router.HandleFunc("/campaigns/summary", as.CampaignsSummary)
|
||||
|
@ -71,6 +83,9 @@ func (as *Server) registerRoutes() {
|
|||
router.HandleFunc("/import/group", as.ImportGroup)
|
||||
router.HandleFunc("/import/email", as.ImportEmail)
|
||||
router.HandleFunc("/import/site", as.ImportSite)
|
||||
router.HandleFunc("/webhooks/", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
router.HandleFunc("/webhooks/{id:[0-9]+}/validate", mid.Use(as.ValidateWebhook, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
router.HandleFunc("/webhooks/{id:[0-9]+}", mid.Use(as.Webhook, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
as.handler = router
|
||||
}
|
||||
|
||||
|
|
|
@ -6,18 +6,14 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gorilla/mux"
|
||||
"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.
|
||||
var ErrUsernameTaken = errors.New("Username already taken")
|
||||
|
||||
|
@ -33,9 +29,11 @@ var ErrInsufficientPermission = errors.New("Permission denied")
|
|||
|
||||
// userRequest is the payload which represents the creation of a new user.
|
||||
type userRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
PasswordChangeRequired bool `json:"password_change_required"`
|
||||
AccountLocked bool `json:"account_locked"`
|
||||
}
|
||||
|
||||
func (ur *userRequest) Validate(existingUser *models.User) error {
|
||||
|
@ -89,11 +87,12 @@ func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
|
|||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if ur.Password == "" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: ErrEmptyPassword.Error()}, http.StatusBadRequest)
|
||||
err = auth.CheckPasswordPolicy(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hash, err := util.NewHash(ur.Password)
|
||||
hash, err := auth.GeneratePasswordHash(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -104,11 +103,12 @@ func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
user := models.User{
|
||||
Username: ur.Username,
|
||||
Hash: hash,
|
||||
ApiKey: util.GenerateSecureKey(),
|
||||
Role: role,
|
||||
RoleID: role.ID,
|
||||
Username: ur.Username,
|
||||
Hash: hash,
|
||||
ApiKey: auth.GenerateSecureKey(auth.APIKeyLength),
|
||||
Role: role,
|
||||
RoleID: role.ID,
|
||||
PasswordChangeRequired: ur.PasswordChangeRequired,
|
||||
}
|
||||
err = models.PutUser(&user)
|
||||
if err != nil {
|
||||
|
@ -195,19 +195,27 @@ 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
|
||||
// managing the user's account, and making a simple change like
|
||||
// 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
|
||||
// assumption here is that the API key is a proper bearer token proving
|
||||
// authenticated access to the account.
|
||||
existingUser.PasswordChangeRequired = ur.PasswordChangeRequired
|
||||
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 {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
existingUser.Hash = hash
|
||||
}
|
||||
existingUser.AccountLocked = ur.AccountLocked
|
||||
err = models.PutUser(&existingUser)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
|
@ -13,9 +14,11 @@ import (
|
|||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
func (s *APISuite) createUnpriviledgedUser(slug string) *models.User {
|
||||
func createUnpriviledgedUser(t *testing.T, slug string) *models.User {
|
||||
role, err := models.GetRoleBySlug(slug)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting role by slug: %v", err)
|
||||
}
|
||||
unauthorizedUser := &models.User{
|
||||
Username: "foo",
|
||||
Hash: "bar",
|
||||
|
@ -24,56 +27,82 @@ func (s *APISuite) createUnpriviledgedUser(slug string) *models.User {
|
|||
RoleID: role.ID,
|
||||
}
|
||||
err = models.PutUser(unauthorizedUser)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error saving unpriviledged user: %v", err)
|
||||
}
|
||||
return unauthorizedUser
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetUsers() {
|
||||
func TestGetUsers(t *testing.T) {
|
||||
testCtx := setupTest(t)
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/users", nil)
|
||||
r = ctx.Set(r, "user", s.admin)
|
||||
r = ctx.Set(r, "user", testCtx.admin)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.Users(w, r)
|
||||
s.Equal(w.Code, http.StatusOK)
|
||||
testCtx.apiServer.Users(w, r)
|
||||
expected := http.StatusOK
|
||||
if w.Code != expected {
|
||||
t.Fatalf("unexpected error code received. expected %d got %d", expected, w.Code)
|
||||
}
|
||||
|
||||
got := []models.User{}
|
||||
err := json.NewDecoder(w.Body).Decode(&got)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding users data: %v", err)
|
||||
}
|
||||
|
||||
// We only expect one user
|
||||
s.Equal(1, len(got))
|
||||
expectedUsers := 1
|
||||
if len(got) != expectedUsers {
|
||||
t.Fatalf("unexpected number of users returned. expected %d got %d", expectedUsers, len(got))
|
||||
}
|
||||
// And it should be the admin user
|
||||
s.Equal(s.admin.Id, got[0].Id)
|
||||
if testCtx.admin.Id != got[0].Id {
|
||||
t.Fatalf("unexpected user received. expected %d got %d", testCtx.admin.Id, got[0].Id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APISuite) TestCreateUser() {
|
||||
func TestCreateUser(t *testing.T) {
|
||||
testCtx := setupTest(t)
|
||||
payload := &userRequest{
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
Password: "validpassword",
|
||||
Role: models.RoleUser,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshaling userRequest payload: %v", err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest(http.MethodPost, "/api/users", bytes.NewBuffer(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
r = ctx.Set(r, "user", s.admin)
|
||||
r = ctx.Set(r, "user", testCtx.admin)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.Users(w, r)
|
||||
s.Equal(w.Code, http.StatusOK)
|
||||
testCtx.apiServer.Users(w, r)
|
||||
expected := http.StatusOK
|
||||
if w.Code != expected {
|
||||
t.Fatalf("unexpected error code received. expected %d got %d", expected, w.Code)
|
||||
}
|
||||
|
||||
got := &models.User{}
|
||||
err = json.NewDecoder(w.Body).Decode(got)
|
||||
s.Nil(err)
|
||||
s.Equal(got.Username, payload.Username)
|
||||
s.Equal(got.Role.Slug, payload.Role)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding user payload: %v", err)
|
||||
}
|
||||
if got.Username != payload.Username {
|
||||
t.Fatalf("unexpected username received. expected %s got %s", payload.Username, got.Username)
|
||||
}
|
||||
if got.Role.Slug != payload.Role {
|
||||
t.Fatalf("unexpected role received. expected %s got %s", payload.Role, got.Role.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModifyUser tests that a user with the appropriate access is able to
|
||||
// modify their username and password.
|
||||
func (s *APISuite) TestModifyUser() {
|
||||
unpriviledgedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
func TestModifyUser(t *testing.T) {
|
||||
testCtx := setupTest(t)
|
||||
unpriviledgedUser := createUnpriviledgedUser(t, models.RoleUser)
|
||||
newPassword := "new-password"
|
||||
newUsername := "new-username"
|
||||
payload := userRequest{
|
||||
|
@ -82,33 +111,48 @@ func (s *APISuite) TestModifyUser() {
|
|||
Role: unpriviledgedUser.Role.Slug,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshaling userRequest payload: %v", err)
|
||||
}
|
||||
url := fmt.Sprintf("/api/users/%d", unpriviledgedUser.Id)
|
||||
r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unpriviledgedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
testCtx.apiServer.ServeHTTP(w, r)
|
||||
response := &models.User{}
|
||||
err = json.NewDecoder(w.Body).Decode(response)
|
||||
s.Nil(err)
|
||||
s.Equal(w.Code, http.StatusOK)
|
||||
s.Equal(response.Username, newUsername)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding user payload: %v", err)
|
||||
}
|
||||
expected := http.StatusOK
|
||||
if w.Code != expected {
|
||||
t.Fatalf("unexpected error code received. expected %d got %d", expected, w.Code)
|
||||
}
|
||||
if response.Username != newUsername {
|
||||
t.Fatalf("unexpected username received. expected %s got %s", newUsername, response.Username)
|
||||
}
|
||||
got, err := models.GetUser(unpriviledgedUser.Id)
|
||||
s.Nil(err)
|
||||
s.Equal(response.Username, got.Username)
|
||||
s.Equal(newUsername, got.Username)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting unpriviledged user: %v", err)
|
||||
}
|
||||
if response.Username != got.Username {
|
||||
t.Fatalf("unexpected username received. expected %s got %s", response.Username, got.Username)
|
||||
}
|
||||
err = bcrypt.CompareHashAndPassword([]byte(got.Hash), []byte(newPassword))
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("incorrect hash received for created user. expected %s got %s", []byte(newPassword), []byte(got.Hash))
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnauthorizedListUsers ensures that users without the ModifySystem
|
||||
// permission are unable to list the users registered in Gophish.
|
||||
func (s *APISuite) TestUnauthorizedListUsers() {
|
||||
func TestUnauthorizedListUsers(t *testing.T) {
|
||||
testCtx := setupTest(t)
|
||||
// First, let's create a standard user which doesn't
|
||||
// have ModifySystem permissions.
|
||||
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
unauthorizedUser := createUnpriviledgedUser(t, models.RoleUser)
|
||||
// We'll try to make a request to the various users API endpoints to
|
||||
// ensure that they fail. Previously, we could hit the handlers directly
|
||||
// but we need to go through the router for this test to ensure the
|
||||
|
@ -117,72 +161,99 @@ func (s *APISuite) TestUnauthorizedListUsers() {
|
|||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
s.Equal(w.Code, http.StatusForbidden)
|
||||
testCtx.apiServer.ServeHTTP(w, r)
|
||||
expected := http.StatusForbidden
|
||||
if w.Code != expected {
|
||||
t.Fatalf("unexpected error code received. expected %d got %d", expected, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnauthorizedModifyUsers verifies that users without ModifySystem
|
||||
// permission (a "standard" user) can only get or modify their own information.
|
||||
func (s *APISuite) TestUnauthorizedGetUser() {
|
||||
func TestUnauthorizedGetUser(t *testing.T) {
|
||||
testCtx := setupTest(t)
|
||||
// First, we'll make sure that a user with the "user" role is unable to
|
||||
// get the information of another user (in this case, the main admin).
|
||||
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
url := fmt.Sprintf("/api/users/%d", s.admin.Id)
|
||||
unauthorizedUser := createUnpriviledgedUser(t, models.RoleUser)
|
||||
url := fmt.Sprintf("/api/users/%d", testCtx.admin.Id)
|
||||
r := httptest.NewRequest(http.MethodGet, url, nil)
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
s.Equal(w.Code, http.StatusForbidden)
|
||||
testCtx.apiServer.ServeHTTP(w, r)
|
||||
expected := http.StatusForbidden
|
||||
if w.Code != expected {
|
||||
t.Fatalf("unexpected error code received. expected %d got %d", expected, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnauthorizedModifyRole ensures that users without the ModifySystem
|
||||
// privilege are unable to modify their own role, preventing a potential
|
||||
// privilege escalation issue.
|
||||
func (s *APISuite) TestUnauthorizedSetRole() {
|
||||
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
func TestUnauthorizedSetRole(t *testing.T) {
|
||||
testCtx := setupTest(t)
|
||||
unauthorizedUser := createUnpriviledgedUser(t, models.RoleUser)
|
||||
url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
|
||||
payload := &userRequest{
|
||||
Username: unauthorizedUser.Username,
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshaling userRequest payload: %v", err)
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
s.Equal(w.Code, http.StatusBadRequest)
|
||||
testCtx.apiServer.ServeHTTP(w, r)
|
||||
expected := http.StatusBadRequest
|
||||
if w.Code != expected {
|
||||
t.Fatalf("unexpected error code received. expected %d got %d", expected, w.Code)
|
||||
}
|
||||
response := &models.Response{}
|
||||
err = json.NewDecoder(w.Body).Decode(response)
|
||||
s.Nil(err)
|
||||
s.Equal(response.Message, ErrInsufficientPermission.Error())
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding response payload: %v", err)
|
||||
}
|
||||
if response.Message != ErrInsufficientPermission.Error() {
|
||||
t.Fatalf("incorrect error received when setting role. expected %s got %s", ErrInsufficientPermission.Error(), response.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModifyWithExistingUsername verifies that it's not possible to modify
|
||||
// an user's username to one which already exists.
|
||||
func (s *APISuite) TestModifyWithExistingUsername() {
|
||||
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
func TestModifyWithExistingUsername(t *testing.T) {
|
||||
testCtx := setupTest(t)
|
||||
unauthorizedUser := createUnpriviledgedUser(t, models.RoleUser)
|
||||
payload := &userRequest{
|
||||
Username: s.admin.Username,
|
||||
Username: testCtx.admin.Username,
|
||||
Role: unauthorizedUser.Role.Slug,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshaling userRequest payload: %v", err)
|
||||
}
|
||||
url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
|
||||
r := httptest.NewRequest(http.MethodPut, url, bytes.NewReader(body))
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
s.Equal(w.Code, http.StatusBadRequest)
|
||||
expected := &models.Response{
|
||||
testCtx.apiServer.ServeHTTP(w, r)
|
||||
expected := http.StatusBadRequest
|
||||
if w.Code != expected {
|
||||
t.Fatalf("unexpected error code received. expected %d got %d", expected, w.Code)
|
||||
}
|
||||
expectedResponse := &models.Response{
|
||||
Message: ErrUsernameTaken.Error(),
|
||||
Success: false,
|
||||
}
|
||||
got := &models.Response{}
|
||||
err = json.NewDecoder(w.Body).Decode(got)
|
||||
s.Nil(err)
|
||||
s.Equal(got.Message, expected.Message)
|
||||
if err != nil {
|
||||
t.Fatalf("error decoding response payload: %v", err)
|
||||
}
|
||||
if got.Message != expectedResponse.Message {
|
||||
t.Fatalf("incorrect error received when setting role. expected %s got %s", expectedResponse.Message, got.Message)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
|
@ -93,7 +94,19 @@ func (as *Server) SendTestEmail(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
s.SMTP = smtp
|
||||
}
|
||||
s.FromAddress = s.SMTP.FromAddress
|
||||
|
||||
_, err = mail.ParseAddress(s.Template.EnvelopeSender)
|
||||
if err != nil {
|
||||
_, err = mail.ParseAddress(s.SMTP.FromAddress)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
} else {
|
||||
s.FromAddress = s.SMTP.FromAddress
|
||||
}
|
||||
} else {
|
||||
s.FromAddress = s.Template.EnvelopeSender
|
||||
}
|
||||
|
||||
// Validate the given request
|
||||
if err = s.Validate(); err != nil {
|
||||
|
@ -118,5 +131,4 @@ func (as *Server) SendTestEmail(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/webhook"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Webhooks returns a list of webhooks, both active and disabled
|
||||
func (as *Server) Webhooks(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
whs, err := models.GetWebhooks()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, whs, http.StatusOK)
|
||||
|
||||
case r.Method == "POST":
|
||||
wh := models.Webhook{}
|
||||
err := json.NewDecoder(r.Body).Decode(&wh)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = models.PostWebhook(&wh)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, wh, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook returns details of a single webhook specified by "id" parameter
|
||||
func (as *Server) Webhook(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
wh, err := models.GetWebhook(id)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Webhook not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, wh, http.StatusOK)
|
||||
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteWebhook(id)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Infof("Deleted webhook with id: %d", id)
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Webhook deleted Successfully!"}, http.StatusOK)
|
||||
|
||||
case r.Method == "PUT":
|
||||
wh = models.Webhook{}
|
||||
err = json.NewDecoder(r.Body).Decode(&wh)
|
||||
if err != nil {
|
||||
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 {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, wh, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateWebhook makes an HTTP request to a specified remote url to ensure that it's valid.
|
||||
func (as *Server) ValidateWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
type validationEvent struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
switch {
|
||||
case r.Method == "POST":
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
wh, err := models.GetWebhook(id)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload := validationEvent{Success: true}
|
||||
err = webhook.Send(webhook.EndPoint{URL: wh.URL, Secret: wh.Secret}, payload)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, wh, http.StatusOK)
|
||||
}
|
||||
}
|
|
@ -1,62 +1,88 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// ControllersSuite is a suite of tests to cover API related functions
|
||||
type ControllersSuite struct {
|
||||
suite.Suite
|
||||
// testContext is the data required to test API related functions
|
||||
type testContext struct {
|
||||
apiKey string
|
||||
config *config.Config
|
||||
adminServer *httptest.Server
|
||||
phishServer *httptest.Server
|
||||
origPath string
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) SetupSuite() {
|
||||
func setupTest(t *testing.T) *testContext {
|
||||
wd, _ := os.Getwd()
|
||||
fmt.Println(wd)
|
||||
conf := &config.Config{
|
||||
DBName: "sqlite3",
|
||||
DBPath: ":memory:",
|
||||
MigrationsPath: "../db/db_sqlite3/migrations/",
|
||||
}
|
||||
abs, _ := filepath.Abs("../db/db_sqlite3/migrations/")
|
||||
fmt.Printf("in controllers_test.go: %s\n", abs)
|
||||
err := models.Setup(conf)
|
||||
if err != nil {
|
||||
s.T().Fatalf("Failed creating database: %v", err)
|
||||
t.Fatalf("error setting up database: %v", err)
|
||||
}
|
||||
s.config = conf
|
||||
s.Nil(err)
|
||||
// Setup the admin server for use in testing
|
||||
s.adminServer = httptest.NewUnstartedServer(NewAdminServer(s.config.AdminConf).server.Handler)
|
||||
s.adminServer.Config.Addr = s.config.AdminConf.ListenURL
|
||||
s.adminServer.Start()
|
||||
ctx := &testContext{}
|
||||
ctx.config = conf
|
||||
ctx.adminServer = httptest.NewUnstartedServer(NewAdminServer(ctx.config.AdminConf).server.Handler)
|
||||
ctx.adminServer.Config.Addr = ctx.config.AdminConf.ListenURL
|
||||
ctx.adminServer.Start()
|
||||
// Get the API key to use for these tests
|
||||
u, err := models.GetUser(1)
|
||||
s.Nil(err)
|
||||
s.apiKey = u.ApiKey
|
||||
// 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 {
|
||||
t.Fatalf("error getting first user from database: %v", err)
|
||||
}
|
||||
|
||||
// Create a second user to test account locked status
|
||||
u2 := models.User{Username: "houdini", Hash: hash, AccountLocked: true}
|
||||
models.PutUser(&u2)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating new user: %v", err)
|
||||
}
|
||||
|
||||
ctx.apiKey = u.ApiKey
|
||||
// Start the phishing server
|
||||
s.phishServer = httptest.NewUnstartedServer(NewPhishingServer(s.config.PhishConf).server.Handler)
|
||||
s.phishServer.Config.Addr = s.config.PhishConf.ListenURL
|
||||
s.phishServer.Start()
|
||||
ctx.phishServer = httptest.NewUnstartedServer(NewPhishingServer(ctx.config.PhishConf).server.Handler)
|
||||
ctx.phishServer.Config.Addr = ctx.config.PhishConf.ListenURL
|
||||
ctx.phishServer.Start()
|
||||
// Move our cwd up to the project root for help with resolving
|
||||
// static assets
|
||||
origPath, _ := os.Getwd()
|
||||
ctx.origPath = origPath
|
||||
err = os.Chdir("../")
|
||||
s.Nil(err)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TearDownTest() {
|
||||
campaigns, _ := models.GetCampaigns(1)
|
||||
for _, campaign := range campaigns {
|
||||
models.DeleteCampaign(campaign.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("error changing directories to setup asset discovery: %v", err)
|
||||
}
|
||||
createTestData(t)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) SetupTest() {
|
||||
func tearDown(t *testing.T, ctx *testContext) {
|
||||
// Tear down the admin and phishing servers
|
||||
ctx.adminServer.Close()
|
||||
ctx.phishServer.Close()
|
||||
// Reset the path for the next test
|
||||
os.Chdir(ctx.origPath)
|
||||
}
|
||||
|
||||
func createTestData(t *testing.T) {
|
||||
// Add a group
|
||||
group := models.Group{Name: "Test Group"}
|
||||
group.Targets = []models.Target{
|
||||
|
@ -67,12 +93,12 @@ func (s *ControllersSuite) SetupTest() {
|
|||
models.PostGroup(&group)
|
||||
|
||||
// Add a template
|
||||
t := models.Template{Name: "Test Template"}
|
||||
t.Subject = "Test subject"
|
||||
t.Text = "Text text"
|
||||
t.HTML = "<html>Test</html>"
|
||||
t.UserId = 1
|
||||
models.PostTemplate(&t)
|
||||
template := models.Template{Name: "Test Template"}
|
||||
template.Subject = "Test subject"
|
||||
template.Text = "Text text"
|
||||
template.HTML = "<html>Test</html>"
|
||||
template.UserId = 1
|
||||
models.PostTemplate(&template)
|
||||
|
||||
// Add a landing page
|
||||
p := models.Page{Name: "Test Page"}
|
||||
|
@ -91,20 +117,10 @@ func (s *ControllersSuite) SetupTest() {
|
|||
// Set the status such that no emails are attempted
|
||||
c := models.Campaign{Name: "Test campaign"}
|
||||
c.UserId = 1
|
||||
c.Template = t
|
||||
c.Template = template
|
||||
c.Page = p
|
||||
c.SMTP = smtp
|
||||
c.Groups = []models.Group{group}
|
||||
models.PostCampaign(&c, c.UserId)
|
||||
c.UpdateStatus(models.CampaignEmailsSent)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TearDownSuite() {
|
||||
// Tear down the admin and phishing servers
|
||||
s.adminServer.Close()
|
||||
s.phishServer.Close()
|
||||
}
|
||||
|
||||
func TestControllerSuite(t *testing.T) {
|
||||
suite.Run(t, new(ControllersSuite))
|
||||
}
|
||||
|
|
|
@ -82,19 +82,20 @@ func WithContactAddress(addr string) PhishingServerOption {
|
|||
}
|
||||
|
||||
// Start launches the phishing server, listening on the configured address.
|
||||
func (ps *PhishingServer) Start() error {
|
||||
func (ps *PhishingServer) Start() {
|
||||
if ps.config.UseTLS {
|
||||
// Only support TLS 1.2 and above - ref #1691, #1689
|
||||
ps.server.TLSConfig = defaultTLSConfig
|
||||
err := util.CheckAndCreateSSL(ps.config.CertPath, ps.config.KeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Starting phishing server at https://%s", ps.config.ListenURL)
|
||||
return ps.server.ListenAndServeTLS(ps.config.CertPath, ps.config.KeyPath)
|
||||
log.Fatal(ps.server.ListenAndServeTLS(ps.config.CertPath, ps.config.KeyPath))
|
||||
}
|
||||
// If TLS isn't configured, just listen on HTTP
|
||||
log.Infof("Starting phishing server at http://%s", ps.config.ListenURL)
|
||||
return ps.server.ListenAndServe()
|
||||
log.Fatal(ps.server.ListenAndServe())
|
||||
}
|
||||
|
||||
// Shutdown attempts to gracefully shutdown the server.
|
||||
|
@ -120,6 +121,10 @@ func (ps *PhishingServer) registerRoutes() {
|
|||
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
|
||||
phishHandler := gzipWrapper(router)
|
||||
|
||||
// Respect X-Forwarded-For and X-Real-IP headers in case we're behind a
|
||||
// reverse proxy.
|
||||
phishHandler = handlers.ProxyHeaders(phishHandler)
|
||||
|
||||
// Setup logging
|
||||
phishHandler = handlers.CombinedLoggingHandler(log.Writer(), phishHandler)
|
||||
ps.server.Handler = phishHandler
|
||||
|
@ -161,6 +166,7 @@ func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) {
|
|||
// ReportHandler tracks emails as they are reported, updating the status for the given Result
|
||||
func (ps *PhishingServer) ReportHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r, err := setupContext(r)
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*") // To allow Chrome extensions (or other pages) to report a campaign without violating CORS
|
||||
if err != nil {
|
||||
// Log the error if it wasn't something we can safely ignore
|
||||
if err != ErrInvalidRequest && err != ErrCampaignComplete {
|
||||
|
@ -203,6 +209,7 @@ func (ps *PhishingServer) PhishHandler(w http.ResponseWriter, r *http.Request) {
|
|||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Server", config.ServerName) // Useful for checking if this is a GoPhish server (e.g. for campaign reporting plugins)
|
||||
var ptx models.PhishingTemplateContext
|
||||
// Check for a preview
|
||||
if preview, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
|
||||
|
@ -353,12 +360,7 @@ func setupContext(r *http.Request) (*http.Request, error) {
|
|||
}
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return r, err
|
||||
}
|
||||
// Respect X-Forwarded headers
|
||||
if fips := r.Header.Get("X-Forwarded-For"); fips != "" {
|
||||
ip = strings.Split(fips, ", ")[0]
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
// Handle post processing such as GeoIP
|
||||
err = rs.UpdateGeo(ip)
|
||||
|
|
|
@ -5,22 +5,25 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
func (s *ControllersSuite) getFirstCampaign() models.Campaign {
|
||||
func getFirstCampaign(t *testing.T) models.Campaign {
|
||||
campaigns, err := models.GetCampaigns(1)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting first campaign from database: %v", err)
|
||||
}
|
||||
return campaigns[0]
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) getFirstEmailRequest() models.EmailRequest {
|
||||
campaign := s.getFirstCampaign()
|
||||
func getFirstEmailRequest(t *testing.T) models.EmailRequest {
|
||||
campaign := getFirstCampaign(t)
|
||||
req := models.EmailRequest{
|
||||
TemplateId: campaign.TemplateId,
|
||||
Template: campaign.Template,
|
||||
|
@ -33,205 +36,334 @@ func (s *ControllersSuite) getFirstEmailRequest() models.EmailRequest {
|
|||
FromAddress: campaign.SMTP.FromAddress,
|
||||
}
|
||||
err := models.PostEmailRequest(&req)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating email request: %v", err)
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) openEmail(rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", s.phishServer.URL, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
func openEmail(t *testing.T, ctx *testContext, rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ctx.phishServer.URL, models.RecipientParameter, rid))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting /track endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
s.Nil(err)
|
||||
got, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading response body from /track endpoint: %v", err)
|
||||
}
|
||||
expected, err := ioutil.ReadFile("static/images/pixel.png")
|
||||
s.Nil(err)
|
||||
s.Equal(bytes.Compare(body, expected), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading local transparent pixel: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, expected) {
|
||||
t.Fatalf("unexpected tracking pixel data received. expected %#v got %#v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) reportedEmail(rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", s.phishServer.URL, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
s.Equal(resp.StatusCode, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) reportEmail404(rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", s.phishServer.URL, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
s.Equal(resp.StatusCode, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) openEmail404(rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", s.phishServer.URL, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
func openEmail404(t *testing.T, ctx *testContext, rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ctx.phishServer.URL, models.RecipientParameter, rid))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting /track endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
s.Nil(err)
|
||||
s.Equal(resp.StatusCode, http.StatusNotFound)
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusNotFound
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received for /track endpoint. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) clickLink(rid string, expectedHTML string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", s.phishServer.URL, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
s.Nil(err)
|
||||
log.Printf("%s\n\n\n", body)
|
||||
s.Equal(bytes.Compare(body, []byte(expectedHTML)), 0)
|
||||
func reportedEmail(t *testing.T, ctx *testContext, rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", ctx.phishServer.URL, models.RecipientParameter, rid))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting /report endpoint: %v", err)
|
||||
}
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusNoContent
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received for /report endpoint. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) clickLink404(rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", s.phishServer.URL, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
s.Nil(err)
|
||||
s.Equal(resp.StatusCode, http.StatusNotFound)
|
||||
func reportEmail404(t *testing.T, ctx *testContext, rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", ctx.phishServer.URL, models.RecipientParameter, rid))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting /report endpoint: %v", err)
|
||||
}
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusNotFound
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received for /report endpoint. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) transparencyRequest(r models.Result, rid, path string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s%s?%s=%s", s.phishServer.URL, path, models.RecipientParameter, rid))
|
||||
s.Nil(err)
|
||||
func clickLink(t *testing.T, ctx *testContext, rid string, expectedHTML string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", ctx.phishServer.URL, models.RecipientParameter, rid))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting / endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
s.Equal(resp.StatusCode, http.StatusOK)
|
||||
got, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading payload from / endpoint response: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, []byte(expectedHTML)) {
|
||||
t.Fatalf("invalid response received from / endpoint. expected %s got %s", got, expectedHTML)
|
||||
}
|
||||
}
|
||||
|
||||
func clickLink404(t *testing.T, ctx *testContext, rid string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", ctx.phishServer.URL, models.RecipientParameter, rid))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting / endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusNotFound
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received for / endpoint. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func transparencyRequest(t *testing.T, ctx *testContext, r models.Result, rid, path string) {
|
||||
resp, err := http.Get(fmt.Sprintf("%s%s?%s=%s", ctx.phishServer.URL, path, models.RecipientParameter, rid))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting %s endpoint: %v", path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusOK
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received for / endpoint. expected %d got %d", expected, got)
|
||||
}
|
||||
tr := &TransparencyResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(tr)
|
||||
s.Nil(err)
|
||||
s.Equal(tr.ContactAddress, s.config.ContactAddress)
|
||||
s.Equal(tr.SendDate, r.SendDate)
|
||||
s.Equal(tr.Server, config.ServerName)
|
||||
if err != nil {
|
||||
t.Fatalf("error unmarshaling transparency request: %v", err)
|
||||
}
|
||||
expectedResponse := &TransparencyResponse{
|
||||
ContactAddress: ctx.config.ContactAddress,
|
||||
SendDate: r.SendDate,
|
||||
Server: config.ServerName,
|
||||
}
|
||||
if !reflect.DeepEqual(tr, expectedResponse) {
|
||||
t.Fatalf("unexpected transparency response received. expected %v got %v", expectedResponse, tr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestOpenedPhishingEmail() {
|
||||
campaign := s.getFirstCampaign()
|
||||
func TestOpenedPhishingEmail(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
campaign := getFirstCampaign(t)
|
||||
result := campaign.Results[0]
|
||||
s.Equal(result.Status, models.StatusSending)
|
||||
if result.Status != models.StatusSending {
|
||||
t.Fatalf("unexpected result status received. expected %s got %s", models.StatusSending, result.Status)
|
||||
}
|
||||
|
||||
s.openEmail(result.RId)
|
||||
openEmail(t, ctx, result.RId)
|
||||
|
||||
campaign = s.getFirstCampaign()
|
||||
campaign = getFirstCampaign(t)
|
||||
result = campaign.Results[0]
|
||||
lastEvent := campaign.Events[len(campaign.Events)-1]
|
||||
s.Equal(result.Status, models.EventOpened)
|
||||
s.Equal(lastEvent.Message, models.EventOpened)
|
||||
s.Equal(result.ModifiedDate, lastEvent.Time)
|
||||
if result.Status != models.EventOpened {
|
||||
t.Fatalf("unexpected result status received. expected %s got %s", models.EventOpened, result.Status)
|
||||
}
|
||||
if lastEvent.Message != models.EventOpened {
|
||||
t.Fatalf("unexpected event status received. expected %s got %s", lastEvent.Message, models.EventOpened)
|
||||
}
|
||||
if result.ModifiedDate != lastEvent.Time {
|
||||
t.Fatalf("unexpected result modified date received. expected %s got %s", lastEvent.Time, result.ModifiedDate)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestReportedPhishingEmail() {
|
||||
campaign := s.getFirstCampaign()
|
||||
func TestReportedPhishingEmail(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
campaign := getFirstCampaign(t)
|
||||
result := campaign.Results[0]
|
||||
s.Equal(result.Status, models.StatusSending)
|
||||
if result.Status != models.StatusSending {
|
||||
t.Fatalf("unexpected result status received. expected %s got %s", models.StatusSending, result.Status)
|
||||
}
|
||||
|
||||
s.reportedEmail(result.RId)
|
||||
reportedEmail(t, ctx, result.RId)
|
||||
|
||||
campaign = s.getFirstCampaign()
|
||||
campaign = getFirstCampaign(t)
|
||||
result = campaign.Results[0]
|
||||
lastEvent := campaign.Events[len(campaign.Events)-1]
|
||||
s.Equal(result.Reported, true)
|
||||
s.Equal(lastEvent.Message, models.EventReported)
|
||||
s.Equal(result.ModifiedDate, lastEvent.Time)
|
||||
|
||||
if result.Reported != true {
|
||||
t.Fatalf("unexpected result report status received. expected %v got %v", true, result.Reported)
|
||||
}
|
||||
if lastEvent.Message != models.EventReported {
|
||||
t.Fatalf("unexpected event status received. expected %s got %s", lastEvent.Message, models.EventReported)
|
||||
}
|
||||
if result.ModifiedDate != lastEvent.Time {
|
||||
t.Fatalf("unexpected result modified date received. expected %s got %s", lastEvent.Time, result.ModifiedDate)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
|
||||
campaign := s.getFirstCampaign()
|
||||
func TestClickedPhishingLinkAfterOpen(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
campaign := getFirstCampaign(t)
|
||||
result := campaign.Results[0]
|
||||
s.Equal(result.Status, models.StatusSending)
|
||||
if result.Status != models.StatusSending {
|
||||
t.Fatalf("unexpected result status received. expected %s got %s", models.StatusSending, result.Status)
|
||||
}
|
||||
|
||||
s.openEmail(result.RId)
|
||||
s.clickLink(result.RId, campaign.Page.HTML)
|
||||
openEmail(t, ctx, result.RId)
|
||||
clickLink(t, ctx, result.RId, campaign.Page.HTML)
|
||||
|
||||
campaign = s.getFirstCampaign()
|
||||
campaign = getFirstCampaign(t)
|
||||
result = campaign.Results[0]
|
||||
lastEvent := campaign.Events[len(campaign.Events)-1]
|
||||
s.Equal(result.Status, models.EventClicked)
|
||||
s.Equal(lastEvent.Message, models.EventClicked)
|
||||
s.Equal(result.ModifiedDate, lastEvent.Time)
|
||||
if result.Status != models.EventClicked {
|
||||
t.Fatalf("unexpected result status received. expected %s got %s", models.EventClicked, result.Status)
|
||||
}
|
||||
if lastEvent.Message != models.EventClicked {
|
||||
t.Fatalf("unexpected event status received. expected %s got %s", lastEvent.Message, models.EventClicked)
|
||||
}
|
||||
if result.ModifiedDate != lastEvent.Time {
|
||||
t.Fatalf("unexpected result modified date received. expected %s got %s", lastEvent.Time, result.ModifiedDate)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestNoRecipientID() {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/track", s.phishServer.URL))
|
||||
s.Nil(err)
|
||||
s.Equal(resp.StatusCode, http.StatusNotFound)
|
||||
func TestNoRecipientID(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
resp, err := http.Get(fmt.Sprintf("%s/track", ctx.phishServer.URL))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting /track endpoint: %v", err)
|
||||
}
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusNotFound
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received for /track endpoint. expected %d got %d", expected, got)
|
||||
}
|
||||
|
||||
resp, err = http.Get(s.phishServer.URL)
|
||||
s.Nil(err)
|
||||
s.Equal(resp.StatusCode, http.StatusNotFound)
|
||||
resp, err = http.Get(ctx.phishServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting /track endpoint: %v", err)
|
||||
}
|
||||
got = resp.StatusCode
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received for / endpoint. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestInvalidRecipientID() {
|
||||
func TestInvalidRecipientID(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
rid := "XXXXXXXXXX"
|
||||
s.openEmail404(rid)
|
||||
s.clickLink404(rid)
|
||||
s.reportEmail404(rid)
|
||||
openEmail404(t, ctx, rid)
|
||||
clickLink404(t, ctx, rid)
|
||||
reportEmail404(t, ctx, rid)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestCompletedCampaignClick() {
|
||||
campaign := s.getFirstCampaign()
|
||||
func TestCompletedCampaignClick(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
campaign := getFirstCampaign(t)
|
||||
result := campaign.Results[0]
|
||||
s.Equal(result.Status, models.StatusSending)
|
||||
s.openEmail(result.RId)
|
||||
if result.Status != models.StatusSending {
|
||||
t.Fatalf("unexpected result status received. expected %s got %s", models.StatusSending, result.Status)
|
||||
}
|
||||
|
||||
campaign = s.getFirstCampaign()
|
||||
openEmail(t, ctx, result.RId)
|
||||
|
||||
campaign = getFirstCampaign(t)
|
||||
result = campaign.Results[0]
|
||||
s.Equal(result.Status, models.EventOpened)
|
||||
if result.Status != models.EventOpened {
|
||||
t.Fatalf("unexpected result status received. expected %s got %s", models.EventOpened, result.Status)
|
||||
}
|
||||
|
||||
models.CompleteCampaign(campaign.Id, 1)
|
||||
s.openEmail404(result.RId)
|
||||
s.clickLink404(result.RId)
|
||||
openEmail404(t, ctx, result.RId)
|
||||
clickLink404(t, ctx, result.RId)
|
||||
|
||||
campaign = s.getFirstCampaign()
|
||||
campaign = getFirstCampaign(t)
|
||||
result = campaign.Results[0]
|
||||
s.Equal(result.Status, models.EventOpened)
|
||||
if result.Status != models.EventOpened {
|
||||
t.Fatalf("unexpected result status received. expected %s got %s", models.EventOpened, result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestRobotsHandler() {
|
||||
expected := []byte("User-agent: *\nDisallow: /\n")
|
||||
resp, err := http.Get(fmt.Sprintf("%s/robots.txt", s.phishServer.URL))
|
||||
s.Nil(err)
|
||||
s.Equal(resp.StatusCode, http.StatusOK)
|
||||
func TestRobotsHandler(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
resp, err := http.Get(fmt.Sprintf("%s/robots.txt", ctx.phishServer.URL))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting /robots.txt endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
got := resp.StatusCode
|
||||
expectedStatus := http.StatusOK
|
||||
if got != expectedStatus {
|
||||
t.Fatalf("invalid status code received for /track endpoint. expected %d got %d", expectedStatus, got)
|
||||
}
|
||||
expected := []byte("User-agent: *\nDisallow: /\n")
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
s.Nil(err)
|
||||
s.Equal(bytes.Compare(body, expected), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading response body from /robots.txt endpoint: %v", err)
|
||||
}
|
||||
if !bytes.Equal(body, expected) {
|
||||
t.Fatalf("invalid robots.txt response received. expected %s got %s", expected, body)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestInvalidPreviewID() {
|
||||
func TestInvalidPreviewID(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
bogusRId := fmt.Sprintf("%sbogus", models.PreviewPrefix)
|
||||
s.openEmail404(bogusRId)
|
||||
s.clickLink404(bogusRId)
|
||||
s.reportEmail404(bogusRId)
|
||||
openEmail404(t, ctx, bogusRId)
|
||||
clickLink404(t, ctx, bogusRId)
|
||||
reportEmail404(t, ctx, bogusRId)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestPreviewTrack() {
|
||||
req := s.getFirstEmailRequest()
|
||||
s.openEmail(req.RId)
|
||||
func TestPreviewTrack(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
req := getFirstEmailRequest(t)
|
||||
openEmail(t, ctx, req.RId)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestPreviewClick() {
|
||||
req := s.getFirstEmailRequest()
|
||||
s.clickLink(req.RId, req.Page.HTML)
|
||||
func TestPreviewClick(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
req := getFirstEmailRequest(t)
|
||||
clickLink(t, ctx, req.RId, req.Page.HTML)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestInvalidTransparencyRequest() {
|
||||
func TestInvalidTransparencyRequest(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
bogusRId := fmt.Sprintf("bogus%s", TransparencySuffix)
|
||||
s.openEmail404(bogusRId)
|
||||
s.clickLink404(bogusRId)
|
||||
s.reportEmail404(bogusRId)
|
||||
openEmail404(t, ctx, bogusRId)
|
||||
clickLink404(t, ctx, bogusRId)
|
||||
reportEmail404(t, ctx, bogusRId)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestTransparencyRequest() {
|
||||
campaign := s.getFirstCampaign()
|
||||
func TestTransparencyRequest(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
campaign := getFirstCampaign(t)
|
||||
result := campaign.Results[0]
|
||||
rid := fmt.Sprintf("%s%s", result.RId, TransparencySuffix)
|
||||
s.transparencyRequest(result, rid, "/")
|
||||
s.transparencyRequest(result, rid, "/track")
|
||||
s.transparencyRequest(result, rid, "/report")
|
||||
transparencyRequest(t, ctx, result, rid, "/")
|
||||
transparencyRequest(t, ctx, result, rid, "/track")
|
||||
transparencyRequest(t, ctx, result, rid, "/report")
|
||||
|
||||
// And check with the URL encoded version of a +
|
||||
rid = fmt.Sprintf("%s%s", result.RId, "%2b")
|
||||
s.transparencyRequest(result, rid, "/")
|
||||
s.transparencyRequest(result, rid, "/track")
|
||||
s.transparencyRequest(result, rid, "/report")
|
||||
transparencyRequest(t, ctx, result, rid, "/")
|
||||
transparencyRequest(t, ctx, result, rid, "/track")
|
||||
transparencyRequest(t, ctx, result, rid, "/report")
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestRedirectTemplating() {
|
||||
func TestRedirectTemplating(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
p := models.Page{
|
||||
Name: "Redirect Page",
|
||||
HTML: "<html>Test</html>",
|
||||
|
@ -239,7 +371,9 @@ func (s *ControllersSuite) TestRedirectTemplating() {
|
|||
RedirectURL: "http://example.com/{{.RId}}",
|
||||
}
|
||||
err := models.PostPage(&p)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error posting new page: %v", err)
|
||||
}
|
||||
smtp, _ := models.GetSMTP(1, 1)
|
||||
template, _ := models.GetTemplate(1, 1)
|
||||
group, _ := models.GetGroup(1, 1)
|
||||
|
@ -251,7 +385,9 @@ func (s *ControllersSuite) TestRedirectTemplating() {
|
|||
campaign.SMTP = smtp
|
||||
campaign.Groups = []models.Group{group}
|
||||
err = models.PostCampaign(&campaign, campaign.UserId)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating campaign: %v", err)
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
|
@ -259,12 +395,22 @@ func (s *ControllersSuite) TestRedirectTemplating() {
|
|||
},
|
||||
}
|
||||
result := campaign.Results[0]
|
||||
resp, err := client.PostForm(fmt.Sprintf("%s/?%s=%s", s.phishServer.URL, models.RecipientParameter, result.RId), url.Values{"username": {"test"}, "password": {"test"}})
|
||||
s.Nil(err)
|
||||
resp, err := client.PostForm(fmt.Sprintf("%s/?%s=%s", ctx.phishServer.URL, models.RecipientParameter, result.RId), url.Values{"username": {"test"}, "password": {"test"}})
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting / endpoint: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
s.Equal(http.StatusFound, resp.StatusCode)
|
||||
got := resp.StatusCode
|
||||
expectedStatus := http.StatusFound
|
||||
if got != expectedStatus {
|
||||
t.Fatalf("invalid status code received for /track endpoint. expected %d got %d", expectedStatus, got)
|
||||
}
|
||||
expectedURL := fmt.Sprintf("http://example.com/%s", result.RId)
|
||||
got, err := resp.Location()
|
||||
s.Nil(err)
|
||||
s.Equal(expectedURL, got.String())
|
||||
gotURL, err := resp.Location()
|
||||
if err != nil {
|
||||
t.Fatalf("error getting Location header from response: %v", err)
|
||||
}
|
||||
if gotURL.String() != expectedURL {
|
||||
t.Fatalf("invalid redirect received. expected %s got %s", expectedURL, gotURL)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,11 @@ package controllers
|
|||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/NYTimes/gziphandler"
|
||||
|
@ -15,6 +17,7 @@ import (
|
|||
"github.com/gophish/gophish/controllers/api"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/middleware/ratelimit"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gophish/gophish/worker"
|
||||
|
@ -32,9 +35,31 @@ type AdminServerOption func(*AdminServer)
|
|||
// AdminServer is an HTTP server that implements the administrative Gophish
|
||||
// handlers, including the dashboard and REST API.
|
||||
type AdminServer struct {
|
||||
server *http.Server
|
||||
worker worker.Worker
|
||||
config config.AdminServer
|
||||
server *http.Server
|
||||
worker worker.Worker
|
||||
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.
|
||||
|
@ -52,10 +77,12 @@ func NewAdminServer(config config.AdminServer, options ...AdminServerOption) *Ad
|
|||
ReadTimeout: 10 * time.Second,
|
||||
Addr: config.ListenURL,
|
||||
}
|
||||
defaultLimiter := ratelimit.NewPostLimiter()
|
||||
as := &AdminServer{
|
||||
worker: defaultWorker,
|
||||
server: defaultServer,
|
||||
config: config,
|
||||
worker: defaultWorker,
|
||||
server: defaultServer,
|
||||
limiter: defaultLimiter,
|
||||
config: config,
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(as)
|
||||
|
@ -65,22 +92,23 @@ func NewAdminServer(config config.AdminServer, options ...AdminServerOption) *Ad
|
|||
}
|
||||
|
||||
// Start launches the admin server, listening on the configured address.
|
||||
func (as *AdminServer) Start() error {
|
||||
func (as *AdminServer) Start() {
|
||||
if as.worker != nil {
|
||||
go as.worker.Start()
|
||||
}
|
||||
if as.config.UseTLS {
|
||||
// Only support TLS 1.2 and above - ref #1691, #1689
|
||||
as.server.TLSConfig = defaultTLSConfig
|
||||
err := util.CheckAndCreateSSL(as.config.CertPath, as.config.KeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Starting admin server at https://%s", as.config.ListenURL)
|
||||
return as.server.ListenAndServeTLS(as.config.CertPath, as.config.KeyPath)
|
||||
log.Fatal(as.server.ListenAndServeTLS(as.config.CertPath, as.config.KeyPath))
|
||||
}
|
||||
// If TLS isn't configured, just listen on HTTP
|
||||
log.Infof("Starting admin server at http://%s", as.config.ListenURL)
|
||||
return as.server.ListenAndServe()
|
||||
log.Fatal(as.server.ListenAndServe())
|
||||
}
|
||||
|
||||
// Shutdown attempts to gracefully shutdown the server.
|
||||
|
@ -96,8 +124,9 @@ func (as *AdminServer) registerRoutes() {
|
|||
router := mux.NewRouter()
|
||||
// Base Front-end routes
|
||||
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("/reset_password", mid.Use(as.ResetPassword, 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("/templates", mid.Use(as.Templates, mid.RequireLogin))
|
||||
|
@ -106,24 +135,38 @@ func (as *AdminServer) registerRoutes() {
|
|||
router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, 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("/webhooks", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
router.HandleFunc("/impersonate", mid.Use(as.Impersonate, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
// 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)
|
||||
|
||||
// Setup static file serving
|
||||
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
|
||||
|
||||
// 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.Secure(as.config.UseTLS))
|
||||
csrf.Secure(as.config.UseTLS),
|
||||
csrf.TrustedOrigins(as.config.TrustedOrigins))
|
||||
adminHandler := csrfHandler(router)
|
||||
adminHandler = mid.Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
|
||||
adminHandler = mid.Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext, mid.ApplySecurityHeaders)
|
||||
|
||||
// Setup GZIP compression
|
||||
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
|
||||
adminHandler = gzipWrapper(adminHandler)
|
||||
|
||||
// Respect X-Forwarded-For and X-Real-IP headers in case we're behind a
|
||||
// reverse proxy.
|
||||
adminHandler = handlers.ProxyHeaders(adminHandler)
|
||||
|
||||
// Setup logging
|
||||
adminHandler = handlers.CombinedLoggingHandler(log.Writer(), adminHandler)
|
||||
as.server.Handler = adminHandler
|
||||
|
@ -142,12 +185,14 @@ type templateParams struct {
|
|||
// the CSRF token.
|
||||
func newTemplateParams(r *http.Request) templateParams {
|
||||
user := ctx.Get(r, "user").(models.User)
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
modifySystem, _ := user.HasPermission(models.PermissionModifySystem)
|
||||
return templateParams{
|
||||
Token: csrf.Token(r),
|
||||
User: user,
|
||||
ModifySystem: modifySystem,
|
||||
Version: config.Version,
|
||||
Flashes: session.Flashes(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,22 +251,37 @@ func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) {
|
|||
case r.Method == "GET":
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "Settings"
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
session.Save(r, w)
|
||||
getTemplate(w, "settings").ExecuteTemplate(w, "base", params)
|
||||
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"}
|
||||
if err == auth.ErrInvalidPassword {
|
||||
msg.Message = "Invalid Password"
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
msg.Message = err.Error()
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -234,6 +294,64 @@ func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
|
|||
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.EscapedPath()
|
||||
if path != "" {
|
||||
next = "/" + strings.TrimLeft(path, "/")
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, next, http.StatusFound)
|
||||
}
|
||||
|
||||
func (as *AdminServer) handleInvalidLogin(w http.ResponseWriter, r *http.Request, message string) {
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
Flash(w, r, "danger", message)
|
||||
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
|
||||
func (as *AdminServer) Webhooks(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "Webhooks"
|
||||
getTemplate(w, "webhooks").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// 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,
|
||||
// a session is created
|
||||
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -255,37 +373,34 @@ func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
||||
case r.Method == "POST":
|
||||
//Attempt to login
|
||||
succ, u, err := auth.Login(r)
|
||||
// Find the user with the provided username
|
||||
username, password := r.FormValue("username"), r.FormValue("password")
|
||||
u, err := models.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
as.handleInvalidLogin(w, r, "Invalid Username/Password")
|
||||
return
|
||||
}
|
||||
// Validate the user's password
|
||||
err = auth.ValidatePassword(password, u.Hash)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
as.handleInvalidLogin(w, r, "Invalid Username/Password")
|
||||
return
|
||||
}
|
||||
if u.AccountLocked {
|
||||
as.handleInvalidLogin(w, r, "Account Locked")
|
||||
return
|
||||
}
|
||||
u.LastLogin = time.Now().UTC()
|
||||
err = models.PutUser(&u)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
//If we've logged in, save the session and redirect to the dashboard
|
||||
if succ {
|
||||
session.Values["id"] = u.Id
|
||||
session.Save(r, w)
|
||||
next := "/"
|
||||
url, err := url.Parse(r.FormValue("next"))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,9 +410,72 @@ func (as *AdminServer) Logout(w http.ResponseWriter, r *http.Request) {
|
|||
delete(session.Values, "id")
|
||||
Flash(w, r, "success", "You have successfully logged out")
|
||||
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 {
|
||||
templates := template.New("template")
|
||||
_, err := templates.ParseFiles("templates/base.html", "templates/nav.html", "templates/"+tmpl+".html", "templates/flashes.html")
|
||||
|
|
|
@ -5,106 +5,126 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func (s *ControllersSuite) TestLoginCSRF() {
|
||||
resp, err := http.PostForm(fmt.Sprintf("%s/login", s.adminServer.URL),
|
||||
func attemptLogin(t *testing.T, ctx *testContext, client *http.Client, username, password, optionalPath string) *http.Response {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/login", ctx.adminServer.URL))
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting the /login endpoint: %v", err)
|
||||
}
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusOK
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromResponse(resp)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing /login response body")
|
||||
}
|
||||
elem := doc.Find("input[name='csrf_token']").First()
|
||||
token, ok := elem.Attr("value")
|
||||
if !ok {
|
||||
t.Fatal("unable to find csrf_token value in login response")
|
||||
}
|
||||
if client == nil {
|
||||
client = &http.Client{}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login%s", ctx.adminServer.URL, optionalPath), strings.NewReader(url.Values{
|
||||
"username": {username},
|
||||
"password": {password},
|
||||
"csrf_token": {token},
|
||||
}.Encode()))
|
||||
if err != nil {
|
||||
t.Fatalf("error creating new /login request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Cookie", resp.Header.Get("Set-Cookie"))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting the /login endpoint: %v", err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestLoginCSRF(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
resp, err := http.PostForm(fmt.Sprintf("%s/login", ctx.adminServer.URL),
|
||||
url.Values{
|
||||
"username": {"admin"},
|
||||
"password": {"gophish"},
|
||||
})
|
||||
|
||||
s.Equal(resp.StatusCode, http.StatusForbidden)
|
||||
fmt.Println(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error requesting the /login endpoint: %v", err)
|
||||
}
|
||||
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusForbidden
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestInvalidCredentials() {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/login", s.adminServer.URL))
|
||||
s.Equal(err, nil)
|
||||
s.Equal(resp.StatusCode, http.StatusOK)
|
||||
|
||||
doc, err := goquery.NewDocumentFromResponse(resp)
|
||||
s.Equal(err, nil)
|
||||
elem := doc.Find("input[name='csrf_token']").First()
|
||||
token, ok := elem.Attr("value")
|
||||
s.Equal(ok, true)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login", s.adminServer.URL), strings.NewReader(url.Values{
|
||||
"username": {"admin"},
|
||||
"password": {"invalid"},
|
||||
"csrf_token": {token},
|
||||
}.Encode()))
|
||||
s.Equal(err, nil)
|
||||
|
||||
req.Header.Set("Cookie", resp.Header.Get("Set-Cookie"))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err = client.Do(req)
|
||||
s.Equal(err, nil)
|
||||
s.Equal(resp.StatusCode, http.StatusUnauthorized)
|
||||
func TestInvalidCredentials(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
resp := attemptLogin(t, ctx, nil, "admin", "bogus", "")
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusUnauthorized
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestSuccessfulLogin() {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/login", s.adminServer.URL))
|
||||
s.Equal(err, nil)
|
||||
s.Equal(resp.StatusCode, http.StatusOK)
|
||||
|
||||
doc, err := goquery.NewDocumentFromResponse(resp)
|
||||
s.Equal(err, nil)
|
||||
elem := doc.Find("input[name='csrf_token']").First()
|
||||
token, ok := elem.Attr("value")
|
||||
s.Equal(ok, true)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login", s.adminServer.URL), strings.NewReader(url.Values{
|
||||
"username": {"admin"},
|
||||
"password": {"gophish"},
|
||||
"csrf_token": {token},
|
||||
}.Encode()))
|
||||
s.Equal(err, nil)
|
||||
|
||||
req.Header.Set("Cookie", resp.Header.Get("Set-Cookie"))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err = client.Do(req)
|
||||
s.Equal(err, nil)
|
||||
s.Equal(resp.StatusCode, http.StatusOK)
|
||||
func TestSuccessfulLogin(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
resp := attemptLogin(t, ctx, nil, "admin", "gophish", "")
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusOK
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestSuccessfulRedirect() {
|
||||
func TestSuccessfulRedirect(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
next := "/campaigns"
|
||||
resp, err := http.Get(fmt.Sprintf("%s/login", s.adminServer.URL))
|
||||
s.Equal(err, nil)
|
||||
s.Equal(resp.StatusCode, http.StatusOK)
|
||||
|
||||
doc, err := goquery.NewDocumentFromResponse(resp)
|
||||
s.Equal(err, nil)
|
||||
elem := doc.Find("input[name='csrf_token']").First()
|
||||
token, ok := elem.Attr("value")
|
||||
s.Equal(ok, true)
|
||||
|
||||
client := &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}}
|
||||
resp := attemptLogin(t, ctx, client, "admin", "gophish", fmt.Sprintf("?next=%s", next))
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusFound
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login?next=%s", s.adminServer.URL, next), strings.NewReader(url.Values{
|
||||
"username": {"admin"},
|
||||
"password": {"gophish"},
|
||||
"csrf_token": {token},
|
||||
}.Encode()))
|
||||
s.Equal(err, nil)
|
||||
|
||||
req.Header.Set("Cookie", resp.Header.Get("Set-Cookie"))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err = client.Do(req)
|
||||
s.Equal(err, nil)
|
||||
s.Equal(resp.StatusCode, http.StatusFound)
|
||||
url, err := resp.Location()
|
||||
s.Equal(err, nil)
|
||||
s.Equal(url.Path, next)
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing response Location header: %v", err)
|
||||
}
|
||||
if url.Path != next {
|
||||
t.Fatalf("unexpected Location header received. expected %s got %s", next, url.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountLocked(t *testing.T) {
|
||||
ctx := setupTest(t)
|
||||
defer tearDown(t, ctx)
|
||||
resp := attemptLogin(t, ctx, nil, "houdini", "gophish", "")
|
||||
got := resp.StatusCode
|
||||
expected := http.StatusUnauthorized
|
||||
if got != expected {
|
||||
t.Fatalf("invalid status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
CREATE TABLE IF NOT EXISTS `webhooks` (
|
||||
id integer primary key auto_increment,
|
||||
name varchar(255),
|
||||
url varchar(1000),
|
||||
secret varchar(255),
|
||||
is_active boolean default 0
|
||||
);
|
||||
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
CREATE TABLE IF NOT EXISTS `imap` (user_id bigint,host varchar(255),port int,username varchar(255),password varchar(255),modified_date datetime,tls boolean,enabled boolean,folder varchar(255),restrict_domain varchar(255),delete_reported_campaign_email boolean,last_login datetime,imap_freq int);
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
DROP TABLE `imap`;
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE `imap` ADD COLUMN ignore_cert_errors BOOLEAN;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
|
@ -0,0 +1,6 @@
|
|||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE `users` ADD COLUMN last_login datetime;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE `users` ADD COLUMN account_locked BOOLEAN;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE templates ADD COLUMN envelope_sender varchar(255);
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
|
@ -25,4 +25,4 @@ DROP TABLE "results";
|
|||
DROP TABLE "smtp";
|
||||
DROP TABLE "targets";
|
||||
DROP TABLE "templates";
|
||||
DROP TABLE "users";
|
||||
DROP TABLE "users";
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
CREATE TABLE IF NOT EXISTS "webhooks" (
|
||||
"id" integer primary key autoincrement,
|
||||
"name" varchar(255),
|
||||
"url" varchar(1000),
|
||||
"secret" varchar(255),
|
||||
"is_active" boolean default 0
|
||||
);
|
||||
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
CREATE TABLE IF NOT EXISTS "imap" ("user_id" bigint, "host" varchar(255), "port" integer, "username" varchar(255), "password" varchar(255), "modified_date" datetime default CURRENT_TIMESTAMP, "tls" BOOLEAN, "enabled" BOOLEAN, "folder" varchar(255), "restrict_domain" varchar(255), "delete_reported_campaign_email" BOOLEAN, "last_login" datetime, "imap_freq" integer);
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
DROP TABLE "imap";
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE imap ADD COLUMN ignore_cert_errors BOOLEAN;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
|
@ -0,0 +1,6 @@
|
|||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE users ADD COLUMN last_login datetime;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
|
@ -0,0 +1,6 @@
|
|||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE users ADD COLUMN account_locked BOOLEAN;
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE templates ADD COLUMN envelope_sender varchar(255);
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package dialer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RestrictedDialer is used to create a net.Dialer which restricts outbound
|
||||
// connections to only allowlisted IP ranges.
|
||||
type RestrictedDialer struct {
|
||||
allowedHosts []*net.IPNet
|
||||
}
|
||||
|
||||
// DefaultDialer is a global instance of a RestrictedDialer
|
||||
var DefaultDialer = &RestrictedDialer{}
|
||||
|
||||
// SetAllowedHosts sets the list of allowed hosts or IP ranges for the default
|
||||
// dialer.
|
||||
func SetAllowedHosts(allowed []string) {
|
||||
DefaultDialer.SetAllowedHosts(allowed)
|
||||
}
|
||||
|
||||
// AllowedHosts returns the configured hosts that are allowed for the dialer.
|
||||
func (d *RestrictedDialer) AllowedHosts() []string {
|
||||
ranges := []string{}
|
||||
for _, ipRange := range d.allowedHosts {
|
||||
ranges = append(ranges, ipRange.String())
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
// SetAllowedHosts sets the list of allowed hosts or IP ranges for the dialer.
|
||||
func (d *RestrictedDialer) SetAllowedHosts(allowed []string) error {
|
||||
for _, ipRange := range allowed {
|
||||
// For flexibility, try to parse as an IP first since this will
|
||||
// undoubtedly cause issues. If it works, then just append the
|
||||
// appropriate subnet mask, then parse as CIDR
|
||||
if singleIP := net.ParseIP(ipRange); singleIP != nil {
|
||||
if singleIP.To4() != nil {
|
||||
ipRange += "/32"
|
||||
} else {
|
||||
ipRange += "/128"
|
||||
}
|
||||
}
|
||||
_, parsed, err := net.ParseCIDR(ipRange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("provided ip range is not valid CIDR notation: %v", err)
|
||||
}
|
||||
d.allowedHosts = append(d.allowedHosts, parsed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dialer returns a net.Dialer that restricts outbound connections to only the
|
||||
// addresses allowed by the DefaultDialer.
|
||||
func Dialer() *net.Dialer {
|
||||
return DefaultDialer.Dialer()
|
||||
}
|
||||
|
||||
// Dialer returns a net.Dialer that restricts outbound connections to only the
|
||||
// allowed addresses over TCP.
|
||||
//
|
||||
// By default, since Gophish anticipates connections originating to hosts on
|
||||
// the local network, we only deny access to the link-local addresses at
|
||||
// 169.254.0.0/16.
|
||||
//
|
||||
// If hosts are provided, then Gophish blocks access to all local addresses
|
||||
// except the ones provided.
|
||||
//
|
||||
// This implementation is based on the blog post by Andrew Ayer at
|
||||
// https://www.agwa.name/blog/post/preventing_server_side_request_forgery_in_golang
|
||||
func (d *RestrictedDialer) Dialer() *net.Dialer {
|
||||
return &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
Control: restrictedControl(d.allowedHosts),
|
||||
}
|
||||
}
|
||||
|
||||
// defaultDeny represents the list of IP ranges that we want to block unless
|
||||
// explicitly overriden.
|
||||
var defaultDeny = []string{
|
||||
"169.254.0.0/16", // Link-local (used for VPS instance metadata)
|
||||
}
|
||||
|
||||
// allInternal represents all internal hosts such that the only connections
|
||||
// allowed are external ones.
|
||||
var allInternal = []string{
|
||||
"0.0.0.0/8",
|
||||
"127.0.0.0/8", // IPv4 loopback
|
||||
"10.0.0.0/8", // RFC1918
|
||||
"100.64.0.0/10", // CGNAT
|
||||
"172.16.0.0/12", // RFC1918
|
||||
"169.254.0.0/16", // RFC3927 link-local
|
||||
"192.88.99.0/24", // IPv6 to IPv4 Relay
|
||||
"192.168.0.0/16", // RFC1918
|
||||
"198.51.100.0/24", // TEST-NET-2
|
||||
"203.0.113.0/24", // TEST-NET-3
|
||||
"224.0.0.0/4", // Multicast
|
||||
"240.0.0.0/4", // Reserved
|
||||
"255.255.255.255/32", // Broadcast
|
||||
"::/0", // Default route
|
||||
"::/128", // Unspecified address
|
||||
"::1/128", // IPv6 loopback
|
||||
"::ffff:0:0/96", // IPv4 mapped addresses.
|
||||
"::ffff:0:0:0/96", // IPv4 translated addresses.
|
||||
"fe80::/10", // IPv6 link-local
|
||||
"fc00::/7", // IPv6 unique local addr
|
||||
}
|
||||
|
||||
type dialControl = func(network, address string, c syscall.RawConn) error
|
||||
|
||||
type restrictedDialer struct {
|
||||
*net.Dialer
|
||||
allowed []string
|
||||
}
|
||||
|
||||
func restrictedControl(allowed []*net.IPNet) dialControl {
|
||||
return func(network string, address string, conn syscall.RawConn) error {
|
||||
if !(network == "tcp4" || network == "tcp6") {
|
||||
return fmt.Errorf("%s is not a safe network type", network)
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s is not a valid host/port pair: %s", address, err)
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("%s is not a valid IP address", host)
|
||||
}
|
||||
|
||||
denyList := defaultDeny
|
||||
if len(allowed) > 0 {
|
||||
denyList = allInternal
|
||||
}
|
||||
|
||||
for _, ipRange := range allowed {
|
||||
if ipRange.Contains(ip) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, ipRange := range denyList {
|
||||
_, parsed, err := net.ParseCIDR(ipRange)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing denied range: %v", err)
|
||||
}
|
||||
if parsed.Contains(ip) {
|
||||
return fmt.Errorf("upstream connection denied to internal host")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package dialer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultDeny(t *testing.T) {
|
||||
control := restrictedControl([]*net.IPNet{})
|
||||
host := "169.254.169.254"
|
||||
expected := fmt.Errorf("upstream connection denied to internal host at %s", host)
|
||||
conn := new(syscall.RawConn)
|
||||
got := control("tcp4", fmt.Sprintf("%s:80", host), *conn)
|
||||
if !strings.Contains(got.Error(), "upstream connection denied") {
|
||||
t.Fatalf("unexpected error dialing denylisted host. expected %v got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAllow(t *testing.T) {
|
||||
control := restrictedControl([]*net.IPNet{})
|
||||
host := "1.1.1.1"
|
||||
conn := new(syscall.RawConn)
|
||||
got := control("tcp4", fmt.Sprintf("%s:80", host), *conn)
|
||||
if got != nil {
|
||||
t.Fatalf("error dialing allowed host. got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomAllow(t *testing.T) {
|
||||
host := "127.0.0.1"
|
||||
_, ipRange, _ := net.ParseCIDR(fmt.Sprintf("%s/32", host))
|
||||
allowed := []*net.IPNet{ipRange}
|
||||
control := restrictedControl(allowed)
|
||||
conn := new(syscall.RawConn)
|
||||
got := control("tcp4", fmt.Sprintf("%s:80", host), *conn)
|
||||
if got != nil {
|
||||
t.Fatalf("error dialing allowed host. got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomDeny(t *testing.T) {
|
||||
host := "127.0.0.1"
|
||||
_, ipRange, _ := net.ParseCIDR(fmt.Sprintf("%s/32", host))
|
||||
allowed := []*net.IPNet{ipRange}
|
||||
control := restrictedControl(allowed)
|
||||
conn := new(syscall.RawConn)
|
||||
expected := fmt.Errorf("upstream connection denied to internal host at %s", host)
|
||||
got := control("tcp4", "192.168.1.2:80", *conn)
|
||||
if !strings.Contains(got.Error(), "upstream connection denied") {
|
||||
t.Fatalf("unexpected error dialing denylisted host. expected %v got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleIP(t *testing.T) {
|
||||
orig := DefaultDialer.AllowedHosts()
|
||||
host := "127.0.0.1"
|
||||
DefaultDialer.SetAllowedHosts([]string{host})
|
||||
control := DefaultDialer.Dialer().Control
|
||||
conn := new(syscall.RawConn)
|
||||
expected := fmt.Errorf("upstream connection denied to internal host at %s", host)
|
||||
got := control("tcp4", "192.168.1.2:80", *conn)
|
||||
if !strings.Contains(got.Error(), "upstream connection denied") {
|
||||
t.Fatalf("unexpected error dialing denylisted host. expected %v got %v", expected, got)
|
||||
}
|
||||
|
||||
host = "::1"
|
||||
DefaultDialer.SetAllowedHosts([]string{host})
|
||||
control = DefaultDialer.Dialer().Control
|
||||
conn = new(syscall.RawConn)
|
||||
expected = fmt.Errorf("upstream connection denied to internal host at %s", host)
|
||||
got = control("tcp4", "192.168.1.2:80", *conn)
|
||||
if !strings.Contains(got.Error(), "upstream connection denied") {
|
||||
t.Fatalf("unexpected error dialing denylisted host. expected %v got %v", expected, got)
|
||||
}
|
||||
|
||||
// Test an allowed connection
|
||||
got = control("tcp4", fmt.Sprintf("[%s]:80", host), *conn)
|
||||
if got != nil {
|
||||
t.Fatalf("error dialing allowed host. got %v", got)
|
||||
}
|
||||
DefaultDialer.SetAllowedHosts(orig)
|
||||
}
|
|
@ -5,25 +5,31 @@ if [ -n "${ADMIN_LISTEN_URL+set}" ] ; then
|
|||
jq -r \
|
||||
--arg ADMIN_LISTEN_URL "${ADMIN_LISTEN_URL}" \
|
||||
'.admin_server.listen_url = $ADMIN_LISTEN_URL' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${ADMIN_USE_TLS+set}" ] ; then
|
||||
jq -r \
|
||||
--argjson ADMIN_USE_TLS "${ADMIN_USE_TLS}" \
|
||||
'.admin_server.use_tls = $ADMIN_USE_TLS' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${ADMIN_CERT_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg ADMIN_CERT_PATH "${ADMIN_CERT_PATH}" \
|
||||
'.admin_server.cert_path = $ADMIN_CERT_PATH' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${ADMIN_KEY_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg ADMIN_KEY_PATH "${ADMIN_KEY_PATH}" \
|
||||
'.admin_server.key_path = $ADMIN_KEY_PATH' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${ADMIN_TRUSTED_ORIGINS+set}" ] ; then
|
||||
jq -r \
|
||||
--arg ADMIN_TRUSTED_ORIGINS "${ADMIN_TRUSTED_ORIGINS}" \
|
||||
'.admin_server.trusted_origins = ($ADMIN_TRUSTED_ORIGINS|split(","))' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
# set config for phish_server
|
||||
|
@ -31,25 +37,25 @@ if [ -n "${PHISH_LISTEN_URL+set}" ] ; then
|
|||
jq -r \
|
||||
--arg PHISH_LISTEN_URL "${PHISH_LISTEN_URL}" \
|
||||
'.phish_server.listen_url = $PHISH_LISTEN_URL' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${PHISH_USE_TLS+set}" ] ; then
|
||||
jq -r \
|
||||
--argjson PHISH_USE_TLS "${PHISH_USE_TLS}" \
|
||||
'.phish_server.use_tls = $PHISH_USE_TLS' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${PHISH_CERT_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg PHISH_CERT_PATH "${PHISH_CERT_PATH}" \
|
||||
'.phish_server.cert_path = $PHISH_CERT_PATH' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${PHISH_KEY_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg PHISH_KEY_PATH "${PHISH_KEY_PATH}" \
|
||||
'.phish_server.key_path = $PHISH_KEY_PATH' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
# set contact_address
|
||||
|
@ -57,9 +63,25 @@ if [ -n "${CONTACT_ADDRESS+set}" ] ; then
|
|||
jq -r \
|
||||
--arg CONTACT_ADDRESS "${CONTACT_ADDRESS}" \
|
||||
'.contact_address = $CONTACT_ADDRESS' config.json > config.json.tmp && \
|
||||
mv config.json.tmp config.json
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
# db_name has to be changed to mysql for mysql connection to work
|
||||
if [ -n "${DB_NAME+set}" ] ; then
|
||||
jq -r \
|
||||
--arg DB_NAME "${DB_NAME}" \
|
||||
'.db_name = $DB_NAME' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
if [ -n "${DB_FILE_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg DB_FILE_PATH "${DB_FILE_PATH}" \
|
||||
'.db_path = $DB_FILE_PATH' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
echo "Runtime configuration: "
|
||||
cat config.json
|
||||
|
||||
# start gophish
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
module github.com/gophish/gophish
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/PuerkitoBio/goquery v1.5.0
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // 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/gophish/gomail v0.0.0-20200818021916-1f6d0dfd512e
|
||||
github.com/gorilla/context v1.1.1
|
||||
github.com/gorilla/csrf v1.6.2
|
||||
github.com/gorilla/handlers v1.4.2
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/jinzhu/gorm v1.9.12
|
||||
github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible
|
||||
github.com/jordan-wright/unindexed v0.0.0-20181209214434-78fa79113c0f
|
||||
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/oschwald/maxminddb-golang v1.6.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
|
||||
)
|
|
@ -0,0 +1,115 @@
|
|||
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c h1:bkb2NMGo3/Du52wvYj9Whth5KZfMV6d3O0Vbr3nz/UE=
|
||||
bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
|
||||
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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/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/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.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gophish/gomail v0.0.0-20200818021916-1f6d0dfd512e h1:URNpXdOxXAfuZ8wsr/DY27KTffVenKDjtNVAEwcR2Oo=
|
||||
github.com/gophish/gomail v0.0.0-20200818021916-1f6d0dfd512e/go.mod h1:JGlHttcLdDp3F4g8bPHqqQnUUDuB3poB4zLXozQ0xCY=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/csrf v1.6.2 h1:QqQ/OWwuFp4jMKgBFAzJVW3FMULdyUW7JoM4pEWuqKg=
|
||||
github.com/gorilla/csrf v1.6.2/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
|
||||
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
|
||||
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
|
||||
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
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/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible h1:d60x4RsAHk/UX/0OT8Gc6D7scVvhBbEANpTAWrDhA/I=
|
||||
github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
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/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 h1:mkl3tvPHIuPaWsLtmHTybJeoVEW7cbePK73Ir8VtruA=
|
||||
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/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.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
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/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U=
|
||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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=
|
||||
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/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
43
gophish.go
43
gophish.go
|
@ -26,7 +26,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|||
THE SOFTWARE.
|
||||
*/
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
|
@ -34,14 +36,25 @@ import (
|
|||
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/controllers"
|
||||
"github.com/gophish/gophish/dialer"
|
||||
"github.com/gophish/gophish/imap"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/webhook"
|
||||
)
|
||||
|
||||
const (
|
||||
modeAll string = "all"
|
||||
modeAdmin string = "admin"
|
||||
modePhish string = "phish"
|
||||
)
|
||||
|
||||
var (
|
||||
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()
|
||||
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() {
|
||||
|
@ -69,7 +82,14 @@ func main() {
|
|||
}
|
||||
config.Version = string(version)
|
||||
|
||||
err = log.Setup(conf)
|
||||
// Configure our various upstream clients to make sure that we restrict
|
||||
// outbound connections as needed.
|
||||
dialer.SetAllowedHosts(conf.AdminConf.AllowedInternalHosts)
|
||||
webhook.SetTransport(&http.Transport{
|
||||
DialContext: dialer.Dialer().DialContext,
|
||||
})
|
||||
|
||||
err = log.Setup(conf.Logging)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -80,6 +100,7 @@ func main() {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Unlock any maillogs that may have been locked for processing
|
||||
// when Gophish was last shutdown.
|
||||
err = models.UnlockAllMailLogs()
|
||||
|
@ -99,14 +120,26 @@ func main() {
|
|||
phishConfig := conf.PhishConf
|
||||
phishServer := controllers.NewPhishingServer(phishConfig)
|
||||
|
||||
go adminServer.Start()
|
||||
go phishServer.Start()
|
||||
imapMonitor := imap.NewMonitor()
|
||||
if *mode == "admin" || *mode == "all" {
|
||||
go adminServer.Start()
|
||||
go imapMonitor.Start()
|
||||
}
|
||||
if *mode == "phish" || *mode == "all" {
|
||||
go phishServer.Start()
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
<-c
|
||||
log.Info("CTRL+C Received... Gracefully shutting down servers")
|
||||
adminServer.Shutdown()
|
||||
phishServer.Shutdown()
|
||||
if *mode == modeAdmin || *mode == modeAll {
|
||||
adminServer.Shutdown()
|
||||
imapMonitor.Shutdown()
|
||||
}
|
||||
if *mode == modePhish || *mode == modeAll {
|
||||
phishServer.Shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
var gulp = require('gulp'),
|
||||
rename = require('gulp-rename'),
|
||||
concat = require('gulp-concat'),
|
||||
uglify = require('gulp-uglify'),
|
||||
uglify = require('gulp-uglify-es').default,
|
||||
cleanCSS = require('gulp-clean-css'),
|
||||
babel = require('gulp-babel'),
|
||||
|
||||
|
@ -61,6 +61,9 @@ scripts = function () {
|
|||
app_directory + 'settings.js',
|
||||
app_directory + 'templates.js',
|
||||
app_directory + 'gophish.js',
|
||||
app_directory + 'users.js',
|
||||
app_directory + 'webhooks.js',
|
||||
app_directory + 'passwords.js'
|
||||
])
|
||||
.pipe(rename({
|
||||
suffix: '.min'
|
||||
|
@ -97,4 +100,4 @@ exports.vendorjs = vendorjs
|
|||
exports.scripts = scripts
|
||||
exports.styles = styles
|
||||
exports.build = gulp.parallel(vendorjs, scripts, styles)
|
||||
exports.default = exports.build
|
||||
exports.default = exports.build
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
package imap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-message/charset"
|
||||
"github.com/gophish/gophish/dialer"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
|
||||
"github.com/jordan-wright/email"
|
||||
)
|
||||
|
||||
// Client interface for IMAP interactions
|
||||
type Client interface {
|
||||
Login(username, password string) (cmd *imap.Command, err error)
|
||||
Logout(timeout time.Duration) (cmd *imap.Command, err error)
|
||||
Select(name string, readOnly bool) (mbox *imap.MailboxStatus, err error)
|
||||
Store(seq *imap.SeqSet, item imap.StoreItem, value interface{}, ch chan *imap.Message) (err error)
|
||||
Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) (err error)
|
||||
}
|
||||
|
||||
// Email represents an email.Email with an included IMAP Sequence Number
|
||||
type Email struct {
|
||||
SeqNum uint32 `json:"seqnum"`
|
||||
*email.Email
|
||||
}
|
||||
|
||||
// Mailbox holds onto the credentials and other information
|
||||
// needed for connecting to an IMAP server.
|
||||
type Mailbox struct {
|
||||
Host string
|
||||
TLS bool
|
||||
IgnoreCertErrors bool
|
||||
User string
|
||||
Pwd string
|
||||
Folder string
|
||||
// Read only mode, false (original logic) if not initialized
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
// Validate validates supplied IMAP model by connecting to the server
|
||||
func Validate(s *models.IMAP) error {
|
||||
err := s.Validate()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.Host = s.Host + ":" + strconv.Itoa(int(s.Port)) // Append port
|
||||
mailServer := Mailbox{
|
||||
Host: s.Host,
|
||||
TLS: s.TLS,
|
||||
IgnoreCertErrors: s.IgnoreCertErrors,
|
||||
User: s.Username,
|
||||
Pwd: s.Password,
|
||||
Folder: s.Folder}
|
||||
|
||||
imapClient, err := mailServer.newClient()
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
} else {
|
||||
imapClient.Logout()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkAsUnread will set the UNSEEN flag on a supplied slice of SeqNums
|
||||
func (mbox *Mailbox) MarkAsUnread(seqs []uint32) error {
|
||||
imapClient, err := mbox.newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer imapClient.Logout()
|
||||
|
||||
seqSet := new(imap.SeqSet)
|
||||
seqSet.AddNum(seqs...)
|
||||
|
||||
item := imap.FormatFlagsOp(imap.RemoveFlags, true)
|
||||
err = imapClient.Store(seqSet, item, imap.SeenFlag, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// DeleteEmails will delete emails from the supplied slice of SeqNums
|
||||
func (mbox *Mailbox) DeleteEmails(seqs []uint32) error {
|
||||
imapClient, err := mbox.newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer imapClient.Logout()
|
||||
|
||||
seqSet := new(imap.SeqSet)
|
||||
seqSet.AddNum(seqs...)
|
||||
|
||||
item := imap.FormatFlagsOp(imap.AddFlags, true)
|
||||
err = imapClient.Store(seqSet, item, imap.DeletedFlag, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUnread will find all unread emails in the folder and return them as a list.
|
||||
func (mbox *Mailbox) GetUnread(markAsRead, delete bool) ([]Email, error) {
|
||||
imap.CharsetReader = charset.Reader
|
||||
var emails []Email
|
||||
|
||||
imapClient, err := mbox.newClient()
|
||||
if err != nil {
|
||||
return emails, fmt.Errorf("failed to create IMAP connection: %s", err)
|
||||
}
|
||||
|
||||
defer imapClient.Logout()
|
||||
|
||||
// Search for unread emails
|
||||
criteria := imap.NewSearchCriteria()
|
||||
criteria.WithoutFlags = []string{imap.SeenFlag}
|
||||
seqs, err := imapClient.Search(criteria)
|
||||
if err != nil {
|
||||
return emails, err
|
||||
}
|
||||
|
||||
if len(seqs) == 0 {
|
||||
return emails, nil
|
||||
}
|
||||
|
||||
seqset := new(imap.SeqSet)
|
||||
seqset.AddNum(seqs...)
|
||||
section := &imap.BodySectionName{}
|
||||
items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchInternalDate, section.FetchItem()}
|
||||
messages := make(chan *imap.Message)
|
||||
|
||||
go func() {
|
||||
if err := imapClient.Fetch(seqset, items, messages); err != nil {
|
||||
log.Error("Error fetching emails: ", err.Error()) // TODO: How to handle this, need to propogate error out
|
||||
}
|
||||
}()
|
||||
|
||||
// Step through each email
|
||||
for msg := range messages {
|
||||
// Extract raw message body. I can't find a better way to do this with the emersion library
|
||||
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
|
||||
tmp := string(buf)
|
||||
re := regexp.MustCompile(`\r`)
|
||||
tmp = re.ReplaceAllString(tmp, "")
|
||||
buf = []byte(tmp)
|
||||
|
||||
rawBodyStream := bytes.NewReader(buf)
|
||||
em, err = email.NewEmailFromReader(rawBodyStream) // Parse with @jordanwright's library
|
||||
if err != nil {
|
||||
return emails, err
|
||||
}
|
||||
|
||||
emtmp := Email{Email: em, SeqNum: msg.SeqNum} // Not sure why msg.Uid is always 0, so swapped to sequence numbers
|
||||
emails = append(emails, emtmp)
|
||||
|
||||
}
|
||||
return emails, nil
|
||||
}
|
||||
|
||||
// newClient will initiate a new IMAP connection with the given creds.
|
||||
func (mbox *Mailbox) newClient() (*client.Client, error) {
|
||||
var imapClient *client.Client
|
||||
var err error
|
||||
restrictedDialer := dialer.Dialer()
|
||||
if mbox.TLS {
|
||||
config := new(tls.Config)
|
||||
config.InsecureSkipVerify = mbox.IgnoreCertErrors
|
||||
imapClient, err = client.DialWithDialerTLS(restrictedDialer, mbox.Host, config)
|
||||
} else {
|
||||
imapClient, err = client.DialWithDialer(restrictedDialer, mbox.Host)
|
||||
}
|
||||
if err != nil {
|
||||
return imapClient, err
|
||||
}
|
||||
|
||||
err = imapClient.Login(mbox.User, mbox.Pwd)
|
||||
if err != nil {
|
||||
return imapClient, err
|
||||
}
|
||||
|
||||
_, err = imapClient.Select(mbox.Folder, mbox.ReadOnly)
|
||||
if err != nil {
|
||||
return imapClient, err
|
||||
}
|
||||
|
||||
return imapClient, nil
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
package imap
|
||||
|
||||
/* TODO:
|
||||
* - Have a counter per config for number of consecutive login errors and backoff (e.g if supplied creds are incorrect)
|
||||
* - Have a DB field "last_login_error" if last login failed
|
||||
* - DB counter for non-campaign emails that the admin should investigate
|
||||
* - Add field to User for numner of non-campaign emails reported
|
||||
*/
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/jordan-wright/email"
|
||||
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
// Pattern for GoPhish emails e.g ?rid=AbC1234
|
||||
// We include the optional quoted-printable 3D at the front, just in case decoding fails. e.g ?rid=3DAbC1234
|
||||
// We also include alternative URL encoded representations of '=' and '?' to handle Microsoft ATP URLs e.g %3Frid%3DAbC1234
|
||||
var goPhishRegex = regexp.MustCompile("((\\?|%3F)rid(=|%3D)(3D)?([A-Za-z0-9]{7}))")
|
||||
|
||||
// Monitor is a worker that monitors IMAP servers for reported campaign emails
|
||||
type Monitor struct {
|
||||
cancel func()
|
||||
}
|
||||
|
||||
// Monitor.start() checks for campaign emails
|
||||
// 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.
|
||||
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).
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
dbusers, err := models.GetUsers() //Slice of all user ids. Each user gets their own IMAP monitor routine.
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
break
|
||||
}
|
||||
for _, dbuser := range dbusers {
|
||||
if _, ok := usermap[dbuser.Id]; !ok { // If we don't currently have a running Go routine for this user, start one.
|
||||
log.Info("Starting new IMAP monitor for user ", dbuser.Username)
|
||||
usermap[dbuser.Id] = 1
|
||||
go monitor(dbuser.Id, ctx)
|
||||
}
|
||||
}
|
||||
time.Sleep(10 * time.Second) // Every ten seconds we check if a new user has been created
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
func monitor(uid int64, ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
// 1. Check if user exists, if not, return.
|
||||
_, err := models.GetUser(uid)
|
||||
if err != nil { // Not sure if there's a better way to determine user existence via id.
|
||||
log.Info("User ", uid, " seems to have been deleted. Stopping IMAP monitor for this user.")
|
||||
return
|
||||
}
|
||||
// 2. Check if user has IMAP settings.
|
||||
imapSettings, err := models.GetIMAP(uid)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
break
|
||||
}
|
||||
if len(imapSettings) > 0 {
|
||||
im := imapSettings[0]
|
||||
// 3. Check if IMAP is enabled
|
||||
if im.Enabled {
|
||||
log.Debug("Checking IMAP for user ", uid, ": ", im.Username, " -> ", im.Host)
|
||||
checkForNewEmails(im)
|
||||
time.Sleep((time.Duration(im.IMAPFreq) - 10) * time.Second) // Subtract 10 to compensate for the default sleep of 10 at the bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// NewMonitor returns a new instance of imap.Monitor
|
||||
func NewMonitor() *Monitor {
|
||||
im := &Monitor{}
|
||||
return im
|
||||
}
|
||||
|
||||
// Start launches the IMAP campaign monitor
|
||||
func (im *Monitor) Start() error {
|
||||
log.Info("Starting IMAP monitor manager")
|
||||
ctx, cancel := context.WithCancel(context.Background()) // ctx is the derivedContext
|
||||
im.cancel = cancel
|
||||
go im.start(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown attempts to gracefully shutdown the IMAP monitor.
|
||||
func (im *Monitor) Shutdown() error {
|
||||
log.Info("Shutting down IMAP monitor manager")
|
||||
im.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkForNewEmails logs into an IMAP account and checks unread emails
|
||||
// for the rid campaign identifier.
|
||||
func checkForNewEmails(im models.IMAP) {
|
||||
im.Host = im.Host + ":" + strconv.Itoa(int(im.Port)) // Append port
|
||||
mailServer := Mailbox{
|
||||
Host: im.Host,
|
||||
TLS: im.TLS,
|
||||
IgnoreCertErrors: im.IgnoreCertErrors,
|
||||
User: im.Username,
|
||||
Pwd: im.Password,
|
||||
Folder: im.Folder,
|
||||
}
|
||||
|
||||
msgs, err := mailServer.GetUnread(true, false)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
// Update last_succesful_login here via im.Host
|
||||
err = models.SuccessfulLogin(&im)
|
||||
|
||||
if len(msgs) > 0 {
|
||||
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 deleteEmails []uint32 // SeqNums of campaign emails. If DeleteReportedCampaignEmail is true, we will delete these
|
||||
for _, m := range msgs {
|
||||
// Check if sender is from company's domain, if enabled. TODO: Make this an IMAP filter
|
||||
if im.RestrictDomain != "" { // e.g domainResitct = widgets.com
|
||||
splitEmail := strings.Split(m.Email.From, "@")
|
||||
senderDomain := splitEmail[len(splitEmail)-1]
|
||||
if senderDomain != im.RestrictDomain {
|
||||
log.Debug("Ignoring email as not from company domain: ", senderDomain)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
rids, err := matchEmail(m.Email) // Search email Text, HTML, and each attachment for rid parameters
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Error searching email for rids from user '%s': %s", m.Email.From, err.Error())
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
log.Debugf("Marking %d emails as unread as failed to report", len(reportingFailed))
|
||||
err := mailServer.MarkAsUnread(reportingFailed) // Set emails as unread that we failed to report to GoPhish
|
||||
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 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 {
|
||||
log.Debug("No new emails for ", im.Username)
|
||||
}
|
||||
}
|
||||
|
||||
func checkRIDs(em *email.Email, rids map[string]bool) {
|
||||
// Check Text and HTML
|
||||
emailContent := string(em.Text) + string(em.HTML)
|
||||
for _, r := range goPhishRegex.FindAllStringSubmatch(emailContent, -1) {
|
||||
newrid := r[len(r)-1]
|
||||
if !rids[newrid] {
|
||||
rids[newrid] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
ext := filepath.Ext(a.Filename)
|
||||
if a.Header.Get("Content-Type") == "message/rfc822" || ext == ".eml" {
|
||||
|
||||
// Let's decode the email
|
||||
rawBodyStream := bytes.NewReader(a.Content)
|
||||
attachmentEmail, err := email.NewEmailFromReader(rawBodyStream)
|
||||
if err != nil {
|
||||
return rids, err
|
||||
}
|
||||
|
||||
checkRIDs(attachmentEmail, rids)
|
||||
}
|
||||
}
|
||||
|
||||
return rids, nil
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -12,16 +12,34 @@ import (
|
|||
// It is exported here for use with gorm.
|
||||
var Logger *logrus.Logger
|
||||
|
||||
// ErrInvalidLevel is returned when an invalid log level is given in the config
|
||||
var ErrInvalidLevel = errors.New("invalid log level")
|
||||
|
||||
// Config represents configuration details for logging.
|
||||
type Config struct {
|
||||
Filename string `json:"filename"`
|
||||
Level string `json:"level"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
Logger = logrus.New()
|
||||
Logger.Formatter = &logrus.TextFormatter{DisableColors: true}
|
||||
}
|
||||
|
||||
// Setup configures the logger based on options in the config.json.
|
||||
func Setup(conf *config.Config) error {
|
||||
Logger.SetLevel(logrus.InfoLevel)
|
||||
func Setup(config *Config) error {
|
||||
var err error
|
||||
// Set up logging level
|
||||
level := logrus.InfoLevel
|
||||
if config.Level != "" {
|
||||
level, err = logrus.ParseLevel(config.Level)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
Logger.SetLevel(level)
|
||||
// Set up logging to a file if specified in the config
|
||||
logFile := conf.Logging.Filename
|
||||
logFile := config.Filename
|
||||
if logFile != "" {
|
||||
f, err := os.OpenFile(logFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package logger
|
||||
|
||||
import "testing"
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
func TestLogLevel(t *testing.T) {
|
||||
tests := map[string]logrus.Level{
|
||||
"": logrus.InfoLevel,
|
||||
"debug": logrus.DebugLevel,
|
||||
"info": logrus.InfoLevel,
|
||||
"error": logrus.ErrorLevel,
|
||||
"fatal": logrus.FatalLevel,
|
||||
}
|
||||
config := &Config{}
|
||||
for level, expected := range tests {
|
||||
config.Level = level
|
||||
err := Setup(config)
|
||||
if err != nil {
|
||||
t.Fatalf("error setting logging level %v", err)
|
||||
}
|
||||
if Logger.Level != expected {
|
||||
t.Fatalf("invalid logging level. expected %v got %v", expected, Logger.Level)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,6 +55,7 @@ type Mail interface {
|
|||
Success() error
|
||||
Generate(msg *gomail.Message) error
|
||||
GetDialer() (Dialer, error)
|
||||
GetSmtpFrom() (string, error)
|
||||
}
|
||||
|
||||
// MailWorker is the worker that receives slices of emails
|
||||
|
@ -160,7 +161,14 @@ func sendMail(ctx context.Context, dialer Dialer, ms []Mail) {
|
|||
m.Error(err)
|
||||
continue
|
||||
}
|
||||
err = gomail.Send(sender, message)
|
||||
|
||||
smtp_from, err := m.GetSmtpFrom()
|
||||
if err != nil {
|
||||
m.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = gomail.SendCustomFrom(sender, smtp_from, message)
|
||||
if err != nil {
|
||||
if te, ok := err.(*textproto.Error); ok {
|
||||
switch {
|
||||
|
@ -215,7 +223,9 @@ func sendMail(ctx context.Context, dialer Dialer, ms []Mail) {
|
|||
}
|
||||
}
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": message.GetHeader("To")[0],
|
||||
"smtp_from": smtp_from,
|
||||
"envelope_from": message.GetHeader("From")[0],
|
||||
"email": message.GetHeader("To")[0],
|
||||
}).Info("Email sent")
|
||||
m.Success()
|
||||
}
|
||||
|
|
|
@ -8,14 +8,8 @@ import (
|
|||
"net/textproto"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type MailerSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func generateMessages(dialer Dialer) []Mail {
|
||||
to := []string{"to@example.com"}
|
||||
|
||||
|
@ -47,30 +41,30 @@ func newMockErrorSender(err error) *mockSender {
|
|||
return sender
|
||||
}
|
||||
|
||||
func (ms *MailerSuite) TestDialHost() {
|
||||
func TestDialHost(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
md := newMockDialer()
|
||||
md.setDial(md.unreachableDial)
|
||||
_, err := dialHost(ctx, md)
|
||||
if _, ok := err.(*ErrMaxConnectAttempts); !ok {
|
||||
ms.T().Fatalf("Didn't receive expected ErrMaxConnectAttempts. Got: %s", err)
|
||||
t.Fatalf("Didn't receive expected ErrMaxConnectAttempts. Got: %s", err)
|
||||
}
|
||||
e := err.(*ErrMaxConnectAttempts)
|
||||
if e.underlyingError != errHostUnreachable {
|
||||
ms.T().Fatalf("Got invalid underlying error. Expected %s Got %s\n", e.underlyingError, errHostUnreachable)
|
||||
t.Fatalf("Got invalid underlying error. Expected %s Got %s\n", e.underlyingError, errHostUnreachable)
|
||||
}
|
||||
if md.dialCount != MaxReconnectAttempts {
|
||||
ms.T().Fatalf("Unexpected number of reconnect attempts. Expected %d, Got %d", MaxReconnectAttempts, md.dialCount)
|
||||
t.Fatalf("Unexpected number of reconnect attempts. Expected %d, Got %d", MaxReconnectAttempts, md.dialCount)
|
||||
}
|
||||
md.setDial(md.defaultDial)
|
||||
_, err = dialHost(ctx, md)
|
||||
if err != nil {
|
||||
ms.T().Fatalf("Unexpected error when dialing the mock host: %s", err)
|
||||
t.Fatalf("Unexpected error when dialing the mock host: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MailerSuite) TestMailWorkerStart() {
|
||||
func TestMailWorkerStart(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
@ -97,16 +91,16 @@ func (ms *MailerSuite) TestMailWorkerStart() {
|
|||
got = append(got, message)
|
||||
original := messages[idx].(*mockMessage)
|
||||
if original.from != message.from {
|
||||
ms.T().Fatalf("Invalid message received. Expected %s, Got %s", original.from, message.from)
|
||||
t.Fatalf("Invalid message received. Expected %s, Got %s", original.from, message.from)
|
||||
}
|
||||
idx++
|
||||
}
|
||||
if len(got) != len(messages) {
|
||||
ms.T().Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), len(messages))
|
||||
t.Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), len(messages))
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MailerSuite) TestBackoff() {
|
||||
func TestBackoff(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
@ -139,28 +133,28 @@ func (ms *MailerSuite) TestBackoff() {
|
|||
// Check that we only sent one message
|
||||
expectedCount := 1
|
||||
if len(got) != expectedCount {
|
||||
ms.T().Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||
t.Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||
}
|
||||
|
||||
// Check that it's the correct message
|
||||
originalFrom := messages[1].(*mockMessage).from
|
||||
if got[0].from != originalFrom {
|
||||
ms.T().Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||
t.Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||
}
|
||||
|
||||
// Check that the first message performed a backoff
|
||||
backoffCount := messages[0].(*mockMessage).backoffCount
|
||||
if backoffCount != expectedCount {
|
||||
ms.T().Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedCount)
|
||||
t.Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedCount)
|
||||
}
|
||||
|
||||
// Check that there was a reset performed on the sender
|
||||
if sender.resetCount != expectedCount {
|
||||
ms.T().Fatalf("Did not receive expected reset. Got resetCount %d, expected %d", sender.resetCount, expectedCount)
|
||||
t.Fatalf("Did not receive expected reset. Got resetCount %d, expected %d", sender.resetCount, expectedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MailerSuite) TestPermError() {
|
||||
func TestPermError(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
@ -193,13 +187,13 @@ func (ms *MailerSuite) TestPermError() {
|
|||
// Check that we only sent one message
|
||||
expectedCount := 1
|
||||
if len(got) != expectedCount {
|
||||
ms.T().Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||
t.Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||
}
|
||||
|
||||
// Check that it's the correct message
|
||||
originalFrom := messages[1].(*mockMessage).from
|
||||
if got[0].from != originalFrom {
|
||||
ms.T().Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||
t.Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||
}
|
||||
|
||||
message := messages[0].(*mockMessage)
|
||||
|
@ -208,21 +202,21 @@ func (ms *MailerSuite) TestPermError() {
|
|||
expectedBackoffCount := 0
|
||||
backoffCount := message.backoffCount
|
||||
if backoffCount != expectedBackoffCount {
|
||||
ms.T().Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedCount)
|
||||
t.Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedCount)
|
||||
}
|
||||
|
||||
// Check that there was a reset performed on the sender
|
||||
if sender.resetCount != expectedCount {
|
||||
ms.T().Fatalf("Did not receive expected reset. Got resetCount %d, expected %d", sender.resetCount, expectedCount)
|
||||
t.Fatalf("Did not receive expected reset. Got resetCount %d, expected %d", sender.resetCount, expectedCount)
|
||||
}
|
||||
|
||||
// Check that the email errored out appropriately
|
||||
if !reflect.DeepEqual(message.err, expectedError) {
|
||||
ms.T().Fatalf("Did not received expected error. Got %#v\nExpected %#v", message.err, expectedError)
|
||||
t.Fatalf("Did not received expected error. Got %#v\nExpected %#v", message.err, expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MailerSuite) TestUnknownError() {
|
||||
func TestUnknownError(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
|
@ -252,13 +246,13 @@ func (ms *MailerSuite) TestUnknownError() {
|
|||
// Check that we only sent one message
|
||||
expectedCount := 1
|
||||
if len(got) != expectedCount {
|
||||
ms.T().Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||
t.Fatalf("Unexpected number of messages received. Expected %d Got %d", len(got), expectedCount)
|
||||
}
|
||||
|
||||
// Check that it's the correct message
|
||||
originalFrom := messages[1].(*mockMessage).from
|
||||
if got[0].from != originalFrom {
|
||||
ms.T().Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||
t.Fatalf("Invalid message received. Expected %s, Got %s", originalFrom, got[0].from)
|
||||
}
|
||||
|
||||
message := messages[0].(*mockMessage)
|
||||
|
@ -271,21 +265,17 @@ func (ms *MailerSuite) TestUnknownError() {
|
|||
expectedBackoffCount := 1
|
||||
backoffCount := message.backoffCount
|
||||
if backoffCount != expectedBackoffCount {
|
||||
ms.T().Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedBackoffCount)
|
||||
t.Fatalf("Did not receive expected backoff. Got backoffCount %d, Expected %d", backoffCount, expectedBackoffCount)
|
||||
}
|
||||
|
||||
// Check that the underlying connection was reestablished
|
||||
expectedDialCount := 2
|
||||
if dialer.dialCount != expectedDialCount {
|
||||
ms.T().Fatalf("Did not receive expected dial count. Got %d expected %d", dialer.dialCount, expectedDialCount)
|
||||
t.Fatalf("Did not receive expected dial count. Got %d expected %d", dialer.dialCount, expectedDialCount)
|
||||
}
|
||||
|
||||
// Check that the email errored out appropriately
|
||||
if !reflect.DeepEqual(message.err, expectedError) {
|
||||
ms.T().Fatalf("Did not received expected error. Got %#v\nExpected %#v", message.err, expectedError)
|
||||
t.Fatalf("Did not received expected error. Got %#v\nExpected %#v", message.err, expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailerSuite(t *testing.T) {
|
||||
suite.Run(t, new(MailerSuite))
|
||||
}
|
||||
|
|
|
@ -13,11 +13,6 @@ import (
|
|||
// being 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
|
||||
type mockDialer struct {
|
||||
dialCount int
|
||||
|
@ -137,10 +132,6 @@ func (mm *mockMessage) defaultDialer() (Dialer, error) {
|
|||
return newMockDialer(), nil
|
||||
}
|
||||
|
||||
func (mm *mockMessage) errorDialer() (Dialer, error) {
|
||||
return nil, errDialerUnavailable
|
||||
}
|
||||
|
||||
func (mm *mockMessage) GetDialer() (Dialer, error) {
|
||||
return mm.getdialer()
|
||||
}
|
||||
|
@ -171,6 +162,10 @@ func (mm *mockMessage) Generate(message *gomail.Message) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (mm *mockMessage) GetSmtpFrom() (string, error) {
|
||||
return mm.from, nil
|
||||
}
|
||||
|
||||
func (mm *mockMessage) Success() error {
|
||||
mm.finished = true
|
||||
return nil
|
||||
|
|
|
@ -77,7 +77,7 @@ func RequireAPIKey(handler http.Handler) http.Handler {
|
|||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
|
||||
w.Header().Set("Access-Control-Max-Age", "1000")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
|
||||
return
|
||||
|
@ -114,13 +114,21 @@ func RequireAPIKey(handler http.Handler) http.Handler {
|
|||
func RequireLogin(handler http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
q.Set("next", r.URL.Path)
|
||||
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,6 +176,17 @@ func RequirePermission(perm string) func(http.Handler) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// ApplySecurityHeaders applies various security headers according to best-
|
||||
// practices.
|
||||
func ApplySecurityHeaders(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
csp := "frame-ancestors 'none';"
|
||||
w.Header().Set("Content-Security-Policy", csp)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// JSONError returns an error in JSON format with the given
|
||||
// status code and message
|
||||
func JSONError(w http.ResponseWriter, c int, m string) {
|
||||
|
|
|
@ -9,19 +9,17 @@ import (
|
|||
"github.com/gophish/gophish/config"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
var successHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("success"))
|
||||
})
|
||||
|
||||
type MiddlewareSuite struct {
|
||||
suite.Suite
|
||||
type testContext struct {
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) SetupSuite() {
|
||||
func setupTest(t *testing.T) *testContext {
|
||||
conf := &config.Config{
|
||||
DBName: "sqlite3",
|
||||
DBPath: ":memory:",
|
||||
|
@ -29,12 +27,16 @@ func (s *MiddlewareSuite) SetupSuite() {
|
|||
}
|
||||
err := models.Setup(conf)
|
||||
if err != nil {
|
||||
s.T().Fatalf("Failed creating database: %v", err)
|
||||
t.Fatalf("Failed creating database: %v", err)
|
||||
}
|
||||
// Get the API key to use for these tests
|
||||
u, err := models.GetUser(1)
|
||||
s.Nil(err)
|
||||
s.apiKey = u.ApiKey
|
||||
if err != nil {
|
||||
t.Fatalf("error getting user: %v", err)
|
||||
}
|
||||
ctx := &testContext{}
|
||||
ctx.apiKey = u.ApiKey
|
||||
return ctx
|
||||
}
|
||||
|
||||
// MiddlewarePermissionTest maps an expected HTTP Method to an expected HTTP
|
||||
|
@ -43,7 +45,8 @@ type MiddlewarePermissionTest map[string]int
|
|||
|
||||
// TestEnforceViewOnly ensures that only users with the ModifyObjects
|
||||
// permission have the ability to send non-GET requests.
|
||||
func (s *MiddlewareSuite) TestEnforceViewOnly() {
|
||||
func TestEnforceViewOnly(t *testing.T) {
|
||||
setupTest(t)
|
||||
permissionTests := map[string]MiddlewarePermissionTest{
|
||||
models.RoleAdmin: MiddlewarePermissionTest{
|
||||
http.MethodGet: http.StatusOK,
|
||||
|
@ -64,7 +67,9 @@ func (s *MiddlewareSuite) TestEnforceViewOnly() {
|
|||
}
|
||||
for r, checks := range permissionTests {
|
||||
role, err := models.GetRoleBySlug(r)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting role by slug: %v", err)
|
||||
}
|
||||
|
||||
for method, expected := range checks {
|
||||
req := httptest.NewRequest(method, "/", nil)
|
||||
|
@ -76,12 +81,16 @@ func (s *MiddlewareSuite) TestEnforceViewOnly() {
|
|||
})
|
||||
|
||||
EnforceViewOnly(successHandler).ServeHTTP(response, req)
|
||||
s.Equal(response.Code, expected)
|
||||
got := response.Code
|
||||
if got != expected {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestRequirePermission() {
|
||||
func TestRequirePermission(t *testing.T) {
|
||||
setupTest(t)
|
||||
middleware := RequirePermission(models.PermissionModifySystem)
|
||||
handler := middleware(successHandler)
|
||||
|
||||
|
@ -95,26 +104,49 @@ func (s *MiddlewareSuite) TestRequirePermission() {
|
|||
response := httptest.NewRecorder()
|
||||
// Test that with the requested permission, the request succeeds
|
||||
role, err := models.GetRoleBySlug(role)
|
||||
s.Nil(err)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting role by slug: %v", err)
|
||||
}
|
||||
req = ctx.Set(req, "user", models.User{
|
||||
Role: role,
|
||||
RoleID: role.ID,
|
||||
})
|
||||
handler.ServeHTTP(response, req)
|
||||
s.Equal(response.Code, expected)
|
||||
got := response.Code
|
||||
if got != expected {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestRequireAPIKey() {
|
||||
func TestRequireAPIKey(t *testing.T) {
|
||||
setupTest(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
// Test that making a request without an API key is denied
|
||||
RequireAPIKey(successHandler).ServeHTTP(response, req)
|
||||
s.Equal(response.Code, http.StatusUnauthorized)
|
||||
expected := http.StatusUnauthorized
|
||||
got := response.Code
|
||||
if got != expected {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestInvalidAPIKey() {
|
||||
func TestCORSHeaders(t *testing.T) {
|
||||
setupTest(t)
|
||||
req := httptest.NewRequest(http.MethodOptions, "/", nil)
|
||||
response := httptest.NewRecorder()
|
||||
RequireAPIKey(successHandler).ServeHTTP(response, req)
|
||||
expected := "POST, GET, OPTIONS, PUT, DELETE"
|
||||
got := response.Result().Header.Get("Access-Control-Allow-Methods")
|
||||
if got != expected {
|
||||
t.Fatalf("incorrect cors options received. expected %s got %s", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidAPIKey(t *testing.T) {
|
||||
setupTest(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
query := req.URL.Query()
|
||||
query.Set("api_key", "bogus-api-key")
|
||||
|
@ -122,18 +154,58 @@ func (s *MiddlewareSuite) TestInvalidAPIKey() {
|
|||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
RequireAPIKey(successHandler).ServeHTTP(response, req)
|
||||
s.Equal(response.Code, http.StatusUnauthorized)
|
||||
expected := http.StatusUnauthorized
|
||||
got := response.Code
|
||||
if got != expected {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestBearerToken() {
|
||||
func TestBearerToken(t *testing.T) {
|
||||
testCtx := setupTest(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.apiKey))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", testCtx.apiKey))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
RequireAPIKey(successHandler).ServeHTTP(response, req)
|
||||
s.Equal(response.Code, http.StatusOK)
|
||||
expected := http.StatusOK
|
||||
got := response.Code
|
||||
if got != expected {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareSuite(t *testing.T) {
|
||||
suite.Run(t, new(MiddlewareSuite))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplySecurityHeaders(t *testing.T) {
|
||||
expected := map[string]string{
|
||||
"Content-Security-Policy": "frame-ancestors 'none';",
|
||||
"X-Frame-Options": "DENY",
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
response := httptest.NewRecorder()
|
||||
ApplySecurityHeaders(successHandler).ServeHTTP(response, req)
|
||||
for header, value := range expected {
|
||||
got := response.Header().Get(header)
|
||||
if got != value {
|
||||
t.Fatalf("incorrect security header received for %s: expected %s got %s", header, value, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,143 @@
|
|||
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.Since(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 {
|
||||
clientIP = r.RemoteAddr
|
||||
}
|
||||
if r.Method == http.MethodPost && !limiter.allow(clientIP) {
|
||||
log.Error("")
|
||||
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -1,11 +1,156 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Attachment contains the fields and methods for
|
||||
// an email attachment
|
||||
type Attachment struct {
|
||||
Id int64 `json:"-"`
|
||||
TemplateId int64 `json:"-"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Id int64 `json:"-"`
|
||||
TemplateId int64 `json:"-"`
|
||||
Content string `json:"content"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
vanillaFile bool // Vanilla file has no template variables
|
||||
}
|
||||
|
||||
// Validate ensures that the provided attachment uses the supported template variables correctly.
|
||||
func (a Attachment) Validate() error {
|
||||
vc := ValidationContext{
|
||||
FromAddress: "foo@bar.com",
|
||||
BaseURL: "http://example.com",
|
||||
}
|
||||
td := Result{
|
||||
BaseRecipient: BaseRecipient{
|
||||
Email: "foo@bar.com",
|
||||
FirstName: "Foo",
|
||||
LastName: "Bar",
|
||||
Position: "Test",
|
||||
},
|
||||
RId: "123456",
|
||||
}
|
||||
ptx, err := NewPhishingTemplateContext(vc, td.BaseRecipient, td.RId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = a.ApplyTemplate(ptx)
|
||||
return err
|
||||
}
|
||||
|
||||
// ApplyTemplate parses different attachment files and applies the supplied phishing template.
|
||||
func (a *Attachment) ApplyTemplate(ptx PhishingTemplateContext) (io.Reader, error) {
|
||||
|
||||
decodedAttachment := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
|
||||
|
||||
// If we've already determined there are no template variables in this attachment return it immediately
|
||||
if a.vanillaFile == true {
|
||||
return decodedAttachment, nil
|
||||
}
|
||||
|
||||
// Decided to use the file extension rather than the content type, as there seems to be quite
|
||||
// a bit of variability with types. e.g sometimes a Word docx file would have:
|
||||
// "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
fileExtension := filepath.Ext(a.Name)
|
||||
|
||||
switch fileExtension {
|
||||
|
||||
case ".docx", ".docm", ".pptx", ".xlsx", ".xlsm":
|
||||
// Most modern office formats are xml based and can be unarchived.
|
||||
// .docm and .xlsm files are comprised of xml, and a binary blob for the macro code
|
||||
|
||||
// Zip archives require random access for reading, so it's hard to stream bytes. Solution seems to be to use a buffer.
|
||||
// See https://stackoverflow.com/questions/16946978/how-to-unzip-io-readcloser
|
||||
b := new(bytes.Buffer)
|
||||
b.ReadFrom(decodedAttachment)
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(b.Bytes()), int64(b.Len())) // Create a new zip reader from the file
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newZipArchive := new(bytes.Buffer)
|
||||
zipWriter := zip.NewWriter(newZipArchive) // For writing the new archive
|
||||
|
||||
// i. Read each file from the Word document archive
|
||||
// ii. Apply the template to it
|
||||
// iii. Add the templated content to a new zip Word archive
|
||||
a.vanillaFile = true
|
||||
for _, zipFile := range zipReader.File {
|
||||
ff, err := zipFile.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer ff.Close()
|
||||
contents, err := ioutil.ReadAll(ff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subFileExtension := filepath.Ext(zipFile.Name)
|
||||
var tFile string
|
||||
if subFileExtension == ".xml" || subFileExtension == ".rels" { // Ignore other files, e.g binary ones and images
|
||||
// First we look for instances where Word has URL escaped our template variables. This seems to happen when inserting a remote image, converting {{.Foo}} to %7b%7b.foo%7d%7d.
|
||||
// See https://stackoverflow.com/questions/68287630/disable-url-encoding-for-includepicture-in-microsoft-word
|
||||
rx, _ := regexp.Compile("%7b%7b.([a-zA-Z]+)%7d%7d")
|
||||
contents := rx.ReplaceAllFunc(contents, func(m []byte) []byte {
|
||||
d, err := url.QueryUnescape(string(m))
|
||||
if err != nil {
|
||||
return m
|
||||
}
|
||||
return []byte(d)
|
||||
})
|
||||
|
||||
// For each file apply the template.
|
||||
tFile, err = ExecuteTemplate(string(contents), ptx)
|
||||
if err != nil {
|
||||
zipWriter.Close() // Don't use defer when writing files https://www.joeshaw.org/dont-defer-close-on-writable-files/
|
||||
return nil, err
|
||||
}
|
||||
// Check if the subfile changed. We only need this to be set once to know in the future to check the 'parent' file
|
||||
if tFile != string(contents) {
|
||||
a.vanillaFile = false
|
||||
}
|
||||
} else {
|
||||
tFile = string(contents) // Could move this to the declaration of tFile, but might be confusing to read
|
||||
}
|
||||
// Write new Word archive
|
||||
newZipFile, err := zipWriter.Create(zipFile.Name)
|
||||
if err != nil {
|
||||
zipWriter.Close() // Don't use defer when writing files https://www.joeshaw.org/dont-defer-close-on-writable-files/
|
||||
return nil, err
|
||||
}
|
||||
_, err = newZipFile.Write([]byte(tFile))
|
||||
if err != nil {
|
||||
zipWriter.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
zipWriter.Close()
|
||||
return bytes.NewReader(newZipArchive.Bytes()), err
|
||||
|
||||
case ".txt", ".html", ".ics":
|
||||
b, err := ioutil.ReadAll(decodedAttachment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processedAttachment, err := ExecuteTemplate(string(b), ptx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if processedAttachment == string(b) {
|
||||
a.vanillaFile = true
|
||||
}
|
||||
return strings.NewReader(processedAttachment), nil
|
||||
default:
|
||||
return decodedAttachment, nil // Default is to simply return the file
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func (s *ModelsSuite) TestAttachment(c *check.C) {
|
||||
ptx := PhishingTemplateContext{
|
||||
BaseRecipient: BaseRecipient{
|
||||
FirstName: "Foo",
|
||||
LastName: "Bar",
|
||||
Email: "foo@bar.com",
|
||||
Position: "Space Janitor",
|
||||
},
|
||||
BaseURL: "http://testurl.com",
|
||||
URL: "http://testurl.com/?rid=1234567",
|
||||
TrackingURL: "http://testurl.local/track?rid=1234567",
|
||||
Tracker: "<img alt='' style='display: none' src='http://testurl.local/track?rid=1234567'/>",
|
||||
From: "From Address",
|
||||
RId: "1234567",
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir("testdata")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open attachment folder 'testdata': %v\n", err)
|
||||
}
|
||||
for _, ff := range files {
|
||||
if !ff.IsDir() && !strings.Contains(ff.Name(), "templated") {
|
||||
fname := ff.Name()
|
||||
fmt.Printf("Checking attachment file -> %s\n", fname)
|
||||
data := readFile("testdata/" + fname)
|
||||
if filepath.Ext(fname) == ".b64" {
|
||||
fname = fname[:len(fname)-4]
|
||||
}
|
||||
a := Attachment{
|
||||
Content: data,
|
||||
Name: fname,
|
||||
}
|
||||
t, err := a.ApplyTemplate(ptx)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(a.vanillaFile, check.Equals, strings.Contains(fname, "without-vars"))
|
||||
c.Assert(a.vanillaFile, check.Not(check.Equals), strings.Contains(fname, "with-vars"))
|
||||
|
||||
// Verfify template was applied as expected
|
||||
tt, err := ioutil.ReadAll(t)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse templated file '%s': %v\n", fname, err)
|
||||
}
|
||||
templatedFile := base64.StdEncoding.EncodeToString(tt)
|
||||
expectedOutput := readFile("testdata/" + strings.TrimSuffix(ff.Name(), filepath.Ext(ff.Name())) + ".templated" + filepath.Ext(ff.Name())) // e.g text-file-with-vars.templated.txt
|
||||
c.Assert(templatedFile, check.Equals, expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readFile(fname string) string {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open file '%s': %v\n", fname, err)
|
||||
}
|
||||
reader := bufio.NewReader(f)
|
||||
content, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read file '%s': %v\n", fname, err)
|
||||
}
|
||||
data := ""
|
||||
if filepath.Ext(fname) == ".b64" {
|
||||
data = string(content)
|
||||
} else {
|
||||
data = base64.StdEncoding.EncodeToString(content)
|
||||
}
|
||||
return data
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/webhook"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -26,7 +27,7 @@ type Campaign struct {
|
|||
Status string `json:"status"`
|
||||
Results []Result `json:"results,omitempty"`
|
||||
Groups []Group `json:"groups,omitempty"`
|
||||
Events []Event `json:"timeline,omitemtpy"`
|
||||
Events []Event `json:"timeline,omitempty"`
|
||||
SMTPId int64 `json:"-"`
|
||||
SMTP SMTP `json:"smtp"`
|
||||
URL string `json:"url"`
|
||||
|
@ -74,7 +75,7 @@ type CampaignStats struct {
|
|||
// that occurs during the campaign
|
||||
type Event struct {
|
||||
Id int64 `json:"-"`
|
||||
CampaignId int64 `json:"-"`
|
||||
CampaignId int64 `json:"campaign_id"`
|
||||
Email string `json:"email"`
|
||||
Time time.Time `json:"time"`
|
||||
Message string `json:"message"`
|
||||
|
@ -154,9 +155,24 @@ func (c *Campaign) UpdateStatus(s string) error {
|
|||
}
|
||||
|
||||
// AddEvent creates a new campaign event in the database
|
||||
func (c *Campaign) AddEvent(e *Event) error {
|
||||
e.CampaignId = c.Id
|
||||
func AddEvent(e *Event, campaignID int64) error {
|
||||
e.CampaignId = campaignID
|
||||
e.Time = time.Now().UTC()
|
||||
|
||||
whs, err := GetActiveWebhooks()
|
||||
if err == nil {
|
||||
whEndPoints := []webhook.EndPoint{}
|
||||
for _, wh := range whs {
|
||||
whEndPoints = append(whEndPoints, webhook.EndPoint{
|
||||
URL: wh.URL,
|
||||
Secret: wh.Secret,
|
||||
})
|
||||
}
|
||||
webhook.SendAll(whEndPoints, e)
|
||||
} else {
|
||||
log.Errorf("error getting active webhooks: %v", err)
|
||||
}
|
||||
|
||||
return db.Save(e).Error
|
||||
}
|
||||
|
||||
|
@ -308,7 +324,7 @@ func GetCampaignSummaries(uid int64) (CampaignSummaries, error) {
|
|||
cs := []CampaignSummary{}
|
||||
// Get the basic campaign information
|
||||
query := db.Table("campaigns").Where("user_id = ?", uid)
|
||||
query = query.Select("id, name, created_date, launch_date, completed_date, status")
|
||||
query = query.Select("id, name, created_date, launch_date, send_by_date, completed_date, status")
|
||||
err := query.Scan(&cs).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -331,7 +347,7 @@ func GetCampaignSummaries(uid int64) (CampaignSummaries, error) {
|
|||
func GetCampaignSummary(id int64, uid int64) (CampaignSummary, error) {
|
||||
cs := CampaignSummary{}
|
||||
query := db.Table("campaigns").Where("user_id = ? AND id = ?", uid, id)
|
||||
query = query.Select("id, name, created_date, launch_date, completed_date, status")
|
||||
query = query.Select("id, name, created_date, launch_date, send_by_date, completed_date, status")
|
||||
err := query.Scan(&cs).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -346,6 +362,38 @@ func GetCampaignSummary(id int64, uid int64) (CampaignSummary, error) {
|
|||
return cs, nil
|
||||
}
|
||||
|
||||
// GetCampaignMailContext returns a campaign object with just the relevant
|
||||
// data needed to generate and send emails. This includes the top-level
|
||||
// metadata, the template, and the sending profile.
|
||||
//
|
||||
// This should only ever be used if you specifically want this lightweight
|
||||
// context, since it returns a non-standard campaign object.
|
||||
// ref: #1726
|
||||
func GetCampaignMailContext(id int64, uid int64) (Campaign, error) {
|
||||
c := Campaign{}
|
||||
err := db.Where("id = ?", id).Where("user_id = ?", uid).Find(&c).Error
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
err = db.Table("smtp").Where("id=?", c.SMTPId).Find(&c.SMTP).Error
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
err = db.Where("smtp_id=?", c.SMTP.Id).Find(&c.SMTP.Headers).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return c, err
|
||||
}
|
||||
err = db.Table("templates").Where("id=?", c.TemplateId).Find(&c.Template).Error
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
err = db.Where("template_id=?", c.Template.Id).Find(&c.Template.Attachments).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return c, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetCampaign returns the campaign, if it exists, specified by the given id and user_id.
|
||||
func GetCampaign(id int64, uid int64) (Campaign, error) {
|
||||
c := Campaign{}
|
||||
|
@ -443,7 +491,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
t, err := GetTemplateByName(c.Template.Name, uid)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"template": t.Name,
|
||||
"template": c.Template.Name,
|
||||
}).Error("Template does not exist")
|
||||
return ErrTemplateNotFound
|
||||
} else if err != nil {
|
||||
|
@ -456,7 +504,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
p, err := GetPageByName(c.Page.Name, uid)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"page": p.Name,
|
||||
"page": c.Page.Name,
|
||||
}).Error("Page does not exist")
|
||||
return ErrPageNotFound
|
||||
} else if err != nil {
|
||||
|
@ -469,7 +517,7 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
s, err := GetSMTPByName(c.SMTP.Name, uid)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"smtp": s.Name,
|
||||
"smtp": c.SMTP.Name,
|
||||
}).Error("Sending profile does not exist")
|
||||
return ErrSMTPNotFound
|
||||
} else if err != nil {
|
||||
|
@ -484,13 +532,14 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
err = c.AddEvent(&Event{Message: "Campaign Created"})
|
||||
err = AddEvent(&Event{Message: "Campaign Created"}, c.Id)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
// Insert all the results
|
||||
resultMap := make(map[string]bool)
|
||||
recipientIndex := 0
|
||||
tx := db.Begin()
|
||||
for _, g := range c.Groups {
|
||||
// Insert a result for each target in the group
|
||||
for _, t := range g.Targets {
|
||||
|
@ -515,24 +564,30 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
Reported: false,
|
||||
ModifiedDate: c.CreatedDate,
|
||||
}
|
||||
err = r.GenerateId()
|
||||
err = r.GenerateId(tx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
processing := false
|
||||
if r.SendDate.Before(c.CreatedDate) || r.SendDate.Equal(c.CreatedDate) {
|
||||
r.Status = StatusSending
|
||||
processing = true
|
||||
}
|
||||
err = db.Save(r).Error
|
||||
err = tx.Save(r).Error
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": t.Email,
|
||||
}).Error(err)
|
||||
}).Errorf("error creating result: %v", err)
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
c.Results = append(c.Results, *r)
|
||||
log.Infof("Creating maillog for %s to send at %s\n", r.Email, sendDate)
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": r.Email,
|
||||
"send_date": sendDate,
|
||||
}).Debug("creating maillog")
|
||||
m := &MailLog{
|
||||
UserId: c.UserId,
|
||||
CampaignId: c.Id,
|
||||
|
@ -540,16 +595,18 @@ func PostCampaign(c *Campaign, uid int64) error {
|
|||
SendDate: sendDate,
|
||||
Processing: processing,
|
||||
}
|
||||
err = db.Save(m).Error
|
||||
err = tx.Save(m).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": t.Email,
|
||||
}).Errorf("error creating maillog entry: %v", err)
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
recipientIndex++
|
||||
}
|
||||
}
|
||||
err = db.Save(c).Error
|
||||
return err
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
//DeleteCampaign deletes the specified campaign
|
||||
|
@ -604,7 +661,8 @@ func CompleteCampaign(id int64, uid int64) error {
|
|||
// Mark the campaign as complete
|
||||
c.CompletedDate = time.Now().UTC()
|
||||
c.Status = CampaignComplete
|
||||
err = db.Where("id=? and user_id=?", id, uid).Save(&c).Error
|
||||
err = db.Model(&Campaign{}).Where("id=? and user_id=?", id, uid).
|
||||
Select([]string{"completed_date", "status"}).UpdateColumns(&c).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
check "gopkg.in/check.v1"
|
||||
|
@ -14,6 +16,11 @@ func (s *ModelsSuite) TestGenerateSendDate(c *check.C) {
|
|||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(campaign.LaunchDate, check.Equals, campaign.CreatedDate)
|
||||
|
||||
// For comparing the dates, we need to fetch the campaign again. This is
|
||||
// to solve an issue where the campaign object right now has time down to
|
||||
// the microsecond, while in MySQL it's rounded down to the second.
|
||||
campaign, _ = GetCampaign(campaign.Id, campaign.UserId)
|
||||
|
||||
ms, err := GetMailLogsByCampaign(campaign.Id)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
for _, m := range ms {
|
||||
|
@ -27,6 +34,8 @@ func (s *ModelsSuite) TestGenerateSendDate(c *check.C) {
|
|||
err = PostCampaign(&campaign, campaign.UserId)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
|
||||
campaign, _ = GetCampaign(campaign.Id, campaign.UserId)
|
||||
|
||||
ms, err = GetMailLogsByCampaign(campaign.Id)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
for _, m := range ms {
|
||||
|
@ -41,6 +50,8 @@ func (s *ModelsSuite) TestGenerateSendDate(c *check.C) {
|
|||
err = PostCampaign(&campaign, campaign.UserId)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
|
||||
campaign, _ = GetCampaign(campaign.Id, campaign.UserId)
|
||||
|
||||
ms, err = GetMailLogsByCampaign(campaign.Id)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
sendingOffset := 2 / float64(len(ms))
|
||||
|
@ -133,3 +144,194 @@ func (s *ModelsSuite) TestCompleteCampaignAlsoDeletesMailLogs(c *check.C) {
|
|||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(len(ms), check.Equals, 0)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestCampaignGetResults(c *check.C) {
|
||||
campaign := s.createCampaign(c)
|
||||
got, err := GetCampaign(campaign.Id, campaign.UserId)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(len(campaign.Results), check.Equals, len(got.Results))
|
||||
}
|
||||
|
||||
func setupCampaignDependencies(b *testing.B, size int) {
|
||||
group := Group{Name: "Test Group"}
|
||||
// Create a large group of 5000 members
|
||||
for i := 0; i < size; i++ {
|
||||
group.Targets = append(group.Targets, Target{BaseRecipient: BaseRecipient{Email: fmt.Sprintf("test%d@example.com", i), FirstName: "User", LastName: fmt.Sprintf("%d", i)}})
|
||||
}
|
||||
group.UserId = 1
|
||||
err := PostGroup(&group)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting group: %v", err)
|
||||
}
|
||||
|
||||
// Add a template
|
||||
template := Template{Name: "Test Template"}
|
||||
template.Subject = "{{.RId}} - Subject"
|
||||
template.Text = "{{.RId}} - Text"
|
||||
template.HTML = "{{.RId}} - HTML"
|
||||
template.UserId = 1
|
||||
err = PostTemplate(&template)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting template: %v", err)
|
||||
}
|
||||
|
||||
// Add a landing page
|
||||
p := Page{Name: "Test Page"}
|
||||
p.HTML = "<html>Test</html>"
|
||||
p.UserId = 1
|
||||
err = PostPage(&p)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting page: %v", err)
|
||||
}
|
||||
|
||||
// Add a sending profile
|
||||
smtp := SMTP{Name: "Test Page"}
|
||||
smtp.UserId = 1
|
||||
smtp.Host = "example.com"
|
||||
smtp.FromAddress = "test@test.com"
|
||||
err = PostSMTP(&smtp)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting smtp: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setupCampaign sets up the campaign dependencies as well as posting the
|
||||
// actual campaign
|
||||
func setupCampaign(b *testing.B, size int) Campaign {
|
||||
setupCampaignDependencies(b, size)
|
||||
campaign := Campaign{Name: "Test campaign"}
|
||||
campaign.UserId = 1
|
||||
campaign.Template = Template{Name: "Test Template"}
|
||||
campaign.Page = Page{Name: "Test Page"}
|
||||
campaign.SMTP = SMTP{Name: "Test Page"}
|
||||
campaign.Groups = []Group{Group{Name: "Test Group"}}
|
||||
PostCampaign(&campaign, 1)
|
||||
return campaign
|
||||
}
|
||||
|
||||
func BenchmarkCampaign100(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
setupCampaignDependencies(b, 100)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
campaign := Campaign{Name: "Test campaign"}
|
||||
campaign.UserId = 1
|
||||
campaign.Template = Template{Name: "Test Template"}
|
||||
campaign.Page = Page{Name: "Test Page"}
|
||||
campaign.SMTP = SMTP{Name: "Test Page"}
|
||||
campaign.Groups = []Group{Group{Name: "Test Group"}}
|
||||
|
||||
b.StartTimer()
|
||||
err := PostCampaign(&campaign, 1)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting campaign: %v", err)
|
||||
}
|
||||
b.StopTimer()
|
||||
db.Delete(Result{})
|
||||
db.Delete(MailLog{})
|
||||
db.Delete(Campaign{})
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkCampaign1000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
setupCampaignDependencies(b, 1000)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
campaign := Campaign{Name: "Test campaign"}
|
||||
campaign.UserId = 1
|
||||
campaign.Template = Template{Name: "Test Template"}
|
||||
campaign.Page = Page{Name: "Test Page"}
|
||||
campaign.SMTP = SMTP{Name: "Test Page"}
|
||||
campaign.Groups = []Group{Group{Name: "Test Group"}}
|
||||
|
||||
b.StartTimer()
|
||||
err := PostCampaign(&campaign, 1)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting campaign: %v", err)
|
||||
}
|
||||
b.StopTimer()
|
||||
db.Delete(Result{})
|
||||
db.Delete(MailLog{})
|
||||
db.Delete(Campaign{})
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkCampaign10000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
setupCampaignDependencies(b, 10000)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
campaign := Campaign{Name: "Test campaign"}
|
||||
campaign.UserId = 1
|
||||
campaign.Template = Template{Name: "Test Template"}
|
||||
campaign.Page = Page{Name: "Test Page"}
|
||||
campaign.SMTP = SMTP{Name: "Test Page"}
|
||||
campaign.Groups = []Group{Group{Name: "Test Group"}}
|
||||
|
||||
b.StartTimer()
|
||||
err := PostCampaign(&campaign, 1)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting campaign: %v", err)
|
||||
}
|
||||
b.StopTimer()
|
||||
db.Delete(Result{})
|
||||
db.Delete(MailLog{})
|
||||
db.Delete(Campaign{})
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkGetCampaign100(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
campaign := setupCampaign(b, 100)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GetCampaign(campaign.Id, campaign.UserId)
|
||||
if err != nil {
|
||||
b.Fatalf("error getting campaign: %v", err)
|
||||
}
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkGetCampaign1000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
campaign := setupCampaign(b, 1000)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GetCampaign(campaign.Id, campaign.UserId)
|
||||
if err != nil {
|
||||
b.Fatalf("error getting campaign: %v", err)
|
||||
}
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkGetCampaign5000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
campaign := setupCampaign(b, 5000)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GetCampaign(campaign.Id, campaign.UserId)
|
||||
if err != nil {
|
||||
b.Fatalf("error getting campaign: %v", err)
|
||||
}
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkGetCampaign10000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
campaign := setupCampaign(b, 10000)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GetCampaign(campaign.Id, campaign.UserId)
|
||||
if err != nil {
|
||||
b.Fatalf("error getting campaign: %v", err)
|
||||
}
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/gophish/gomail"
|
||||
"github.com/gophish/gophish/config"
|
||||
|
@ -77,6 +74,10 @@ func (s *EmailRequest) Success() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *EmailRequest) GetSmtpFrom() (string, error) {
|
||||
return s.SMTP.FromAddress, nil
|
||||
}
|
||||
|
||||
// PostEmailRequest stores a SendTestEmailRequest in the database.
|
||||
func PostEmailRequest(s *EmailRequest) error {
|
||||
// Generate an ID to be used in the underlying Result object
|
||||
|
@ -99,14 +100,10 @@ func GetEmailRequestByResultId(id string) (EmailRequest, error) {
|
|||
// Generate fills in the details of a gomail.Message with the contents
|
||||
// from the SendTestEmailRequest.
|
||||
func (s *EmailRequest) Generate(msg *gomail.Message) error {
|
||||
f, err := mail.ParseAddress(s.FromAddress)
|
||||
f, err := mail.ParseAddress(s.getFromAddress())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fn := f.Name
|
||||
if fn == "" {
|
||||
fn = f.Address
|
||||
}
|
||||
msg.SetAddressHeader("From", f.Address, f.Name)
|
||||
|
||||
ptx, err := NewPhishingTemplateContext(s, s.BaseRecipient, s.RId)
|
||||
|
@ -148,7 +145,7 @@ func (s *EmailRequest) Generate(msg *gomail.Message) error {
|
|||
log.Error(err)
|
||||
}
|
||||
// don't set the Subject header if it is blank
|
||||
if len(subject) != 0 {
|
||||
if subject != "" {
|
||||
msg.SetHeader("Subject", subject)
|
||||
}
|
||||
|
||||
|
@ -171,16 +168,10 @@ func (s *EmailRequest) Generate(msg *gomail.Message) error {
|
|||
msg.AddAlternative("text/html", html)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the files
|
||||
for _, a := range s.Template.Attachments {
|
||||
msg.Attach(func(a Attachment) (string, gomail.FileSetting, gomail.FileSetting) {
|
||||
h := map[string][]string{"Content-ID": {fmt.Sprintf("<%s>", a.Name)}}
|
||||
return a.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
||||
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
|
||||
_, err = io.Copy(w, decoder)
|
||||
return err
|
||||
}), gomail.SetHeader(h)
|
||||
}(a))
|
||||
addAttachment(msg, a, ptx)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -106,6 +106,37 @@ func (s *ModelsSuite) TestEmailRequestGenerate(ch *check.C) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestGetSmtpFrom(ch *check.C) {
|
||||
smtp := SMTP{
|
||||
FromAddress: "from@example.com",
|
||||
}
|
||||
template := Template{
|
||||
Name: "Test Template",
|
||||
Subject: "{{.FirstName}} - Subject",
|
||||
Text: "{{.Email}} - Text",
|
||||
HTML: "{{.Email}} - HTML",
|
||||
}
|
||||
req := &EmailRequest{
|
||||
SMTP: smtp,
|
||||
Template: template,
|
||||
URL: "http://127.0.0.1/{{.Email}}",
|
||||
BaseRecipient: BaseRecipient{
|
||||
FirstName: "First",
|
||||
LastName: "Last",
|
||||
Email: "firstlast@example.com",
|
||||
},
|
||||
FromAddress: smtp.FromAddress,
|
||||
RId: fmt.Sprintf("%s-foobar", PreviewPrefix),
|
||||
}
|
||||
|
||||
msg := gomail.NewMessage()
|
||||
err := req.Generate(msg)
|
||||
smtp_from, err := req.GetSmtpFrom()
|
||||
|
||||
ch.Assert(err, check.Equals, nil)
|
||||
ch.Assert(smtp_from, check.Equals, "from@example.com")
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestEmailRequestURLTemplating(ch *check.C) {
|
||||
smtp := SMTP{
|
||||
FromAddress: "from@example.com",
|
||||
|
|
120
models/group.go
120
models/group.go
|
@ -196,13 +196,26 @@ func PostGroup(g *Group) error {
|
|||
return err
|
||||
}
|
||||
// Insert the group into the DB
|
||||
err := db.Save(g).Error
|
||||
tx := db.Begin()
|
||||
err := tx.Save(g).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
for _, t := range g.Targets {
|
||||
insertTargetIntoGroup(t, g.Id)
|
||||
err = insertTargetIntoGroup(tx, t, g.Id)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -213,7 +226,6 @@ func PutGroup(g *Group) error {
|
|||
return err
|
||||
}
|
||||
// Fetch group's existing targets from database.
|
||||
ts := []Target{}
|
||||
ts, err := GetTargets(g.Id)
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
|
@ -221,50 +233,65 @@ func PutGroup(g *Group) error {
|
|||
}).Error("Error getting targets from group")
|
||||
return err
|
||||
}
|
||||
// Check existing targets, removing any that are no longer in the group.
|
||||
tExists := false
|
||||
// Preload the caches
|
||||
cacheNew := make(map[string]int64, len(g.Targets))
|
||||
for _, t := range g.Targets {
|
||||
cacheNew[t.Email] = t.Id
|
||||
}
|
||||
|
||||
cacheExisting := make(map[string]int64, len(ts))
|
||||
for _, t := range ts {
|
||||
tExists = false
|
||||
// Is the target still in the group?
|
||||
for _, nt := range g.Targets {
|
||||
if t.Email == nt.Email {
|
||||
tExists = true
|
||||
break
|
||||
}
|
||||
cacheExisting[t.Email] = t.Id
|
||||
}
|
||||
|
||||
tx := db.Begin()
|
||||
// Check existing targets, removing any that are no longer in the group.
|
||||
for _, t := range ts {
|
||||
if _, ok := cacheNew[t.Email]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the target does not exist in the group any longer, we delete it
|
||||
if !tExists {
|
||||
err := db.Where("group_id=? and target_id=?", g.Id, t.Id).Delete(&GroupTarget{}).Error
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": t.Email,
|
||||
}).Error("Error deleting email")
|
||||
}
|
||||
err := tx.Where("group_id=? and target_id=?", g.Id, t.Id).Delete(&GroupTarget{}).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": t.Email,
|
||||
}).Error("Error deleting email")
|
||||
}
|
||||
}
|
||||
// Add any targets that are not in the database yet.
|
||||
for _, nt := range g.Targets {
|
||||
// Check and see if the target already exists in the db
|
||||
tExists = false
|
||||
for _, t := range ts {
|
||||
if t.Email == nt.Email {
|
||||
tExists = true
|
||||
nt.Id = t.Id
|
||||
break
|
||||
// If the target already exists in the database, we should just update
|
||||
// the record with the latest information.
|
||||
if id, ok := cacheExisting[nt.Email]; ok {
|
||||
nt.Id = id
|
||||
err = UpdateTarget(tx, nt)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Add target if not in database, otherwise update target information.
|
||||
if !tExists {
|
||||
insertTargetIntoGroup(nt, g.Id)
|
||||
} else {
|
||||
UpdateTarget(nt)
|
||||
// Otherwise, add target if not in database
|
||||
err = insertTargetIntoGroup(tx, nt, g.Id)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = db.Save(g).Error
|
||||
err = tx.Save(g).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
err = tx.Commit().Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -285,55 +312,42 @@ func DeleteGroup(g *Group) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func insertTargetIntoGroup(t Target, gid int64) error {
|
||||
func insertTargetIntoGroup(tx *gorm.DB, t Target, gid int64) error {
|
||||
if _, err := mail.ParseAddress(t.Email); err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": t.Email,
|
||||
}).Error("Invalid email")
|
||||
return err
|
||||
}
|
||||
trans := db.Begin()
|
||||
err := trans.Where(t).FirstOrCreate(&t).Error
|
||||
err := tx.Where(t).FirstOrCreate(&t).Error
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": t.Email,
|
||||
}).Error(err)
|
||||
trans.Rollback()
|
||||
return err
|
||||
}
|
||||
err = trans.Where("group_id=? and target_id=?", gid, t.Id).Find(&GroupTarget{}).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err = trans.Save(&GroupTarget{GroupId: gid, TargetId: t.Id}).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
trans.Rollback()
|
||||
return err
|
||||
}
|
||||
err = tx.Save(&GroupTarget{GroupId: gid, TargetId: t.Id}).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": t.Email,
|
||||
}).Error("Error adding many-many mapping")
|
||||
trans.Rollback()
|
||||
return err
|
||||
}
|
||||
err = trans.Commit().Error
|
||||
if err != nil {
|
||||
trans.Rollback()
|
||||
log.Error("Error committing db changes")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTarget updates the given target information in the database.
|
||||
func UpdateTarget(target Target) error {
|
||||
func UpdateTarget(tx *gorm.DB, target Target) error {
|
||||
targetInfo := map[string]interface{}{
|
||||
"first_name": target.FirstName,
|
||||
"last_name": target.LastName,
|
||||
"position": target.Position,
|
||||
}
|
||||
err := db.Model(&target).Where("id = ?", target.Id).Updates(targetInfo).Error
|
||||
err := tx.Model(&target).Where("id = ?", target.Id).Updates(targetInfo).Error
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{
|
||||
"email": target.Email,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"gopkg.in/check.v1"
|
||||
)
|
||||
|
@ -170,3 +173,121 @@ func (s *ModelsSuite) TestPutGroupEmptyAttribute(c *check.C) {
|
|||
c.Assert(targets[1].FirstName, check.Equals, "Second")
|
||||
c.Assert(targets[1].LastName, check.Equals, "Example")
|
||||
}
|
||||
|
||||
func benchmarkPostGroup(b *testing.B, iter, size int) {
|
||||
b.StopTimer()
|
||||
g := &Group{
|
||||
Name: fmt.Sprintf("Group-%d", iter),
|
||||
}
|
||||
for i := 0; i < size; i++ {
|
||||
g.Targets = append(g.Targets, Target{
|
||||
BaseRecipient: BaseRecipient{
|
||||
FirstName: "User",
|
||||
LastName: fmt.Sprintf("%d", i),
|
||||
Email: fmt.Sprintf("test-%d@test.com", i),
|
||||
},
|
||||
})
|
||||
}
|
||||
b.StartTimer()
|
||||
err := PostGroup(g)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting group: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// benchmarkPutGroup modifies half of the group to simulate a large change
|
||||
func benchmarkPutGroup(b *testing.B, iter, size int) {
|
||||
b.StopTimer()
|
||||
// First, we need to create the group
|
||||
g := &Group{
|
||||
Name: fmt.Sprintf("Group-%d", iter),
|
||||
}
|
||||
for i := 0; i < size; i++ {
|
||||
g.Targets = append(g.Targets, Target{
|
||||
BaseRecipient: BaseRecipient{
|
||||
FirstName: "User",
|
||||
LastName: fmt.Sprintf("%d", i),
|
||||
Email: fmt.Sprintf("test-%d@test.com", i),
|
||||
},
|
||||
})
|
||||
}
|
||||
err := PostGroup(g)
|
||||
if err != nil {
|
||||
b.Fatalf("error posting group: %v", err)
|
||||
}
|
||||
// Now we need to change half of the group.
|
||||
for i := 0; i < size/2; i++ {
|
||||
g.Targets[i].Email = fmt.Sprintf("test-modified-%d@test.com", i)
|
||||
}
|
||||
b.StartTimer()
|
||||
err = PutGroup(g)
|
||||
if err != nil {
|
||||
b.Fatalf("error modifying group: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPostGroup100(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchmarkPostGroup(b, i, 100)
|
||||
b.StopTimer()
|
||||
resetBenchmark(b)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkPostGroup1000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchmarkPostGroup(b, i, 1000)
|
||||
b.StopTimer()
|
||||
resetBenchmark(b)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkPostGroup10000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchmarkPostGroup(b, i, 10000)
|
||||
b.StopTimer()
|
||||
resetBenchmark(b)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkPutGroup100(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchmarkPutGroup(b, i, 100)
|
||||
b.StopTimer()
|
||||
resetBenchmark(b)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkPutGroup1000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchmarkPutGroup(b, i, 1000)
|
||||
b.StopTimer()
|
||||
resetBenchmark(b)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkPutGroup10000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
benchmarkPutGroup(b, i, 10000)
|
||||
b.StopTimer()
|
||||
resetBenchmark(b)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
)
|
||||
|
||||
const DefaultIMAPFolder = "INBOX"
|
||||
const DefaultIMAPFreq = 60 // Every 60 seconds
|
||||
|
||||
// IMAP contains the attributes needed to handle logging into an IMAP server to check
|
||||
// for reported emails
|
||||
type IMAP struct {
|
||||
UserId int64 `json:"-" gorm:"column:user_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Host string `json:"host"`
|
||||
Port uint16 `json:"port,string,omitempty"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TLS bool `json:"tls"`
|
||||
IgnoreCertErrors bool `json:"ignore_cert_errors"`
|
||||
Folder string `json:"folder"`
|
||||
RestrictDomain string `json:"restrict_domain"`
|
||||
DeleteReportedCampaignEmail bool `json:"delete_reported_campaign_email"`
|
||||
LastLogin time.Time `json:"last_login,omitempty"`
|
||||
ModifiedDate time.Time `json:"modified_date"`
|
||||
IMAPFreq uint32 `json:"imap_freq,string,omitempty"`
|
||||
}
|
||||
|
||||
// ErrIMAPHostNotSpecified is thrown when there is no Host specified
|
||||
// in the IMAP configuration
|
||||
var ErrIMAPHostNotSpecified = errors.New("No IMAP Host specified")
|
||||
|
||||
// ErrIMAPPortNotSpecified is thrown when there is no Port specified
|
||||
// in the IMAP configuration
|
||||
var ErrIMAPPortNotSpecified = errors.New("No IMAP Port specified")
|
||||
|
||||
// ErrInvalidIMAPHost indicates that the IMAP server string is invalid
|
||||
var ErrInvalidIMAPHost = errors.New("Invalid IMAP server address")
|
||||
|
||||
// ErrInvalidIMAPPort indicates that the IMAP Port is invalid
|
||||
var ErrInvalidIMAPPort = errors.New("Invalid IMAP Port")
|
||||
|
||||
// ErrIMAPUsernameNotSpecified is thrown when there is no Username specified
|
||||
// in the IMAP configuration
|
||||
var ErrIMAPUsernameNotSpecified = errors.New("No Username specified")
|
||||
|
||||
// ErrIMAPPasswordNotSpecified is thrown when there is no Password specified
|
||||
// in the IMAP configuration
|
||||
var ErrIMAPPasswordNotSpecified = errors.New("No Password specified")
|
||||
|
||||
// ErrInvalidIMAPFreq is thrown when the frequency for polling the
|
||||
// IMAP server is invalid
|
||||
var ErrInvalidIMAPFreq = errors.New("Invalid polling frequency")
|
||||
|
||||
// TableName specifies the database tablename for Gorm to use
|
||||
func (im IMAP) TableName() string {
|
||||
return "imap"
|
||||
}
|
||||
|
||||
// Validate ensures that IMAP configs/connections are valid
|
||||
func (im *IMAP) Validate() error {
|
||||
switch {
|
||||
case im.Host == "":
|
||||
return ErrIMAPHostNotSpecified
|
||||
case im.Port == 0:
|
||||
return ErrIMAPPortNotSpecified
|
||||
case im.Username == "":
|
||||
return ErrIMAPUsernameNotSpecified
|
||||
case im.Password == "":
|
||||
return ErrIMAPPasswordNotSpecified
|
||||
}
|
||||
|
||||
// Set the default value for Folder
|
||||
if im.Folder == "" {
|
||||
im.Folder = DefaultIMAPFolder
|
||||
}
|
||||
|
||||
// Make sure im.Host is an IP or hostname. NB will fail if unable to resolve the hostname.
|
||||
ip := net.ParseIP(im.Host)
|
||||
_, err := net.LookupHost(im.Host)
|
||||
if ip == nil && err != nil {
|
||||
return ErrInvalidIMAPHost
|
||||
}
|
||||
|
||||
// Make sure 1 >= port <= 65535
|
||||
if im.Port < 1 || im.Port > 65535 {
|
||||
return ErrInvalidIMAPPort
|
||||
}
|
||||
|
||||
// Make sure the polling frequency is between every 30 seconds and every year
|
||||
// If not set it to the default
|
||||
if im.IMAPFreq < 30 || im.IMAPFreq > 31540000 {
|
||||
im.IMAPFreq = DefaultIMAPFreq
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIMAP returns the IMAP server owned by the given user.
|
||||
func GetIMAP(uid int64) ([]IMAP, error) {
|
||||
im := []IMAP{}
|
||||
count := 0
|
||||
err := db.Where("user_id=?", uid).Find(&im).Count(&count).Error
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return im, err
|
||||
}
|
||||
return im, nil
|
||||
}
|
||||
|
||||
// PostIMAP updates IMAP settings for a user in the database.
|
||||
func PostIMAP(im *IMAP, uid int64) error {
|
||||
err := im.Validate()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete old entry. TODO: Save settings and if fails to Save below replace with original
|
||||
err = DeleteIMAP(uid)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert new settings into the DB
|
||||
err = db.Save(im).Error
|
||||
if err != nil {
|
||||
log.Error("Unable to save to database: ", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteIMAP deletes the existing IMAP in the database.
|
||||
func DeleteIMAP(uid int64) error {
|
||||
err := db.Where("user_id=?", uid).Delete(&IMAP{}).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func SuccessfulLogin(im *IMAP) error {
|
||||
err := db.Model(&im).Where("user_id = ?", im.UserId).Update("last_login", time.Now().UTC()).Error
|
||||
if err != nil {
|
||||
log.Error("Unable to update database: ", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -2,7 +2,6 @@ package models
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -10,6 +9,7 @@ import (
|
|||
"math/big"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -27,6 +27,9 @@ var MaxSendAttempts = 8
|
|||
// MailLog is exceeded.
|
||||
var ErrMaxSendAttempts = errors.New("max send attempts exceeded")
|
||||
|
||||
// Attachments with these file extensions have inline disposition
|
||||
var embeddedFileExtensions = []string{".jpg", ".jpeg", ".png", ".gif"}
|
||||
|
||||
// MailLog is a struct that holds information about an email that is to be
|
||||
// sent out.
|
||||
type MailLog struct {
|
||||
|
@ -37,6 +40,8 @@ type MailLog struct {
|
|||
SendDate time.Time `json:"send_date"`
|
||||
SendAttempt int `json:"send_attempt"`
|
||||
Processing bool `json:"-"`
|
||||
|
||||
cachedCampaign *Campaign
|
||||
}
|
||||
|
||||
// GenerateMailLog creates a new maillog for the given campaign and
|
||||
|
@ -123,18 +128,42 @@ func (m *MailLog) Success() error {
|
|||
return err
|
||||
}
|
||||
err = db.Delete(m).Error
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDialer returns a dialer based on the maillog campaign's SMTP configuration
|
||||
func (m *MailLog) GetDialer() (mailer.Dialer, error) {
|
||||
c, err := GetCampaign(m.CampaignId, m.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
c := m.cachedCampaign
|
||||
if c == nil {
|
||||
campaign, err := GetCampaignMailContext(m.CampaignId, m.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c = &campaign
|
||||
}
|
||||
return c.SMTP.GetDialer()
|
||||
}
|
||||
|
||||
// CacheCampaign allows bulk-mail workers to cache the otherwise expensive
|
||||
// campaign lookup operation by providing a pointer to the campaign here.
|
||||
func (m *MailLog) CacheCampaign(campaign *Campaign) error {
|
||||
if campaign.Id != m.CampaignId {
|
||||
return fmt.Errorf("incorrect campaign provided for caching. expected %d got %d", m.CampaignId, campaign.Id)
|
||||
}
|
||||
m.cachedCampaign = campaign
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MailLog) GetSmtpFrom() (string, error) {
|
||||
c, err := GetCampaign(m.CampaignId, m.UserId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
f, err := mail.ParseAddress(c.SMTP.FromAddress)
|
||||
return f.Address, err
|
||||
}
|
||||
|
||||
// Generate fills in the details of a gomail.Message instance with
|
||||
// the correct headers and body from the campaign and recipient listed in
|
||||
// the maillog. We accept the gomail.Message as an argument so that the caller
|
||||
|
@ -144,18 +173,25 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c, err := GetCampaign(m.CampaignId, m.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
c := m.cachedCampaign
|
||||
if c == nil {
|
||||
campaign, err := GetCampaignMailContext(m.CampaignId, m.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c = &campaign
|
||||
}
|
||||
|
||||
f, err := mail.ParseAddress(c.SMTP.FromAddress)
|
||||
f, err := mail.ParseAddress(c.Template.EnvelopeSender)
|
||||
if err != nil {
|
||||
return err
|
||||
f, err = mail.ParseAddress(c.SMTP.FromAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
msg.SetAddressHeader("From", f.Address, f.Name)
|
||||
|
||||
ptx, err := NewPhishingTemplateContext(&c, r.BaseRecipient, r.RId)
|
||||
ptx, err := NewPhishingTemplateContext(c, r.BaseRecipient, r.RId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -191,11 +227,12 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
|
|||
|
||||
// Parse remaining templates
|
||||
subject, err := ExecuteTemplate(c.Template.Subject, ptx)
|
||||
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
// don't set Subject header if the subject is empty
|
||||
if len(subject) != 0 {
|
||||
if subject != "" {
|
||||
msg.SetHeader("Subject", subject)
|
||||
}
|
||||
|
||||
|
@ -220,14 +257,7 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
|
|||
}
|
||||
// Attach the files
|
||||
for _, a := range c.Template.Attachments {
|
||||
msg.Attach(func(a Attachment) (string, gomail.FileSetting, gomail.FileSetting) {
|
||||
h := map[string][]string{"Content-ID": {fmt.Sprintf("<%s>", a.Name)}}
|
||||
return a.Name, gomail.SetCopyFunc(func(w io.Writer) error {
|
||||
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(a.Content))
|
||||
_, err = io.Copy(w, decoder)
|
||||
return err
|
||||
}), gomail.SetHeader(h)
|
||||
}(a))
|
||||
addAttachment(msg, a, ptx)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -299,3 +329,35 @@ func (m *MailLog) generateMessageID() (string, error) {
|
|||
msgid := fmt.Sprintf("<%d.%d.%d@%s>", t, pid, rint, h)
|
||||
return msgid, nil
|
||||
}
|
||||
|
||||
// Check if an attachment should have inline disposition based on
|
||||
// its file extension.
|
||||
func shouldEmbedAttachment(name string) bool {
|
||||
ext := filepath.Ext(name)
|
||||
for _, v := range embeddedFileExtensions {
|
||||
if strings.EqualFold(ext, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add an attachment to a gomail message, with the Content-Disposition
|
||||
// header set to inline or attachment depending on its file extension.
|
||||
func addAttachment(msg *gomail.Message, a Attachment, ptx PhishingTemplateContext) {
|
||||
copyFunc := gomail.SetCopyFunc(func(c Attachment) func(w io.Writer) error {
|
||||
return func(w io.Writer) error {
|
||||
reader, err := a.ApplyTemplate(ptx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, reader)
|
||||
return err
|
||||
}
|
||||
}(a))
|
||||
if shouldEmbedAttachment(a.Name) {
|
||||
msg.Embed(a.Name, copyFunc)
|
||||
} else {
|
||||
msg.Attach(a.Name, copyFunc)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
"net/textproto"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gophish/gophish/config"
|
||||
|
@ -212,15 +213,51 @@ func (s *ModelsSuite) TestGenerateMailLog(ch *check.C) {
|
|||
ch.Assert(m.Processing, check.Equals, false)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestMailLogGetSmtpFrom(ch *check.C) {
|
||||
template := Template{
|
||||
Name: "OverrideSmtpFrom",
|
||||
UserId: 1,
|
||||
Text: "dummytext",
|
||||
HTML: "Dummyhtml",
|
||||
Subject: "Dummysubject",
|
||||
EnvelopeSender: "spoofing@example.com",
|
||||
}
|
||||
ch.Assert(PostTemplate(&template), check.Equals, nil)
|
||||
campaign := s.createCampaignDependencies(ch)
|
||||
campaign.Template = template
|
||||
|
||||
ch.Assert(PostCampaign(&campaign, campaign.UserId), check.Equals, nil)
|
||||
result := campaign.Results[0]
|
||||
|
||||
m := &MailLog{}
|
||||
err := db.Where("r_id=? AND campaign_id=?", result.RId, campaign.Id).
|
||||
Find(m).Error
|
||||
ch.Assert(err, check.Equals, nil)
|
||||
|
||||
msg := gomail.NewMessage()
|
||||
err = m.Generate(msg)
|
||||
ch.Assert(err, check.Equals, nil)
|
||||
|
||||
msgBuff := &bytes.Buffer{}
|
||||
_, err = msg.WriteTo(msgBuff)
|
||||
ch.Assert(err, check.Equals, nil)
|
||||
|
||||
got, err := email.NewEmailFromReader(msgBuff)
|
||||
ch.Assert(err, check.Equals, nil)
|
||||
ch.Assert(got.From, check.Equals, "spoofing@example.com")
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestMailLogGenerate(ch *check.C) {
|
||||
campaign := s.createCampaign(ch)
|
||||
result := campaign.Results[0]
|
||||
expected := &email.Email{
|
||||
From: "test@test.com", // Default smtp.FromAddress
|
||||
Subject: fmt.Sprintf("%s - Subject", result.RId),
|
||||
Text: []byte(fmt.Sprintf("%s - Text", result.RId)),
|
||||
HTML: []byte(fmt.Sprintf("%s - HTML", result.RId)),
|
||||
}
|
||||
got := s.emailFromFirstMailLog(campaign, ch)
|
||||
ch.Assert(got.From, check.Equals, expected.From)
|
||||
ch.Assert(got.Subject, check.Equals, expected.Subject)
|
||||
ch.Assert(string(got.Text), check.Equals, string(expected.Text))
|
||||
ch.Assert(string(got.HTML), check.Equals, string(expected.HTML))
|
||||
|
@ -247,7 +284,7 @@ func (s *ModelsSuite) TestMailLogGenerateOverrideTransparencyHeaders(ch *check.C
|
|||
smtp := SMTP{
|
||||
Name: "Test SMTP",
|
||||
Host: "1.1.1.1:25",
|
||||
FromAddress: "Foo Bar <foo@example.com>",
|
||||
FromAddress: "foo@example.com",
|
||||
UserId: 1,
|
||||
Headers: []Header{
|
||||
Header{Key: "X-Gophish-Contact", Value: ""},
|
||||
|
@ -322,3 +359,111 @@ func (s *ModelsSuite) TestMailLogGenerateEmptySubject(ch *check.C) {
|
|||
got := s.emailFromFirstMailLog(campaign, ch)
|
||||
ch.Assert(got.Subject, check.Equals, expected.Subject)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestShouldEmbedAttachment(ch *check.C) {
|
||||
|
||||
// Supported file extensions
|
||||
ch.Assert(shouldEmbedAttachment(".png"), check.Equals, true)
|
||||
ch.Assert(shouldEmbedAttachment(".jpg"), check.Equals, true)
|
||||
ch.Assert(shouldEmbedAttachment(".jpeg"), check.Equals, true)
|
||||
ch.Assert(shouldEmbedAttachment(".gif"), check.Equals, true)
|
||||
|
||||
// Some other file extensions
|
||||
ch.Assert(shouldEmbedAttachment(".docx"), check.Equals, false)
|
||||
ch.Assert(shouldEmbedAttachment(".txt"), check.Equals, false)
|
||||
ch.Assert(shouldEmbedAttachment(".jar"), check.Equals, false)
|
||||
ch.Assert(shouldEmbedAttachment(".exe"), check.Equals, false)
|
||||
|
||||
// Invalid input
|
||||
ch.Assert(shouldEmbedAttachment(""), check.Equals, false)
|
||||
ch.Assert(shouldEmbedAttachment("png"), check.Equals, false)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestEmbedAttachment(ch *check.C) {
|
||||
campaign := s.createCampaignDependencies(ch)
|
||||
campaign.Template.Attachments = []Attachment{
|
||||
{
|
||||
Name: "test.png",
|
||||
Type: "image/png",
|
||||
Content: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=",
|
||||
},
|
||||
{
|
||||
Name: "test.txt",
|
||||
Type: "text/plain",
|
||||
Content: "VGVzdCB0ZXh0IGZpbGU=",
|
||||
},
|
||||
}
|
||||
PutTemplate(&campaign.Template)
|
||||
ch.Assert(PostCampaign(&campaign, campaign.UserId), check.Equals, nil)
|
||||
got := s.emailFromFirstMailLog(campaign, ch)
|
||||
|
||||
// The email package simply ignores attachments where the Content-Disposition header is set
|
||||
// to inline, so the best we can do without replacing the whole thing is to check that only
|
||||
// the text file was added as an attachment.
|
||||
ch.Assert(got.Attachments, check.HasLen, 1)
|
||||
ch.Assert(got.Attachments[0].Filename, check.Equals, "test.txt")
|
||||
}
|
||||
|
||||
func BenchmarkMailLogGenerate100(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
campaign := setupCampaign(b, 100)
|
||||
ms, err := GetMailLogsByCampaign(campaign.Id)
|
||||
if err != nil {
|
||||
b.Fatalf("error getting maillogs for campaign: %v", err)
|
||||
}
|
||||
ms[0].CacheCampaign(&campaign)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
msg := gomail.NewMessage()
|
||||
ms[0].Generate(msg)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkMailLogGenerate1000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
campaign := setupCampaign(b, 1000)
|
||||
ms, err := GetMailLogsByCampaign(campaign.Id)
|
||||
if err != nil {
|
||||
b.Fatalf("error getting maillogs for campaign: %v", err)
|
||||
}
|
||||
ms[0].CacheCampaign(&campaign)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
msg := gomail.NewMessage()
|
||||
ms[0].Generate(msg)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkMailLogGenerate5000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
campaign := setupCampaign(b, 5000)
|
||||
ms, err := GetMailLogsByCampaign(campaign.Id)
|
||||
if err != nil {
|
||||
b.Fatalf("error getting maillogs for campaign: %v", err)
|
||||
}
|
||||
ms[0].CacheCampaign(&campaign)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
msg := gomail.NewMessage()
|
||||
ms[0].Generate(msg)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
||||
func BenchmarkMailLogGenerate10000(b *testing.B) {
|
||||
setupBenchmark(b)
|
||||
campaign := setupCampaign(b, 10000)
|
||||
ms, err := GetMailLogsByCampaign(campaign.Id)
|
||||
if err != nil {
|
||||
b.Fatalf("error getting maillogs for campaign: %v", err)
|
||||
}
|
||||
ms[0].CacheCampaign(&campaign)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
msg := gomail.NewMessage()
|
||||
ms[0].Generate(msg)
|
||||
}
|
||||
tearDownBenchmark(b)
|
||||
}
|
||||
|
|
124
models/models.go
124
models/models.go
|
@ -2,14 +2,20 @@ package models
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"bitbucket.org/liamstask/goose/lib/goose"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // Blank import needed to import mysql
|
||||
mysql "github.com/go-sql-driver/mysql"
|
||||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3
|
||||
|
@ -20,6 +26,19 @@ var conf *config.Config
|
|||
|
||||
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 (
|
||||
CampaignInProgress string = "In progress"
|
||||
CampaignQueued string = "Queued"
|
||||
|
@ -79,8 +98,38 @@ func chooseDBDriver(name, openStr string) goose.DBDriver {
|
|||
return d
|
||||
}
|
||||
|
||||
// Setup initializes the Conn object
|
||||
// It also populates the Gophish Config object
|
||||
func createTemporaryPassword(u *User) error {
|
||||
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 {
|
||||
// Setup the package-scoped config
|
||||
conf = c
|
||||
|
@ -96,6 +145,30 @@ func Setup(c *config.Config) error {
|
|||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Register certificates for tls encrypted db connections
|
||||
if conf.DBSSLCaPath != "" {
|
||||
switch conf.DBName {
|
||||
case "mysql":
|
||||
rootCertPool := x509.NewCertPool()
|
||||
pem, err := ioutil.ReadFile(conf.DBSSLCaPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
|
||||
log.Error("Failed to append PEM.")
|
||||
return err
|
||||
}
|
||||
mysql.RegisterTLSConfig("ssl_ca", &tls.Config{
|
||||
RootCAs: rootCertPool,
|
||||
})
|
||||
// Default database is sqlite3, which supports no tls, as connection
|
||||
// is file based
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Open our database connection
|
||||
i := 0
|
||||
for {
|
||||
|
@ -126,6 +199,7 @@ func Setup(c *config.Config) error {
|
|||
}
|
||||
// Create the admin user if it doesn't exist
|
||||
var userCount int64
|
||||
var adminUser User
|
||||
db.Model(&User{}).Count(&userCount)
|
||||
adminRole, err := GetRoleBySlug(RoleAdmin)
|
||||
if err != nil {
|
||||
|
@ -133,14 +207,44 @@ func Setup(c *config.Config) error {
|
|||
return err
|
||||
}
|
||||
if userCount == 0 {
|
||||
initUser := User{
|
||||
Username: "admin",
|
||||
Hash: "$2a$10$IYkPp0.QsM81lYYPrQx6W.U6oQGw7wMpozrKhKAHUBVL4mkm/EvAS", //gophish
|
||||
Role: adminRole,
|
||||
RoleID: adminRole.ID,
|
||||
adminUser := User{
|
||||
Username: DefaultAdminUsername,
|
||||
Role: adminRole,
|
||||
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 {
|
||||
log.Error(err)
|
||||
return err
|
||||
|
|
|
@ -96,5 +96,44 @@ func (s *ModelsSuite) createCampaign(ch *check.C) Campaign {
|
|||
c := s.createCampaignDependencies(ch)
|
||||
// Setup and "launch" our campaign
|
||||
ch.Assert(PostCampaign(&c, c.UserId), check.Equals, nil)
|
||||
|
||||
// For comparing the dates, we need to fetch the campaign again. This is
|
||||
// to solve an issue where the campaign object right now has time down to
|
||||
// the microsecond, while in MySQL it's rounded down to the second.
|
||||
c, _ = GetCampaign(c.Id, c.UserId)
|
||||
return c
|
||||
}
|
||||
|
||||
func setupBenchmark(b *testing.B) {
|
||||
conf := &config.Config{
|
||||
DBName: "sqlite3",
|
||||
DBPath: ":memory:",
|
||||
MigrationsPath: "../db/db_sqlite3/migrations/",
|
||||
}
|
||||
err := Setup(conf)
|
||||
if err != nil {
|
||||
b.Fatalf("Failed creating database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func tearDownBenchmark(b *testing.B) {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
b.Fatalf("error closing database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func resetBenchmark(b *testing.B) {
|
||||
db.Delete(Group{})
|
||||
db.Delete(Target{})
|
||||
db.Delete(GroupTarget{})
|
||||
db.Delete(SMTP{})
|
||||
db.Delete(Page{})
|
||||
db.Delete(Result{})
|
||||
db.Delete(MailLog{})
|
||||
db.Delete(Campaign{})
|
||||
|
||||
// Reset users table to default state.
|
||||
db.Not("id", 1).Delete(User{})
|
||||
db.Model(User{}).Update("username", "admin")
|
||||
}
|
||||
|
|
|
@ -138,6 +138,9 @@ func PostPage(p *Page) error {
|
|||
// Per the PUT Method RFC, it presumes all data for a page is provided.
|
||||
func PutPage(p *Page) error {
|
||||
err := p.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.Where("id=?", p.Id).Save(p).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
|
|
@ -39,10 +39,6 @@ type Result struct {
|
|||
}
|
||||
|
||||
func (r *Result) createEvent(status string, details interface{}) (*Event, error) {
|
||||
c, err := GetCampaign(r.CampaignId, r.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e := &Event{Email: r.Email, Message: status}
|
||||
if details != nil {
|
||||
dj, err := json.Marshal(details)
|
||||
|
@ -51,7 +47,7 @@ func (r *Result) createEvent(status string, details interface{}) (*Event, error)
|
|||
}
|
||||
e.Details = string(dj)
|
||||
}
|
||||
c.AddEvent(e)
|
||||
AddEvent(e, r.CampaignId)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
|
@ -189,7 +185,7 @@ func generateResultId() (string, error) {
|
|||
|
||||
// GenerateId generates a unique key to represent the result
|
||||
// in the database
|
||||
func (r *Result) GenerateId() error {
|
||||
func (r *Result) GenerateId(tx *gorm.DB) error {
|
||||
// Keep trying until we generate a unique key (shouldn't take more than one or two iterations)
|
||||
for {
|
||||
rid, err := generateResultId()
|
||||
|
@ -197,7 +193,7 @@ func (r *Result) GenerateId() error {
|
|||
return err
|
||||
}
|
||||
r.RId = rid
|
||||
err = db.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error
|
||||
err = tx.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
break
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
func (s *ModelsSuite) TestGenerateResultId(c *check.C) {
|
||||
r := Result{}
|
||||
r.GenerateId()
|
||||
r.GenerateId(db)
|
||||
match, err := regexp.Match("[a-zA-Z0-9]{7}", []byte(r.RId))
|
||||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(match, check.Equals, true)
|
||||
|
|
|
@ -5,11 +5,13 @@ import (
|
|||
"errors"
|
||||
"net/mail"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gophish/gomail"
|
||||
"github.com/gophish/gophish/dialer"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/mailer"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
@ -56,6 +58,10 @@ type Header struct {
|
|||
// specified in the SMTP configuration
|
||||
var ErrFromAddressNotSpecified = errors.New("No From Address specified")
|
||||
|
||||
// ErrInvalidFromAddress is thrown when the SMTP From field in the sending
|
||||
// profiles containes a value that is not an email address
|
||||
var ErrInvalidFromAddress = errors.New("Invalid SMTP From address because it is not an email address")
|
||||
|
||||
// ErrHostNotSpecified is thrown when there is no Host specified
|
||||
// in the SMTP configuration
|
||||
var ErrHostNotSpecified = errors.New("No SMTP Host specified")
|
||||
|
@ -75,6 +81,8 @@ func (s *SMTP) Validate() error {
|
|||
return ErrFromAddressNotSpecified
|
||||
case s.Host == "":
|
||||
return ErrHostNotSpecified
|
||||
case !validateFromAddress(s.FromAddress):
|
||||
return ErrInvalidFromAddress
|
||||
}
|
||||
_, err := mail.ParseAddress(s.FromAddress)
|
||||
if err != nil {
|
||||
|
@ -94,6 +102,12 @@ func (s *SMTP) Validate() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// validateFromAddress validates
|
||||
func validateFromAddress(email string) bool {
|
||||
r, _ := regexp.Compile("^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,18})$")
|
||||
return r.MatchString(email)
|
||||
}
|
||||
|
||||
// GetDialer returns a dialer for the given SMTP profile
|
||||
func (s *SMTP) GetDialer() (mailer.Dialer, error) {
|
||||
// Setup the message and dial
|
||||
|
@ -101,6 +115,7 @@ func (s *SMTP) GetDialer() (mailer.Dialer, error) {
|
|||
if len(hp) < 2 {
|
||||
hp = append(hp, "25")
|
||||
}
|
||||
host := hp[0]
|
||||
// Any issues should have been caught in validation, but we'll
|
||||
// double check here.
|
||||
port, err := strconv.Atoi(hp[1])
|
||||
|
@ -108,9 +123,10 @@ func (s *SMTP) GetDialer() (mailer.Dialer, error) {
|
|||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
d := gomail.NewDialer(hp[0], port, s.Username, s.Password)
|
||||
dialer := dialer.Dialer()
|
||||
d := gomail.NewWithDialer(dialer, host, port, s.Username, s.Password)
|
||||
d.TLSConfig = &tls.Config{
|
||||
ServerName: s.Host,
|
||||
ServerName: host,
|
||||
InsecureSkipVerify: s.IgnoreCertErrors,
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
|
|
|
@ -12,7 +12,7 @@ func (s *ModelsSuite) TestPostSMTP(c *check.C) {
|
|||
smtp := SMTP{
|
||||
Name: "Test SMTP",
|
||||
Host: "1.1.1.1:25",
|
||||
FromAddress: "Foo Bar <foo@example.com>",
|
||||
FromAddress: "foo@example.com",
|
||||
UserId: 1,
|
||||
}
|
||||
err := PostSMTP(&smtp)
|
||||
|
@ -25,7 +25,7 @@ func (s *ModelsSuite) TestPostSMTP(c *check.C) {
|
|||
func (s *ModelsSuite) TestPostSMTPNoHost(c *check.C) {
|
||||
smtp := SMTP{
|
||||
Name: "Test SMTP",
|
||||
FromAddress: "Foo Bar <foo@example.com>",
|
||||
FromAddress: "foo@example.com",
|
||||
UserId: 1,
|
||||
}
|
||||
err := PostSMTP(&smtp)
|
||||
|
@ -42,12 +42,34 @@ func (s *ModelsSuite) TestPostSMTPNoFrom(c *check.C) {
|
|||
c.Assert(err, check.Equals, ErrFromAddressNotSpecified)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestPostSMTPValidHeader(c *check.C) {
|
||||
func (s *ModelsSuite) TestPostInvalidFrom(c *check.C) {
|
||||
smtp := SMTP{
|
||||
Name: "Test SMTP",
|
||||
Host: "1.1.1.1:25",
|
||||
FromAddress: "Foo Bar <foo@example.com>",
|
||||
UserId: 1,
|
||||
}
|
||||
err := PostSMTP(&smtp)
|
||||
c.Assert(err, check.Equals, ErrInvalidFromAddress)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestPostInvalidFromEmail(c *check.C) {
|
||||
smtp := SMTP{
|
||||
Name: "Test SMTP",
|
||||
Host: "1.1.1.1:25",
|
||||
FromAddress: "example.com",
|
||||
UserId: 1,
|
||||
}
|
||||
err := PostSMTP(&smtp)
|
||||
c.Assert(err, check.Equals, ErrInvalidFromAddress)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestPostSMTPValidHeader(c *check.C) {
|
||||
smtp := SMTP{
|
||||
Name: "Test SMTP",
|
||||
Host: "1.1.1.1:25",
|
||||
FromAddress: "foo@example.com",
|
||||
UserId: 1,
|
||||
Headers: []Header{
|
||||
Header{Key: "Reply-To", Value: "test@example.com"},
|
||||
Header{Key: "X-Mailer", Value: "gophish"},
|
||||
|
@ -73,7 +95,7 @@ func (s *ModelsSuite) TestSMTPGetDialer(ch *check.C) {
|
|||
dialer := d.(*Dialer).Dialer
|
||||
ch.Assert(dialer.Host, check.Equals, host)
|
||||
ch.Assert(dialer.Port, check.Equals, port)
|
||||
ch.Assert(dialer.TLSConfig.ServerName, check.Equals, smtp.Host)
|
||||
ch.Assert(dialer.TLSConfig.ServerName, check.Equals, host)
|
||||
ch.Assert(dialer.TLSConfig.InsecureSkipVerify, check.Equals, smtp.IgnoreCertErrors)
|
||||
}
|
||||
|
||||
|
@ -81,3 +103,15 @@ func (s *ModelsSuite) TestGetInvalidSMTP(ch *check.C) {
|
|||
_, err := GetSMTP(-1, 1)
|
||||
ch.Assert(err, check.Equals, gorm.ErrRecordNotFound)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestDefaultDeniedDial(ch *check.C) {
|
||||
host := "169.254.169.254"
|
||||
port := 25
|
||||
smtp := SMTP{
|
||||
Host: fmt.Sprintf("%s:%d", host, port),
|
||||
}
|
||||
d, err := smtp.GetDialer()
|
||||
ch.Assert(err, check.Equals, nil)
|
||||
_, err = d.Dial()
|
||||
ch.Assert(err, check.ErrorMatches, ".*upstream connection denied.*")
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
|
@ -10,14 +11,15 @@ import (
|
|||
|
||||
// Template models hold the attributes for an email template to be sent to targets
|
||||
type Template struct {
|
||||
Id int64 `json:"id" gorm:"column:id; primary_key:yes"`
|
||||
UserId int64 `json:"-" gorm:"column:user_id"`
|
||||
Name string `json:"name"`
|
||||
Subject string `json:"subject"`
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html" gorm:"column:html"`
|
||||
ModifiedDate time.Time `json:"modified_date"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
Id int64 `json:"id" gorm:"column:id; primary_key:yes"`
|
||||
UserId int64 `json:"-" gorm:"column:user_id"`
|
||||
Name string `json:"name"`
|
||||
EnvelopeSender string `json:"envelope_sender"`
|
||||
Subject string `json:"subject"`
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html" gorm:"column:html"`
|
||||
ModifiedDate time.Time `json:"modified_date"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
}
|
||||
|
||||
// ErrTemplateNameNotSpecified is thrown when a template name is not specified
|
||||
|
@ -33,6 +35,11 @@ func (t *Template) Validate() error {
|
|||
return ErrTemplateNameNotSpecified
|
||||
case t.Text == "" && t.HTML == "":
|
||||
return ErrTemplateMissingParameter
|
||||
case t.EnvelopeSender != "":
|
||||
_, err := mail.ParseAddress(t.EnvelopeSender)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := ValidateTemplate(t.HTML); err != nil {
|
||||
return err
|
||||
|
@ -40,6 +47,12 @@ func (t *Template) Validate() error {
|
|||
if err := ValidateTemplate(t.Text); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range t.Attachments {
|
||||
if err := a.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,10 @@ func NewPhishingTemplateContext(ctx TemplateContext, r BaseRecipient, rid string
|
|||
|
||||
// For the base URL, we'll reset the the path and the query
|
||||
// This will create a URL in the form of http://example.com
|
||||
baseURL, _ := url.Parse(templateURL)
|
||||
baseURL, err := url.Parse(templateURL)
|
||||
if err != nil {
|
||||
return PhishingTemplateContext{}, err
|
||||
}
|
||||
baseURL.Path = ""
|
||||
baseURL.RawQuery = ""
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//zoom.us//iCalendar Event//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
CLASS:PUBLIC
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/London
|
||||
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/London
|
||||
X-LIC-LOCATION:Europe/London
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0000
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:BST
|
||||
DTSTART:19700329T010000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0000
|
||||
TZNAME:GMT
|
||||
DTSTART:19701025T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20210306T182251Z
|
||||
DTSTART;TZID=Europe/London:20210306T183000
|
||||
DTEND;TZID=Europe/London:20210306T190000
|
||||
SUMMARY:Gophish Test Calendar
|
||||
UID:20210306T182251Z-89336450000@fe80:0:0:0:31:49ff:fec9:f252ens5
|
||||
TZID:Europe/London
|
||||
DESCRIPTION:Glenn Wilkinson is inviting you to a scheduled Zoom meeting.\
|
||||
n\nJoin Zoom Meeting\n{{.URL}}\n\nMeeting ID: 893 3645 9466\nPasscode: 31337\
|
||||
nOne tap mobile\n+442039017895\,\,89336450000#\,\,\,\,*509879# United Ki
|
||||
ngdom\n+441314601196\,\,89336450000#\,\,\,\,*509879# United Kingdom\n\nD
|
||||
ial by your location\n +44 203 901 7895 United Kingdom\n +
|
||||
44 131 460 1196 United Kingdom\n +44 203 051 2874 United Kingdom\
|
||||
n +44 203 481 5237 United Kingdom\n +44 203 481 5240 Unite
|
||||
d Kingdom\n +1 253 215 8782 US (Tacoma)\n +1 301 715 8592
|
||||
US (Washington DC)\n +1 312 626 6799 US (Chicago)\n +1 346
|
||||
248 7799 US (Houston)\n +1 646 558 8656 US (New York)\n +
|
||||
1 669 900 9128 US https://us02web.zoom.us/u/kpXDbMrN\n\n
|
||||
LOCATION:{{.URL}}
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT10M
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,51 @@
|
|||
BEGIN:VCALENDAR
|
||||
PRODID:-//zoom.us//iCalendar Event//EN
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
CLASS:PUBLIC
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/London
|
||||
TZURL:http://tzurl.org/zoneinfo-outlook/Europe/London
|
||||
X-LIC-LOCATION:Europe/London
|
||||
BEGIN:DAYLIGHT
|
||||
TZOFFSETFROM:+0000
|
||||
TZOFFSETTO:+0100
|
||||
TZNAME:BST
|
||||
DTSTART:19700329T010000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0000
|
||||
TZNAME:GMT
|
||||
DTSTART:19701025T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20210306T182251Z
|
||||
DTSTART;TZID=Europe/London:20210306T183000
|
||||
DTEND;TZID=Europe/London:20210306T190000
|
||||
SUMMARY:Gophish Test Calendar
|
||||
UID:20210306T182251Z-89336450000@fe80:0:0:0:31:49ff:fec9:f252ens5
|
||||
TZID:Europe/London
|
||||
DESCRIPTION:Glenn Wilkinson is inviting you to a scheduled Zoom meeting.\
|
||||
n\nJoin Zoom Meeting\nhttp://testurl.com/?rid=1234567\n\nMeeting ID: 893 3645 9466\nPasscode: 31337\
|
||||
nOne tap mobile\n+442039017895\,\,89336450000#\,\,\,\,*509879# United Ki
|
||||
ngdom\n+441314601196\,\,89336450000#\,\,\,\,*509879# United Kingdom\n\nD
|
||||
ial by your location\n +44 203 901 7895 United Kingdom\n +
|
||||
44 131 460 1196 United Kingdom\n +44 203 051 2874 United Kingdom\
|
||||
n +44 203 481 5237 United Kingdom\n +44 203 481 5240 Unite
|
||||
d Kingdom\n +1 253 215 8782 US (Tacoma)\n +1 301 715 8592
|
||||
US (Washington DC)\n +1 312 626 6799 US (Chicago)\n +1 346
|
||||
248 7799 US (Houston)\n +1 646 558 8656 US (New York)\n +
|
||||
1 669 900 9128 US https://us02web.zoom.us/u/kpXDbMrN\n\n
|
||||
LOCATION:http://testurl.com/?rid=1234567
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT10M
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,13 @@
|
|||
<html>
|
||||
<head><title>Page for {{.FirstName}}</title></head>
|
||||
|
||||
<body>
|
||||
|
||||
Hello {{.FirstName}} {{.LastName}} <p>
|
||||
|
||||
|
||||
Click <a href="{{.URL}}">here</a> for a legit link.
|
||||
|
||||
</body>
|
||||
{{.Tracker}}
|
||||
</html>
|
|
@ -0,0 +1,13 @@
|
|||
<html>
|
||||
<head><title>Page for Foo</title></head>
|
||||
|
||||
<body>
|
||||
|
||||
Hello Foo Bar <p>
|
||||
|
||||
|
||||
Click <a href="http://testurl.com/?rid=1234567">here</a> for a legit link.
|
||||
|
||||
</body>
|
||||
<img alt='' style='display: none' src='http://testurl.local/track?rid=1234567'/>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<head><title>There are no variables here.</title></head>
|
||||
|
||||
<body>
|
||||
|
||||
There are no vars in this file.
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<head><title>There are no variables here.</title></head>
|
||||
|
||||
<body>
|
||||
|
||||
There are no vars in this file.
|
||||
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue