├── .gitignore ├── template.docx ├── requirements.txt ├── examples ├── Gophish Results for Demo Campaign.docx └── Gophish Results for Demo Campaign.xlsx ├── lib ├── __init__.py ├── banners.py └── goreport.py ├── .isort.cfg ├── LICENSE ├── CHANGELOG.md ├── GoReport.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.pyc 3 | *.config 4 | -------------------------------------------------------------------------------- /template.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrismaddalena/Goreport/master/template.docx -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-docx 2 | requests 3 | gophish 4 | configparser 5 | python-dateutil 6 | user-agents 7 | click 8 | xlsxwriter 9 | -------------------------------------------------------------------------------- /examples/Gophish Results for Demo Campaign.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrismaddalena/Goreport/master/examples/Gophish Results for Demo Campaign.docx -------------------------------------------------------------------------------- /examples/Gophish Results for Demo Campaign.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrismaddalena/Goreport/master/examples/Gophish Results for Demo Campaign.xlsx -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | modules = glob.glob(os.path.dirname(__file__)+"/*.py") 4 | __all__ = [ os.path.basename(f)[:-3] for f in modules] -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile=black 3 | extra_standard_library=asgiref 4 | known_first_party=lib 5 | known_django=django 6 | src_paths=isort,test 7 | line_length=90 8 | use_parentheses=True 9 | multi_line_output=3 10 | include_trailing_comma=True 11 | ensure_newline_before_comments=True 12 | sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 13 | import_heading_stdlib=Standard Libraries 14 | import_heading_firstparty=Goreport Libraries 15 | import_heading_thirdparty=3rd Party Libraries 16 | import_heading_django=Django Imports 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Chris Maddalena 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [3.1] - 10 May 2022 8 | 9 | ### Added 10 | 11 | * Added logic to incremement the number of opened emails for users who don't display the tracker image but do go on to click the link (fixes #19) 12 | * Added tracking of user `position` (job title) data for display in the results 13 | 14 | ### Changed 15 | 16 | * Campaign names are now stripped of any non-alphanumeric characters before they are used for a filename to avoid issues with special characters (fixes #30) 17 | 18 | ### Deprecated 19 | 20 | * None 21 | 22 | ### Removed 23 | 24 | * None 25 | 26 | ### Fixed 27 | 28 | * Fixed incorrect totals being presented when reporting on multiple campaigns at once caused by counters and lists not being reset (fixes #21) 29 | * Fixed submitted data being recorded in the wrong column in Excel reports (fixes #22) 30 | 31 | ### Security 32 | 33 | * None 34 | 35 | ## [3.0] - 31 March 2019 36 | 37 | ## Added 38 | 39 | * Added support for Gophish's "Email Reported" event. 40 | * Added example reports from the Gophish demo database. 41 | * Added a new report table matching each unique IP address to its matching location. 42 | * Added `excel` output option for xlsx reports in place of the old `csv` reports. 43 | 44 | ## Changed 45 | 46 | * Updated the Google Maps API option now that it is the Geolocate API and requires a key. 47 | 48 | ## Removed 49 | 50 | * Dropped the MaxMind DB geolocation due to unreliability and replaced it with an option for ipinfo.io. 51 | * Removed the csv report in favor of a much nicer xlsx workbook report. 52 | 53 | ## Fixed 54 | 55 | * Fixed the `--complete` flag not setting the last campaign in a list to Complete. 56 | * Fixed typos in the reports and improved formatting. 57 | * Geolocation lookups for IP addresses are now much, much more efficient and occur only once per unique address. 58 | -------------------------------------------------------------------------------- /lib/banners.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """This module just prints an ASCII logo and banners.""" 5 | 6 | # Standard Libraries 7 | import random 8 | 9 | 10 | def print_banner(): 11 | """Just a function to print sweet ASCII art banners.""" 12 | banner_1 = (""" 13 | ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██████╗ ██████╗ ████████╗ 14 | ██╔════╝ ██╔═══██╗██╔══██╗██╔════╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝ 15 | ██║ ███╗██║ ██║██████╔╝█████╗ ██████╔╝██║ ██║██████╔╝ ██║ 16 | ██║ ██║██║ ██║██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██╔══██╗ ██║ 17 | ╚██████╔╝╚██████╔╝██║ ██║███████╗██║ ╚██████╔╝██║ ██║ ██║ 18 | ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ 19 | for GoPhish -- getgophish.com 20 | """) 21 | 22 | banner_2 = (""" 23 | ##### ###### 24 | # # #### # # ###### ##### #### ##### ##### 25 | # # # # # # # # # # # # # 26 | # #### # # ###### ##### # # # # # # # 27 | # # # # # # # ##### # # ##### # 28 | # # # # # # # # # # # # # 29 | ##### #### # # ###### # #### # # # 30 | for GoPhish -- getgophish.com 31 | """) 32 | 33 | banner_3 = (""" 34 | _|_|_| _|_|_| _| 35 | _| _|_| _| _| _|_| _|_|_| _|_| _| _|_| _|_|_|_| 36 | _| _|_| _| _| _|_|_| _|_|_|_| _| _| _| _| _|_| _| 37 | _| _| _| _| _| _| _| _| _| _| _| _| _| 38 | _|_|_| _|_| _| _| _|_|_| _|_|_| _|_| _| _|_| 39 | _| 40 | for GoPhish -- getgophish.com _| 41 | """) 42 | 43 | logo = (""" 44 | ``` 45 | ``````````` 46 | ````` ````` 47 | ````` `````` 48 | ````` ````` 49 | ``` ` ` ```` 50 | ``` ` ``` 51 | ``` ` ``` 52 | ``` ` ``` 53 | ``` ` ``` 54 | ``` ` ` ``` 55 | ``` ` ` ``` 56 | ``` ` ` ``` 57 | ``` ` ` ``` 58 | ``` ` ``` 59 | ````` ````` 60 | ````` ````` 61 | ````` ````` 62 | ````` ````` 63 | ```""") 64 | 65 | art = [banner_1, banner_2, banner_3] 66 | print(logo) 67 | print(random.choice(art)) 68 | -------------------------------------------------------------------------------- /GoReport.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Name: GoReport v3.1 5 | Author: Christopher Maddalena 6 | 7 | This is part script and part class for interfacing with the GoPhish API. You provide an API key 8 | and host (e.g. https://ip:port) in a ``gophish.config`` file for the connection. 9 | 10 | Then provide a campaign ID as a command line argument along with your preference for the 11 | report type: python3 goreport.py --id 36 --format word 12 | 13 | The results will be fetched and put through additional processing. An Excel xlsx or Word docx file 14 | is created with all of the campaign details and some of the settings that may be of interest 15 | (e.g. SMTP hostname and other basic info). The class also performs some analysis data points, like 16 | the browser user-agents and IP addresses, to generate statistics for browser versions, operating 17 | systems, and locations. 18 | """ 19 | 20 | # 3rd Party Libraries 21 | import click 22 | 23 | # Goreport Libraries 24 | from lib import banners, goreport 25 | 26 | 27 | # Setup an AliasedGroup for CLICK 28 | class AliasedGroup(click.Group): 29 | """Allows commands to be called by their first unique character.""" 30 | 31 | def get_command(self, ctx, cmd_name): 32 | """ 33 | Allows commands to be called by their first unique character 34 | :param ctx: Context information from click 35 | :param cmd_name: Calling command name 36 | :return: 37 | """ 38 | rv = click.Group.get_command(self, ctx, cmd_name) 39 | if rv is not None: 40 | return rv 41 | matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] 42 | if not matches: 43 | return None 44 | elif len(matches) == 1: 45 | return click.Group.get_command(self, ctx, matches[0]) 46 | ctx.fail('Too many matches: %s' % ', '.join(sorted(matches))) 47 | 48 | 49 | # Create the help option for CLICK 50 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 51 | 52 | 53 | @click.group(cls=AliasedGroup, context_settings=CONTEXT_SETTINGS) 54 | def GoReport(): 55 | """Everything starts here.""" 56 | pass 57 | 58 | 59 | # Setup our CLICK arguments and help text 60 | @GoReport.command(name='report', short_help="Generate a full report for the selected campaign \ 61 | -- either CSV or DOCX.") 62 | @click.option('--id', type=click.STRING, is_flag=False, help="The target campaign's ID. You can \ 63 | provide a comma-separated list of IDs (e.g. -id #,#,#).", required=True) 64 | @click.option('--format', type=click.Choice(['excel', 'word', 'quick']), help="Use this option to \ 65 | choose between report formats.", required=True) 66 | @click.option('--combine', is_flag=True, help="Combine all results into one report. The first \ 67 | campaign ID will be used for information such as campaign name, dates, and URL.", required=False) 68 | @click.option('--complete', is_flag=True, help="Optionally mark the campaign as complete in \ 69 | Gophish.", required=False) 70 | @click.option('--config', type=click.Path(exists=True, readable=True, resolve_path=True), 71 | help="Name an alternate config file for GoReport to use. The default is \ 72 | gophish.config.") 73 | @click.option('-g', '--google', is_flag=True, help="Enables using the Google Maps Geolocate API \ 74 | to match Gophish event coordinates with an address. Requires a Geolocate API key in \ 75 | the config file.", required=False) 76 | @click.option('-v', '--verbose', is_flag=True, help="Sets verbose to true so GoReport will \ 77 | display some additional feedback, such as flagging IP mis-matches.", required=False) 78 | @click.pass_context 79 | def parse_options(self, id, format, combine, complete, config, google, verbose): 80 | """GoReport uses the Gophish API to connect to your Gophish instance using the 81 | IP address, port, and API key for your installation. This information is provided 82 | in the gophish.config file and loaded at runtime. GoReport will collect details 83 | for the specified campaign and output statistics and interesting data for you. 84 | 85 | Select campaign ID(s) to target and then select a report format.\n 86 | * csv: A comma separated file. Good for copy/pasting into other documents.\n 87 | * word: A formatted docx file. A template.docx file is required (see the README).\n 88 | * quick: Command line output of some basic stats. Good for a quick check or client call.\n 89 | """ 90 | # Print the Gophish banner 91 | banners.print_banner() 92 | # Create a new Goreport object that will use the specified report format 93 | gophish = goreport.Goreport(format, config, google, verbose) 94 | # Execute reporting for the provided list of IDs 95 | gophish.run(id, combine, complete) 96 | 97 | 98 | if __name__ == '__main__': 99 | parse_options() 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goreport v3.0, a Gophish Reporting Tool 2 | 3 | This script accepts your Gophish campaign ID(s) as a parameter and then collects the campaign results to present the statistics and perform user-agent parsing and geolocation lookups for IP addresses. Goreport generates lists of IP addresses, operating systems, browser types and versions, and locations with counts for the number of times each one was seen throughout the campaign. 4 | 5 | A note on statistics: Goreport will report the total number of events and the number of email recipients that participated in each event. In other words, Goreport will show how many times Gophish recorded a "Clicked Link" event and how many recipients clicked a link. These are very different numbers. A campaign sent to 10 people could have 9 Clicked Link events when only 3 recipients clicked a link. Knowing that recipients clicked a link or submitted data more than once is valuable information, but make sure you keep the numbers straight. 6 | 7 | ## Goreport Requirements 8 | 9 | This script requires a Gophish server, and active or complete campaign, and the API key for your Gophish application. Get this key by clicking the Settings tab. The API key will be found on the first page. Each Gophish user account has its own API key which acts as the method of authentication for that user to the Gophish API. If you use multiple accounts with Gophish, make sure you grab the correct users API key. 10 | 11 | These Python libraries are required as well: 12 | * Gophish 13 | * requests 14 | * xlsxwriter 15 | * configparser 16 | * python-docx 17 | * click 18 | * user-agents 19 | * python-dateutil (Required by the Gophish library) 20 | 21 | ## Goreport Setup 22 | 23 | You need to do a few things to get started: 24 | 25 | * Run `pip install -r requirements.txt`. 26 | * Edit/create a Gophish.config configuration file that looks like the one below. 27 | * Note: The full host URL is required, so provide http://IP:PORT or https://IP:PORT. 28 | * Be aware of using HTTP vs HTTPS. If you type in the wrong one you'll receive connection errors. 29 | * Get your campaign ID(s) by clicking your campaign(s) and referencing the URL(s) (it's the number at the end). 30 | * If you want to be able to create Word docx reports, drop a "template.docx" template file into the Goreport directory (more information below in Selecting Report Output). 31 | 32 | ## Basic Usage 33 | 34 | This example will assume Gophish is on another server and HTTPS is being used. To access the API endpoint, you will need to use SSH port forwarding with port 3333 (or any other local port you wish to use): 35 | 36 | ### Gophish.config 37 | 38 | ``` 39 | [Gophish] 40 | gp_host: https://127.0.0.1:3333 41 | api_key: 42 | 43 | [ipinfo.io] 44 | ipinfo_token: 45 | 46 | [Google] 47 | geolocate_key: 48 | ``` 49 | 50 | ### A Basic Command 51 | 52 | `python3 Goreport.py --id 26 --format excel` 53 | 54 | That would fetch the results of campaign 26 from https://localhost:3333/api/campaigns/26/?api_key= and output the results to an xlsx file. 55 | 56 | Multiple IDs can be provided at one time for multiple reports. The IDs can be provided using a comma-separated list, a range, or both. 57 | 58 | Example: `python3 Goreport.py --id 26,29-33,54 --format csv` 59 | 60 | ### Changing Config Files 61 | 62 | If you use multiple Gophish user accounts or servers, then you will have multiple API keys. To make it easier to switch between keys, Goreport's `--config` option enables you to override the default config file, gophish.config, with a config file you name. If this argument is provided with a valid, readable config file, Goreport will use it instead of gophish.config to setup the API connections. 63 | 64 | You might use this option if you have, for example, three phishing servers running Gophish. You could setup three config files, each with a different Gophish API key, and then use them as needed. 65 | 66 | Example: `python3 Goreport.py --id 26,29-33,54 --format csv --config phish_server_2.config` 67 | 68 | ### Combining Reports 69 | 70 | If you ran multiple campaigns using the same settings for different target groups, you may wish to run Goreport against these campaigns all at once and then combine the results into one report. This can be accomplished by adding Goreports `--combine` flag. 71 | 72 | Example: `python3 Goreport.py --id 26,29-33,54 --format excel --combine` 73 | 74 | This command would collect the results for campaigns 26, 29, 30, 31, 32, 33, and 54. Normally, Goreport would output seven xlsx files, but the addition of `--combine` tells Goreport to combine the results and output just one report as if they were all one large campaign. 75 | 76 | ### Switching Report Output 77 | 78 | Goreport can output either an Excel spreadsheet (xlsx) or a Word document (docx). There is also a "quick" report option. Simply select your preferred format using the `--format` command line argument, as shown above in the Sample Usage section. There is not much to say about the csv format. 79 | 80 | The Word document is built from a template, template.docx. Place your template file, named template.docx, into the Goreport directory with the main script. Your template should include a table style you want to use and heading styles for Heading 1 and Heading 1. Name your preferred table style "Goreport" and setup your Heading 1 and 2 styles. 81 | 82 | Feel free to create a custom style or use an existing style. The only thing that matters is a template.docx file exists and it has a "Goreport" table style. 83 | 84 | To rename a style, right-click the style, select Modify Table Style, and set a new name. 85 | 86 | The Excel option outputs a nicely formatted Excel workbook with multiple worksheets for the different collections of results and statistics. This is a nice option if you want to easily sort or filter result tables. 87 | 88 | Finally, there is a "quick" option. This does not generate a report document. Instead of a report, it outputs basic information about the campaign to your terminal. This is handy for quickly checking campaign progress or referencing results after campaign completion. 89 | 90 | ### Marking Campaigns as Complete 91 | 92 | If you want to set the status of a campaign to "Complete" when you run your report, Goreport can help you do this automatically with the `--complete` flag. If you provide this flag, Goreport will use the API to mark each campaign ID as "Complete" to end the campaign and update the status in Gophish. 93 | 94 | ## Additional Information 95 | 96 | Gophish performs it's own geolocation lookups with IP addresses and returns latitude and longitude. This works alright, but may fail and return coordinates of `0,0` or may return old information. 97 | 98 | Goreport has two options that might be used to improve location results. The first, and recommended option, is the ipinfo.io API. API access is free as long as you make less than 1,000 queries per 24 hour period. That should not be too difficult for a phishing campaign. 99 | 100 | If an ipinfo.io API key is added to the config file Goreport will automatically use ipinfo.io to gather current geolocation information for each unique IP address. 101 | 102 | The second option is the Google Maps API. Goreport v1.0 used the Maps API when it was free. Google now charges $0.005/request for the Geolocate API (as it is now called). If you would prefer to not use ipinfo.io, activate the Maps Geolocate API on a Google account and add the API key to the Goreport config file. Then add the `--google` flag to your Goreport command anytime you want Goreport to use the API to lookup Gophish's coordinates to get a formatted address. 103 | 104 | ## Technical Information 105 | 106 | If you'd like to review the code, here is a basic outline of the process: 107 | 108 | Goreport.py uses Python 3 and the Command Line Interface Creation Kit (CLICK) library. When the script is run, a new Goreport object is created. The `__init__` function for the Goreport class creates a connection to your Gophish server using the provided API key for authentication. Then the `run()` function is called. 109 | 110 | `Run()` uses the command line options to kick-off reporting. A For loop is used to loop through all campaign IDs provided with `--id`. Your Gophish server is contacted for campaign details for each individual ID. 111 | 112 | First, `collect_all_campaign_info()` is called to stash basic campaign information in variables. This includes data like the campaign's name, when it was run, its status, the SMTP server used, the template's name, and more. 113 | 114 | Second, `process_timeline_events()` is called to get Gophish's timeline model for the ID. This includes the events recorded by Gophish. This function runs second because it fills-in some lists that are reviewed by process_results(). 115 | 116 | Third, `process_results()` is called to get Gophish's results model for the ID. This provides data like the number of targets in the campaign. 117 | 118 | Goreport uses these steps to setup some lists to determine the basic results for the campaign, e.g. who was successfully sent an email, which recipients clicked a linked, and which recipients provided data. 119 | 120 | With this foundation, Goreport can arrange the data in any number of ways for a report. At any time, the lists can be queried to check if a certain email address in the results model appears in the `targets_clicked` list to confirm if that recipient clicked a link. That can then kick-off a review of the timeline model to collect details. Gophish keeps the details like IP address and user-agent in the timeline model and basic information in the results model. 121 | -------------------------------------------------------------------------------- /lib/goreport.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | This is the GoReport class. GoReport handles everything from connecting to the target Gophish 6 | server to pulling campaign information and reporting the results. 7 | """ 8 | 9 | try: 10 | # 3rd Party Libraries 11 | from gophish import Gophish 12 | except: 13 | print("[!] Could not import the Gophish library! Make sure it is installed.\n\ 14 | Run: `python3 -m pip intall gophish`\n\ 15 | Test it by running `python3` and then, in the \ 16 | Python prompt, typing `from gophish import Gophish`.") 17 | exit() 18 | 19 | # Standard Libraries 20 | import configparser 21 | import os.path 22 | import sys 23 | from collections import Counter 24 | 25 | # 3rd Party Libraries 26 | import requests 27 | import xlsxwriter 28 | from docx import Document 29 | from docx.enum.style import WD_STYLE_TYPE 30 | from docx.shared import Cm, Pt, RGBColor 31 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 32 | from user_agents import parse 33 | 34 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 35 | 36 | 37 | class Goreport(object): 38 | """ 39 | This class uses the Gophish library to create a new Gophish API connection 40 | and queries Gophish for information and results related to the specified 41 | campaign ID(s). 42 | """ 43 | # Name of the config file -- default is ``gophish.config`` 44 | goreport_config_file = "gophish.config" 45 | verbose = False 46 | 47 | # Variables for holding Gophish models 48 | results = None 49 | campaign = None 50 | timeline = None 51 | 52 | # Variables for holding campaign information 53 | cam_id = None 54 | cam_url = None 55 | cam_name = None 56 | cam_status = None 57 | launch_date = None 58 | created_date = None 59 | cam_page_name = None 60 | cam_smtp_host = None 61 | completed_date = None 62 | cam_redirect_url = None 63 | cam_from_address = None 64 | cam_subject_line = None 65 | cam_template_name = None 66 | cam_capturing_passwords = None 67 | cam_capturing_credentials = None 68 | 69 | # Variables and lists for tracking event numbers 70 | total_sent = 0 71 | total_opened = 0 72 | total_targets = 0 73 | total_clicked = 0 74 | total_reported = 0 75 | total_submitted = 0 76 | total_unique_opened = 0 77 | total_unique_clicked = 0 78 | total_unique_reported = 0 79 | total_unique_submitted = 0 80 | targets_opened = [] 81 | targets_clicked = [] 82 | targets_reported = [] 83 | targets_submitted = [] 84 | 85 | # Lists and dicts for holding prepared report data 86 | campaign_results_summary = [] 87 | 88 | # Lists for holding totals for statistics 89 | browsers = [] 90 | locations = [] 91 | ip_addresses = [] 92 | ip_and_location = {} 93 | operating_systems = [] 94 | 95 | # Output options 96 | report_format = None 97 | output_word_report = None 98 | output_xlsx_report = None 99 | xlsx_header_bg_color = "#0085CA" 100 | xlsx_header_font_color = "#FFFFFF" 101 | 102 | def __init__(self, report_format, config_file, google, verbose): 103 | """ 104 | Initiate the connection to the Gophish server with the provided host, port, 105 | and API key and prepare to use the external APIs. 106 | """ 107 | try: 108 | # Check if an alternate config file was provided 109 | if config_file: 110 | self.goreport_config_file = config_file 111 | # Open the config file to make sure it exists and is readable 112 | config = configparser.ConfigParser() 113 | config.read(self.goreport_config_file) 114 | except Exception as e: 115 | print(f"[!] Could not open {self.goreport_config_file} -- make sure it exists and is readable.") 116 | print(f"L.. Details: {e}") 117 | sys.exit() 118 | 119 | try: 120 | # Read in the values from the config file 121 | GP_HOST = self.config_section_map(config, 'Gophish')['gp_host'] 122 | API_KEY = self.config_section_map(config, 'Gophish')['api_key'] 123 | except Exception as e: 124 | print("[!] There was a problem reading values from the gophish.config file!") 125 | print(f"L.. Details: {e}") 126 | sys.exit() 127 | 128 | try: 129 | # Read in the values from the config file 130 | self.IPINFO_TOKEN = self.config_section_map(config, 'ipinfo.io')['ipinfo_token'] 131 | if not self.IPINFO_TOKEN: 132 | self.IPINFO_TOKEN = None 133 | except Exception as e: 134 | self.IPINFO_TOKEN = None 135 | print("[!] No ipinfo.io API token was found in the config. GoReport will not lookup IP addresses with ipinfo.io for additional location data.") 136 | print(f"L.. Details: {e}") 137 | 138 | try: 139 | # Read in the values from the config file 140 | self.GEOLOCATE_TOKEN = self.config_section_map(config, 'Google')['geolocate_key'] 141 | if not self.GEOLOCATE_TOKEN: 142 | self.GEOLOCATE_TOKEN = None 143 | except Exception as e: 144 | self.GEOLOCATE_TOKEN = None 145 | if google: 146 | print("[!] No Google Maps API token was found in the config so GoReport will ignore the `--google` flag.") 147 | print(f"L.. Details: {e}") 148 | 149 | # Set command line options for the GoReport object 150 | self.google = google 151 | self.verbose = verbose 152 | self.report_format = report_format 153 | # Connect to the Gophish API 154 | # NOTE: This step succeeds even with a bad API key, so the true test is fetching an ID 155 | print(f"[+] Connecting to Gophish at {GP_HOST}") 156 | print(f"L.. The API Authorization endpoint is: {GP_HOST}/api/campaigns/?api_key={API_KEY}") 157 | self.api = Gophish(API_KEY, host=GP_HOST, verify=False) 158 | 159 | def run(self, id_list, combine_reports, set_complete_status): 160 | """Run everything to process the target campaign.""" 161 | # Output some feedback for user options 162 | if combine_reports: 163 | print("[+] Campaign results will be combined into a single report.") 164 | if set_complete_status: 165 | print('[+] Campaign statuses will be set to "Complete" after processing the results.') 166 | try: 167 | # Create the list of campaign IDs 168 | temp_id = [] 169 | # Handle a mixed set of ranges and comma-separated IDs 170 | if "-" and "," in id_list: 171 | temp = id_list.split(",") 172 | for x in temp: 173 | if "-" in x: 174 | lower = x.split("-")[0] 175 | upper = x.split("-")[1] 176 | for y in range(int(lower), int(upper) + 1): 177 | temp_id.append(str(y)) 178 | else: 179 | temp_id.append(x) 180 | # Process IDs provided as one or more ranges 181 | elif "-" in id_list: 182 | lower = id_list.split("-")[0] 183 | upper = id_list.split("-")[1] 184 | for y in range(int(lower), int(upper) + 1): 185 | temp_id.append(str(y)) 186 | # Process single or only comma-separated IDs 187 | else: 188 | temp_id = id_list.split(",") 189 | id_list = temp_id 190 | except Exception as e: 191 | print("[!] Could not interpret your provided campaign IDs. \ 192 | Ensure the IDs are provided as comma-separated integers or interger ranges, e.g. 5,50-55,71.") 193 | print(f"L.. Details: {e}") 194 | sys.exit() 195 | # Begin processing the campaign IDs by removing any duplicates 196 | try: 197 | # Get length of user-provided list 198 | initial_len = len(id_list) 199 | # Remove duplicate IDs and sort IDs as integers 200 | id_list = sorted(set(id_list), key=int) 201 | # Get length of unique, sorted list 202 | unique_len = len(id_list) 203 | except Exception as e: 204 | temp = [] 205 | for id in id_list: 206 | try: 207 | int(id) 208 | except: 209 | temp.append(id) 210 | print(f"[!] There are {len(temp)} invalid campaign ID(s), i.e. not an integer.") 211 | print(f"L.. Offending IDs: {','.join(temp)}") 212 | print(f"L.. Details: {e}") 213 | sys.exit() 214 | print(f"[+] A total of {initial_len} campaign IDs have been provided for processing.") 215 | # If the lengths are different, then GoReport removed one or more dupes 216 | if initial_len != unique_len: 217 | dupes = initial_len - unique_len 218 | print(f"L.. GoReport found {dupes} duplicate campaign IDs, so those have been trimmed.") 219 | # Provide list of all IDs that will be processed 220 | print(f"[+] GoReport will process the following campaign IDs: {','.join(id_list)}") 221 | # If --combine is used with just one ID it can break reporting, so we catch that here 222 | if len(id_list) == 1 and combine_reports: 223 | combine_reports = False 224 | # Go through each campaign ID and get the results 225 | campaign_counter = 1 226 | for CAM_ID in id_list: 227 | print(f"[+] Now fetching results for Campaign ID {CAM_ID} ({campaign_counter}/{len(id_list)}).") 228 | try: 229 | # Request the details for the provided campaign ID 230 | self.campaign = self.api.campaigns.get(campaign_id=CAM_ID) 231 | except Exception as e: 232 | print(f"[!] There was a problem fetching this campaign {CAM_ID}'s details. Make sure your URL and API key are correct. Check HTTP vs HTTPS!") 233 | print(f"L.. Details: {e}") 234 | try: 235 | try: 236 | # Check to see if a success message was returned with a message 237 | # Possible reasons: campaign ID doesn't exist or problem with host/API key 238 | if self.campaign.success is False: 239 | print(f"[!] Failed to get results for campaign ID {CAM_ID}") 240 | print(f"L.. Details: {self.campaign.message}") 241 | # We can't let an error with an ID stop reporting, so check if this was the last ID 242 | if CAM_ID == id_list[-1] and combine_reports: 243 | self.generate_report() 244 | # If self.campaign.success does not exist then we were successful 245 | except: 246 | print("[+] Success!") 247 | # Collect campaign details and process data 248 | self.collect_all_campaign_info(combine_reports) 249 | self.process_timeline_events(combine_reports) 250 | self.process_results(combine_reports) 251 | # If the --complete flag was set, now set campaign status to Complete 252 | if set_complete_status: 253 | print(f"[+] Setting campaign ID {CAM_ID}'s status to Complete.") 254 | try: 255 | set_complete = self.api.campaigns.complete(CAM_ID) 256 | try: 257 | if set_complete.success is False: 258 | print(f"[!] Failed to set campaign status for ID {CAM_ID}.") 259 | print(f"L.. Details: {set_complete.message}") 260 | # If set_complete.success does not exist then we were successful 261 | except: 262 | pass 263 | except Exception as e: 264 | print(f"[!] Failed to set campaign status for ID {CAM_ID}.") 265 | print(f"L.. Details: {e}") 266 | # Check if this is the last campaign ID in the list 267 | # If this is the last ID and combined reports is on, generate the report 268 | if CAM_ID == id_list[-1] and combine_reports: 269 | self.generate_report() 270 | # Otherwise, if we are not combining reports, generate the reports 271 | elif combine_reports is False: 272 | self.generate_report() 273 | campaign_counter += 1 274 | except Exception as e: 275 | print(f"[!] There was a problem processing campaign ID {CAM_ID}!") 276 | print(f"L.. Details: {e}") 277 | sys.exit() 278 | 279 | def lookup_ip(self, ip): 280 | """Lookup the provided IP address with ipinfo.io for location data. 281 | 282 | Example Result: 283 | {'ip': '52.44.93.197', 284 | 'hostname': 'ec2-52-44-93-197.compute-1.amazonaws.com', 285 | 'city': 'Beaumont', 286 | 'region': 'Texas', 287 | 'country': 'US', 288 | 'loc': '30.0866,-94.1274', 289 | 'postal': '77702', 290 | 'phone': '409', 291 | 'org': 'AS14618 Amazon.com, Inc.'} 292 | """ 293 | ipinfo_url = f"https://ipinfo.io/{ip}?token={self.IPINFO_TOKEN}" 294 | try: 295 | r = requests.get(ipinfo_url) 296 | return r.json() 297 | except Exception as e: 298 | print(f"[!] Failed to lookup `{ip}` with ipinfo.io.") 299 | print(f"L.. Details: {e}") 300 | return None 301 | 302 | def get_google_location_data(self, lat, lon): 303 | """Use Google's Maps API to collect location info for the provided latitude and longitude. 304 | 305 | Google returns a bunch of JSON with a variety of location data. This function returns 306 | Google's pre-formatted `formatted_address` key for a human-readable address. 307 | """ 308 | google_maps_url = f"https://maps.googleapis.com/maps/api/geocode/json?latlng={lat},{lon}&sensor=false&key={self.GEOLOCATE_TOKEN}" 309 | r = requests.get(google_maps_url) 310 | maps_json = r.json() 311 | if r.ok: 312 | try: 313 | if "error_message" in maps_json: 314 | print(f"[!] Google Maps returned an error so using Gophish coordinates. Error: {maps_json['error_message']}") 315 | return f"{lat}, {lon}" 316 | first_result = maps_json['results'][0] 317 | if "formatted_address" in first_result: 318 | return first_result["formatted_address"] 319 | # In case that key is ever unavailable try to assemble an address 320 | else: 321 | components = first_result['address_components'] 322 | country = town = None 323 | for c in components: 324 | if "country" in c['types']: 325 | country = c['long_name'] 326 | if "locality" in c['types']: 327 | town = c['long_name'] 328 | if "administrative_area_level_1" in c['types']: 329 | state = c['long_name'] 330 | return f"{town}, {state}, {country}" 331 | except Exception as e: 332 | print("[!] Failed to parse Google Maps API results so using Gophish coordinates.") 333 | print(f"L.. Error: {e}") 334 | return f"{lat}, {lon}" 335 | else: 336 | print(f"[!] Failed to contact the Google Maps API so using Gophish coordinates. Status code: {r.status_code}") 337 | return f"{lat}, {lon}" 338 | 339 | def geolocate(self, target, ipaddr, google=False): 340 | """Attempt to get location data for the provided target and event. Will use ipinfo.io if an 341 | API key is configured. Otherwise the Gophish latitude and longitude coordinates will be 342 | returned. If `google` is set to True this function will try to match the coordinates to a 343 | location using the Google Maps API. 344 | 345 | Returns a string: City, Region, Country 346 | """ 347 | if ipaddr in self.ip_and_location: 348 | return self.ip_and_location[ipaddr] 349 | else: 350 | if self.IPINFO_TOKEN: 351 | # location_json = self.lookup_ip(event.details['browser']['address']) 352 | location_json = self.lookup_ip(ipaddr) 353 | if location_json: 354 | city = region = country = "Unknown" 355 | if "city" in location_json: 356 | if location_json['city']: 357 | city = location_json['city'] 358 | if "region" in location_json: 359 | if location_json['region']: 360 | region = location_json['region'] 361 | if "country" in location_json: 362 | if location_json['country']: 363 | country = location_json['country'] 364 | location = f"{city}, {region}, {country}" 365 | else: 366 | location = f"{target.latitude}, {target.longitude}" 367 | elif google: 368 | if self.GEOLOCATE_TOKEN: 369 | location = self.get_google_location_data(target.latitude, target.longitude) 370 | else: 371 | location = f"{target.latitude}, {target.longitude}" 372 | else: 373 | location = f"{target.latitude}, {target.longitude}" 374 | self.locations.append(location) 375 | self.ip_and_location[ipaddr] = location 376 | return location 377 | 378 | def compare_ip_addresses(self, target_ip, browser_ip, verbose): 379 | """Compare the IP addresses of the target to that of an event. The goal: Looking for a 380 | mismatch that might identify some sort of interesting event. This might indicate an 381 | email was forwarded, a VPN was switched on/off, or maybe the target is at home. 382 | """ 383 | if target_ip == browser_ip: 384 | return target_ip 385 | else: 386 | # We have an IP mismatch -- hard to tell why this might be. 387 | if verbose: 388 | print(f"[*] Event: This target's ({target_ip}) URL was clicked from a browser at {browser_ip}.") 389 | # This is an IP address not included in the results model, so we add it to our list here 390 | self.ip_addresses.append(browser_ip) 391 | return browser_ip 392 | 393 | def get_basic_campaign_info(self): 394 | """"Helper function to collect a campaign's basic details. This includes campaign name, 395 | status, template, and other details that are not the campaign's results. 396 | 397 | This keeps these calls in one place for tidiness and easier management. 398 | """ 399 | self.cam_name = self.campaign.name 400 | self.cam_status = self.campaign.status 401 | self.created_date = self.campaign.created_date 402 | self.launch_date = self.campaign.launch_date 403 | self.completed_date = self.campaign.completed_date 404 | self.cam_url = self.campaign.url 405 | 406 | # Collect SMTP information 407 | self.smtp = self.campaign.smtp 408 | self.cam_from_address = self.smtp.from_address 409 | self.cam_smtp_host = self.smtp.host 410 | 411 | # Collect the template information 412 | self.template = self.campaign.template 413 | self.cam_subject_line = self.template.subject 414 | self.cam_template_name = self.template.name 415 | self.cam_template_attachments = self.template.attachments 416 | if self.cam_template_attachments == []: 417 | self.cam_template_attachments = "None Used" 418 | 419 | # Collect the landing page information 420 | self.page = self.campaign.page 421 | self.cam_page_name = self.page.name 422 | self.cam_redirect_url = self.page.redirect_url 423 | if self.cam_redirect_url == "": 424 | self.cam_redirect_url = "Not Used" 425 | self.cam_capturing_passwords = self.page.capture_passwords 426 | self.cam_capturing_credentials = self.page.capture_credentials 427 | 428 | def collect_all_campaign_info(self, combine_reports): 429 | """Collect the campaign's details and set values for each of the variables.""" 430 | # Collect the basic campaign details 431 | try: 432 | # Begin by checking if the ID is valid 433 | self.cam_id = self.campaign.id 434 | if combine_reports and self.cam_name is None: 435 | print(f"[+] Reports will be combined -- setting name, dates, and URL based on campaign ID {self.cam_id}.") 436 | self.get_basic_campaign_info() 437 | elif combine_reports is False: 438 | self.get_basic_campaign_info() 439 | # Collect the results and timeline lists 440 | if self.results is None: 441 | self.results = self.campaign.results 442 | self.timeline = self.campaign.timeline 443 | elif combine_reports: 444 | self.results += self.campaign.results 445 | self.timeline += self.campaign.timeline 446 | else: 447 | self.results = self.campaign.results 448 | self.timeline = self.campaign.timeline 449 | except: 450 | print(f"[!] Looks like campaign ID {self.cam_id} does not exist! Skipping it...") 451 | 452 | def process_results(self, combine_reports): 453 | """Process the results model to collect basic data, like total targets and event details. 454 | This should be run after the process_timeline_events() function which creates the 455 | targets_* lists. 456 | 457 | The results model can provide: 458 | first_name, last_name, email, position, and IP address 459 | """ 460 | # Total length of results gives us the total number of targets 461 | if combine_reports and self.total_targets is None: 462 | self.total_targets = len(self.campaign.results) 463 | elif combine_reports: 464 | self.total_targets += len(self.campaign.results) 465 | else: 466 | # Not combining, so reset counters 467 | self.total_unique_opened = 0 468 | self.total_unique_clicked = 0 469 | self.total_unique_reported = 0 470 | self.total_unique_submitted = 0 471 | # Reports will not be combined, so reset tracking between reports 472 | self.total_targets = len(self.campaign.results) 473 | self.ip_addresses = [] 474 | self.campaign_results_summary = [] 475 | # Go through all results and extract data for statistics 476 | for target in self.campaign.results: 477 | temp_dict = {} 478 | # Log the IP address for additional statistics later 479 | if not target.ip == "": 480 | self.ip_addresses.append(target.ip) 481 | self.geolocate(target, target.ip, self.google) 482 | # Add all of the recipient's details and results to the temp dictionary 483 | temp_dict["email"] = target.email 484 | temp_dict["fname"] = target.first_name 485 | temp_dict["lname"] = target.last_name 486 | position = "None Provided" 487 | if target.position: 488 | position = target.position 489 | temp_dict["position"] = position 490 | temp_dict["ip_address"] = target.ip 491 | # Check if this target was recorded as viewing the email (tracking image) 492 | if target.email in self.targets_opened: 493 | temp_dict["opened"] = True 494 | self.total_unique_opened += 1 495 | else: 496 | temp_dict["opened"] = False 497 | # Check if this target clicked the link 498 | if target.email in self.targets_clicked: 499 | temp_dict["clicked"] = True 500 | self.total_unique_clicked += 1 501 | # Incremement the total number of opens for this target if they clicked 502 | # but did not display the tracking image in the email 503 | if target.email not in self.targets_opened: 504 | self.total_unique_opened += 1 505 | else: 506 | temp_dict["clicked"] = False 507 | # Check if this target submitted data 508 | if target.email in self.targets_submitted: 509 | temp_dict["submitted"] = True 510 | self.total_unique_submitted += 1 511 | else: 512 | temp_dict["submitted"] = False 513 | # Check if this target reported the email 514 | if target.email in self.targets_reported: 515 | temp_dict["reported"] = True 516 | self.total_unique_reported += 1 517 | else: 518 | temp_dict["reported"] = False 519 | # Append the temp dictionary to the event summary list 520 | self.campaign_results_summary.append(temp_dict) 521 | 522 | def process_timeline_events(self, combine_reports): 523 | """Process the timeline model to collect basic data, like total clicks, and get detailed 524 | event data for recipients. 525 | 526 | The timeline model contains all events that occurred during the campaign. 527 | """ 528 | # Create counters for enumeration 529 | sent_counter = 0 530 | click_counter = 0 531 | opened_counter = 0 532 | reported_counter = 0 533 | submitted_counter = 0 534 | 535 | # Reset target lists 536 | self.targets_opened = [] 537 | self.targets_clicked = [] 538 | self.targets_reported = [] 539 | self.targets_submitted = [] 540 | # Run through all events and count each of the four basic events 541 | for event in self.campaign.timeline: 542 | if event.message == "Email Sent": 543 | sent_counter += 1 544 | elif event.message == "Email Opened": 545 | opened_counter += 1 546 | self.targets_opened.append(event.email) 547 | elif event.message == "Clicked Link": 548 | click_counter += 1 549 | self.targets_clicked.append(event.email) 550 | elif event.message == "Submitted Data": 551 | submitted_counter += 1 552 | self.targets_submitted.append(event.email) 553 | elif event.message == "Email Reported": 554 | reported_counter += 1 555 | self.targets_reported.append(event.email) 556 | # Assign the counter values to our tracking lists 557 | if combine_reports: 558 | # Append, +=, totals if combining reports 559 | self.total_sent += sent_counter 560 | self.total_opened += opened_counter 561 | self.total_clicked += click_counter 562 | self.total_reported += reported_counter 563 | self.total_submitted += submitted_counter 564 | else: 565 | # Set tracking variables to current counter values for non-combined reports 566 | self.total_sent = sent_counter 567 | self.total_opened = opened_counter 568 | self.total_clicked = click_counter 569 | self.total_reported = reported_counter 570 | self.total_submitted = submitted_counter 571 | 572 | def generate_report(self): 573 | """Determines which type of report generate and the calls the appropriate reporting 574 | functions. 575 | """ 576 | if self.report_format == "excel": 577 | print("[+] Building the report -- you selected a Excel/xlsx report.") 578 | self.output_xlsx_report = self._build_output_xlsx_file_name() 579 | self.write_xlsx_report() 580 | elif self.report_format == "word": 581 | print("[+] Building the report -- you selected a Word/docx report.") 582 | print("[+] Looking for the template.docx to be used for the Word report.") 583 | if os.path.isfile("template.docx"): 584 | print("[+] Template was found -- proceeding with report generation...") 585 | print("L.. Word reports can take a while if you had a lot of recipients.") 586 | self.output_word_report = self._build_output_word_file_name() 587 | self.write_word_report() 588 | else: 589 | print("[!] Could not find the template document! Make sure 'template.docx' is in the GoReport directory.") 590 | sys.exit() 591 | elif self.report_format == "quick": 592 | print("[+] Quick report stats:") 593 | self.get_quick_stats() 594 | 595 | def get_quick_stats(self): 596 | """Present quick stats for the campaign. Just basic numbers and some details.""" 597 | print() 598 | print(self.cam_name) 599 | print(f"Status:\t\t{self.cam_status}") 600 | print(f"Created:\t{self.created_date.split('T')[1].split('.')[0]} on {self.created_date.split('T')[0]}") 601 | print(f"Started:\t{self.launch_date.split('T')[1].split('.')[0]} on {self.launch_date.split('T')[0]}") 602 | if self.cam_status == "Completed": 603 | print(f"Completed:\t{self.completed_date.split('T')[1].split('.')[0]} on {self.completed_date.split('T')[0]}") 604 | print() 605 | print(f"Total Targets:\t{self.total_targets}") 606 | print(f"Emails Sent:\t{self.total_sent}") 607 | print(f"IPs Seen:\t{len(self.ip_addresses)}") 608 | print() 609 | print(f"Total Opened Events:\t\t{self.total_opened}") 610 | print(f"Total Click Events:\t\t{self.total_clicked}") 611 | print(f"Total Submitted Data Events:\t{self.total_submitted}") 612 | print() 613 | print(f"Individuals Who Opened:\t\t\t{self.total_unique_opened}") 614 | print(f"Individuals Who Clicked:\t\t{self.total_unique_clicked}") 615 | print(f"Individuals Who Entered Data:\t\t{self.total_unique_submitted}") 616 | print(f"Individuals Who Reported the Email:\t{self.total_unique_reported}") 617 | 618 | def _build_output_xlsx_file_name(self): 619 | """Create the xlsx report name.""" 620 | safe_name = "".join([c for c in self.cam_name if c.isalpha() or c.isdigit() or c == " "]).rstrip() 621 | xlsx_report = f"Gophish Results for {safe_name}.xlsx" 622 | return xlsx_report 623 | 624 | def _build_output_word_file_name(self): 625 | """Create the docx report name.""" 626 | safe_name = "".join([c for c in self.cam_name if c.isalpha() or c.isdigit() or c == " "]).rstrip() 627 | word_report = f"Gophish Results for {safe_name}.docx" 628 | return word_report 629 | 630 | def _set_word_column_width(self, column, width): 631 | """Custom function for quickly and easily setting the width of a table's column in the Word 632 | docx output. 633 | 634 | This option is missing from the basic Python-docx library. 635 | """ 636 | for cell in column.cells: 637 | cell.width = width 638 | 639 | def write_xlsx_report(self): 640 | """Assemble and output the xlsx file report. 641 | 642 | Throughout this function, results are assembled by adding commas and then adding to a 643 | results string, i.e. 'result_A' and then 'result_A' += ',result_B'. This is so the 644 | result can be written to the csv file and have the different pieces end up in the correct 645 | columns. 646 | """ 647 | goreport_xlsx = xlsxwriter.Workbook(self.output_xlsx_report) 648 | # Bold format 649 | bold_format = goreport_xlsx.add_format({'bold': True}) 650 | bold_format.set_text_wrap() 651 | bold_format.set_align('vcenter') 652 | # Centered format 653 | center_format = goreport_xlsx.add_format() 654 | center_format.set_text_wrap() 655 | center_format.set_align('vcenter') 656 | center_format.set_align('center') 657 | # Header format 658 | header_format = goreport_xlsx.add_format({'bold': True}) 659 | header_format.set_text_wrap() 660 | header_format.set_align('vcenter') 661 | header_format.set_bg_color(self.xlsx_header_bg_color) 662 | header_format.set_font_color(self.xlsx_header_font_color) 663 | # Number cells 664 | num_format = goreport_xlsx.add_format() 665 | num_format.set_align('center') 666 | # Boolean cells - True 667 | true_format = goreport_xlsx.add_format({'bold': True}) 668 | true_format.set_text_wrap() 669 | true_format.set_align('vcenter') 670 | true_format.set_font_color("#9C0006") 671 | true_format.set_bg_color("#FFC7CE") 672 | # Boolean cells - True 673 | false_format = goreport_xlsx.add_format() 674 | false_format.set_text_wrap() 675 | false_format.set_align('vcenter') 676 | false_format.set_font_color("#006100") 677 | false_format.set_bg_color("#C6EFCE") 678 | # Remaining cells 679 | wrap_format = goreport_xlsx.add_format() 680 | wrap_format.set_text_wrap() 681 | wrap_format.set_align('vcenter') 682 | 683 | worksheet = goreport_xlsx.add_worksheet("Overview") 684 | col = 0 685 | row = 0 686 | 687 | worksheet.set_column(0, 10, 62) 688 | 689 | worksheet.write(row, col, "Campaign Results For:", bold_format) 690 | worksheet.write(row, col + 1, f"{self.cam_name}", wrap_format) 691 | row += 1 692 | worksheet.write(row, col, "Status", bold_format) 693 | worksheet.write(row, col + 1, f"{self.cam_status}", wrap_format) 694 | row += 1 695 | worksheet.write(row, col, "Created", bold_format) 696 | worksheet.write(row, col + 1, f"{self.created_date}", wrap_format) 697 | row += 1 698 | worksheet.write(row, col, "Started", bold_format) 699 | worksheet.write(row, col + 1, f"{self.launch_date}", wrap_format) 700 | row += 1 701 | if self.cam_status == "Completed": 702 | worksheet.write(row, col, "Completed", bold_format) 703 | worksheet.write(row, col + 1, f"{self.completed_date}", wrap_format) 704 | row += 1 705 | 706 | worksheet.write(row, col, "") 707 | row += 1 708 | 709 | worksheet.write(row, col, "Campaign Details", bold_format) 710 | row += 1 711 | worksheet.write(row, col, "From", bold_format) 712 | worksheet.write(row, col + 1, f"{self.cam_from_address}", wrap_format) 713 | row += 1 714 | worksheet.write(row, col, "Subject", bold_format) 715 | worksheet.write(row, col + 1, f"{self.cam_subject_line}", wrap_format) 716 | row += 1 717 | worksheet.write(row, col, "Phish URL", bold_format) 718 | worksheet.write(row, col + 1, f"{self.cam_url}", wrap_format) 719 | row += 1 720 | worksheet.write(row, col, "Redirect URL", bold_format) 721 | worksheet.write(row, col + 1, f"{self.cam_redirect_url}", wrap_format) 722 | row += 1 723 | worksheet.write(row, col, "Attachment(s)", bold_format) 724 | worksheet.write(row, col + 1, f"{self.cam_template_attachments}", wrap_format) 725 | row += 1 726 | worksheet.write(row, col, "Captured Passwords", bold_format) 727 | worksheet.write(row, col + 1, f"{self.cam_capturing_credentials}", wrap_format) 728 | row += 1 729 | worksheet.write(row, col, "Stored Passwords", bold_format) 730 | worksheet.write(row, col + 1, f"{self.cam_capturing_passwords}", wrap_format) 731 | row += 1 732 | 733 | worksheet.write(row, col, "") 734 | row += 1 735 | 736 | # Write a high level summary for stats 737 | worksheet.write(row, col, "High Level Results", bold_format) 738 | row += 1 739 | worksheet.write(row, col, "Total Targets", bold_format) 740 | worksheet.write(row, col + 1, self.total_targets, num_format) 741 | row += 1 742 | 743 | worksheet.write(row, col, "The following totals indicate how many events of each type Gophish recorded:", wrap_format) 744 | row += 1 745 | worksheet.write(row, col, "Total Opened Events", bold_format) 746 | worksheet.write_number(row, col + 1, self.total_opened, num_format) 747 | row += 1 748 | worksheet.write(row, col, "Total Clicked Events", bold_format) 749 | worksheet.write_number(row, col + 1, self.total_clicked, num_format) 750 | row += 1 751 | worksheet.write(row, col, "Total Submitted Data Events", bold_format) 752 | worksheet.write(row, col + 1, "", wrap_format) 753 | row += 1 754 | worksheet.write(row, col, "Total Report Events", bold_format) 755 | worksheet.write_number(row, col + 1, self.total_reported, num_format) 756 | row += 1 757 | 758 | worksheet.write(row, col, "The following totals indicate how many targets participated in each event type:", wrap_format) 759 | row += 1 760 | worksheet.write(row, col, "Individuals Who Opened", bold_format) 761 | worksheet.write_number(row, col + 1, self.total_unique_opened, num_format) 762 | row += 1 763 | worksheet.write(row, col, "Individuals Who Clicked", bold_format) 764 | worksheet.write_number(row, col + 1, self.total_unique_clicked, num_format) 765 | row += 1 766 | worksheet.write(row, col, "Individuals Who Submitted Data", bold_format) 767 | worksheet.write_number(row, col + 1, self.total_unique_submitted, num_format) 768 | row += 1 769 | worksheet.write(row, col, "Individuals Who Reported", bold_format) 770 | worksheet.write_number(row, col + 1, self.total_unique_reported, num_format) 771 | row += 1 772 | 773 | worksheet.write(row, col, "") 774 | row += 1 775 | 776 | worksheet = goreport_xlsx.add_worksheet("Summary") 777 | row = 0 778 | col = 0 779 | 780 | worksheet.set_column(0, 10, 20) 781 | 782 | worksheet.write(row, col, "Summary of Events", bold_format) 783 | row += 1 784 | 785 | header_col = 0 786 | headers = ["Email Address", "Open", "Click", "Creds", "Report", "OS", "Browser"] 787 | for header in headers: 788 | worksheet.write(row, header_col, header, header_format) 789 | header_col += 1 790 | row += 1 791 | 792 | # Sort campaign summary by each dict's email entry and then create results table 793 | target_counter = 0 794 | ordered_results = sorted(self.campaign_results_summary, key=lambda k: k['email']) 795 | for target in ordered_results: 796 | worksheet.write(row, col, target['email'], wrap_format) 797 | if target['opened']: 798 | worksheet.write_boolean(row, col + 1, target['opened'], true_format) 799 | else: 800 | worksheet.write_boolean(row, col + 1, target['opened'], false_format) 801 | if target['clicked']: 802 | worksheet.write_boolean(row, col + 2, target['clicked'], true_format) 803 | else: 804 | worksheet.write_boolean(row, col + 2, target['clicked'], false_format) 805 | if target['submitted']: 806 | worksheet.write_boolean(row, col + 3, target['submitted'], true_format) 807 | else: 808 | worksheet.write_boolean(row, col + 3, target['submitted'], false_format) 809 | if target['reported']: 810 | worksheet.write_boolean(row, col + 4, target['reported'], true_format) 811 | else: 812 | worksheet.write_boolean(row, col + 4, target['reported'], false_format) 813 | if target['email'] in self.targets_clicked: 814 | for event in self.timeline: 815 | if event.message == "Clicked Link" and event.email == target['email']: 816 | user_agent = parse(event.details['browser']['user-agent']) 817 | browser_details = user_agent.browser.family + " " + \ 818 | user_agent.browser.version_string 819 | os_details = user_agent.os.family + " " + user_agent.os.version_string 820 | worksheet.write(row, col + 5, browser_details, wrap_format) 821 | worksheet.write(row, col + 6, os_details, wrap_format) 822 | else: 823 | worksheet.write(row, col + 5, "N/A", wrap_format) 824 | worksheet.write(row, col + 6, "N/A", wrap_format) 825 | row += 1 826 | target_counter += 1 827 | print(f"[+] Created row for {target_counter} of {self.total_targets}.") 828 | 829 | print("[+] Finished writing events summary...") 830 | print("[+] Detailed results analysis is next and will take some time if you had a lot of targets...") 831 | # End of the event summary and beginning of the detailed results 832 | 833 | worksheet = goreport_xlsx.add_worksheet("Event Details") 834 | row = 0 835 | col = 0 836 | 837 | worksheet.set_column(0, 10, 40) 838 | 839 | worksheet.write(row, col, "Detailed Analysis", bold_format) 840 | row += 1 841 | 842 | target_counter = 0 843 | for target in self.results: 844 | # Only create a Detailed Analysis section for targets with clicks 845 | if target.email in self.targets_clicked: 846 | position = "" 847 | if target.position: 848 | position = f"({target.position})" 849 | worksheet.write(row, col, f"{target.first_name} {target.last_name} {position}", bold_format) 850 | row += 1 851 | worksheet.write(row, col, target.email, wrap_format) 852 | row += 1 853 | # Go through all events to find events for this target 854 | for event in self.timeline: 855 | if event.message == "Email Sent" and event.email == target.email: 856 | # Parse the timestamp into separate date and time variables 857 | temp = event.time.split('T') 858 | sent_date = temp[0] 859 | sent_time = temp[1].split('.')[0] 860 | # Record the email sent date and time in the report 861 | worksheet.write(row, col, f"Sent on {sent_date.replace(',', '')} at {sent_time}", wrap_format) 862 | row += 1 863 | 864 | if event.message == "Email Opened" and event.email == target.email: 865 | # Record the email preview date and time in the report 866 | temp = event.time.split('T') 867 | worksheet.write(row, col, f"Email Preview at {temp[0]} {temp[1].split('.')[0]}", wrap_format) 868 | row += 1 869 | 870 | if event.message == "Clicked Link" and event.email == target.email: 871 | worksheet.write(row, col, "Email Link Clicked", bold_format) 872 | row += 1 873 | 874 | header_col = 0 875 | headers = ["Time", "IP", "Location", "Browser", "Operating System"] 876 | for header in headers: 877 | worksheet.write(row, header_col, header, header_format) 878 | header_col += 1 879 | row += 1 880 | 881 | temp = event.time.split('T') 882 | worksheet.write(row, col, f"{temp[0]} {temp[1].split('.')[0]}", wrap_format) 883 | 884 | # Check if browser IP matches the target's IP and record result 885 | ip_comparison = self.compare_ip_addresses(target.ip, 886 | event.details['browser']['address'], 887 | self.verbose) 888 | worksheet.write(row, col + 1, f"{ip_comparison}", wrap_format) 889 | 890 | # Parse the location data 891 | loc = self.geolocate(target, event.details['browser']['address'], self.google) 892 | worksheet.write(row, col + 2, loc, wrap_format) 893 | 894 | # Parse the user-agent string and add browser and OS details 895 | user_agent = parse(event.details['browser']['user-agent']) 896 | browser_details = user_agent.browser.family + " " + \ 897 | user_agent.browser.version_string 898 | worksheet.write(row, col + 3, browser_details, wrap_format) 899 | self.browsers.append(browser_details) 900 | 901 | os_details = user_agent.os.family + " " + user_agent.os.version_string 902 | worksheet.write(row, col + 4, os_details, wrap_format) 903 | self.operating_systems.append(os_details) 904 | row += 1 905 | 906 | if event.message == "Submitted Data" and event.email == target.email: 907 | # Now we have events for submitted data. A few notes on this: 908 | # 1. There is no expectation of a Submit event without a Clicked Link event 909 | # 2. Assuming that, the following process does NOT flag IP mismatches 910 | # or add to the list of seen locations, OSs, IPs, or browsers. 911 | worksheet.write(row, col, "Submitted Data Captured", bold_format) 912 | row += 1 913 | 914 | header_col = 0 915 | headers = ["Time", "IP", "Location", "Browser", "Operating System", "Data Captured"] 916 | for header in headers: 917 | worksheet.write(row, header_col, header, header_format) 918 | header_col += 1 919 | row += 1 920 | 921 | temp = event.time.split('T') 922 | worksheet.write(row, col, f"{temp[0]} {temp[1].split('.')[0]}", wrap_format) 923 | 924 | worksheet.write(row, col + 1, f"{event.details['browser']['address']}", wrap_format) 925 | 926 | loc = self.geolocate(target, event.details['browser']['address'], self.google) 927 | worksheet.write(row, col + 2, loc, wrap_format) 928 | 929 | user_agent = parse(event.details['browser']['user-agent']) 930 | browser_details = user_agent.browser.family + " " + \ 931 | user_agent.browser.version_string 932 | worksheet.write(row, col + 3, browser_details, wrap_format) 933 | 934 | os_details = user_agent.os.family + " " + user_agent.os.version_string 935 | worksheet.write(row, col + 4, os_details, wrap_format) 936 | 937 | # Get just the submitted data from the event's payload 938 | submitted_data = "" 939 | data_payload = event.details['payload'] 940 | # Get all of the submitted data 941 | for key, value in data_payload.items(): 942 | # To get just submitted data, we drop the 'rid' key 943 | if not key == "rid": 944 | submitted_data += f"{key}:{str(value).strip('[').strip(']')}" 945 | worksheet.write(row, col + 5, submitted_data, wrap_format) 946 | row += 1 947 | 948 | target_counter += 1 949 | print(f"[+] Processed detailed analysis for {target_counter} of {self.total_targets}.") 950 | else: 951 | # This target had no clicked or submitted events so move on to next 952 | target_counter += 1 953 | print(f"[+] Processed detailed analysis for {target_counter} of {self.total_targets}.") 954 | continue 955 | worksheet.write(row, col, "") 956 | row += 1 957 | 958 | print("[+] Finished writing detailed analysis...") 959 | 960 | worksheet = goreport_xlsx.add_worksheet("Stats") 961 | row = 0 962 | col = 0 963 | 964 | worksheet.set_column(0, 10, 35) 965 | 966 | worksheet.write(row, col, "Recorded Browsers Based on User-Agents:", bold_format) 967 | row += 1 968 | 969 | header_col = 0 970 | headers = ["Browser", "Seen"] 971 | for header in headers: 972 | worksheet.write(row, header_col, header, header_format) 973 | header_col += 1 974 | row += 1 975 | counted_browsers = Counter(self.browsers) 976 | for key, value in counted_browsers.items(): 977 | worksheet.write(row, col, f"{key}", wrap_format) 978 | worksheet.write_number(row, col + 1, value, num_format) 979 | row += 1 980 | 981 | worksheet.write(row, col, "") 982 | row += 1 983 | 984 | worksheet.write(row, col, "Record OS From Browser User-Agents:", bold_format) 985 | row += 1 986 | header_col = 0 987 | headers = ["Operating System", "Seen"] 988 | for header in headers: 989 | worksheet.write(row, header_col, header, header_format) 990 | header_col += 1 991 | row += 1 992 | counted_os = Counter(self.operating_systems) 993 | for key, value in counted_os.items(): 994 | worksheet.write(row, col, f"{key}", wrap_format) 995 | worksheet.write_number(row, col + 1, value, num_format) 996 | row += 1 997 | 998 | worksheet.write(row, col, "") 999 | row += 1 1000 | 1001 | worksheet.write(row, col, "Recorded Locations from IPs:", bold_format) 1002 | row += 1 1003 | header_col = 0 1004 | headers = ["Locations", "Seen"] 1005 | for header in headers: 1006 | worksheet.write(row, header_col, header, header_format) 1007 | header_col += 1 1008 | row += 1 1009 | counted_locations = Counter(self.locations) 1010 | for key, value in counted_locations.items(): 1011 | worksheet.write(row, col, f"{key}", wrap_format) 1012 | worksheet.write_number(row, col + 1, value, num_format) 1013 | row += 1 1014 | 1015 | worksheet.write(row, col, "") 1016 | row += 1 1017 | 1018 | worksheet.write(row, col, "Recorded IPs:", bold_format) 1019 | row += 1 1020 | header_col = 0 1021 | headers = ["IP Address", "Seen"] 1022 | for header in headers: 1023 | worksheet.write(row, header_col, header, header_format) 1024 | header_col += 1 1025 | row += 1 1026 | counted_ip_addresses = Counter(self.ip_addresses) 1027 | for key, value in counted_ip_addresses.items(): 1028 | worksheet.write(row, col, f"{key}", wrap_format) 1029 | worksheet.write_number(row, col + 1, value, num_format) 1030 | row += 1 1031 | 1032 | worksheet.write(row, col, "Recorded IPs and Locations:", bold_format) 1033 | row += 1 1034 | header_col = 0 1035 | headers = ["IP Address", "Location"] 1036 | for header in headers: 1037 | worksheet.write(row, header_col, header, header_format) 1038 | header_col += 1 1039 | row += 1 1040 | for key, value in self.ip_and_location.items(): 1041 | worksheet.write(row, col, f"{key}", wrap_format) 1042 | worksheet.write(row, col + 1, f"{value}", wrap_format) 1043 | row += 1 1044 | 1045 | goreport_xlsx.close() 1046 | print(f"[+] Done! Check '{self.output_xlsx_report}' for your results.") 1047 | 1048 | def write_word_report(self): 1049 | """Assemble and output the Word docx file report.""" 1050 | # Create document writer using the template and a style editor 1051 | d = Document("template.docx") 1052 | styles = d.styles 1053 | 1054 | # Create a custom styles for table cells 1055 | _ = styles.add_style("Cell Text", WD_STYLE_TYPE.CHARACTER) 1056 | cell_text = d.styles["Cell Text"] 1057 | cell_text_font = cell_text.font 1058 | cell_text_font.name = "Calibri" 1059 | cell_text_font.size = Pt(12) 1060 | cell_text_font.bold = True 1061 | cell_text_font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) 1062 | 1063 | _ = styles.add_style("Cell Text Hit", WD_STYLE_TYPE.CHARACTER) 1064 | cell_text_hit = d.styles["Cell Text Hit"] 1065 | cell_text_hit_font = cell_text_hit.font 1066 | cell_text_hit_font.name = "Calibri" 1067 | cell_text_hit_font.size = Pt(12) 1068 | cell_text_hit_font.bold = True 1069 | cell_text_hit_font.color.rgb = RGBColor(0x00, 0x96, 0x00) 1070 | 1071 | _ = styles.add_style("Cell Text Miss", WD_STYLE_TYPE.CHARACTER) 1072 | cell_text_miss = d.styles["Cell Text Miss"] 1073 | cell_text_miss_font = cell_text_miss.font 1074 | cell_text_miss_font.name = "Calibri" 1075 | cell_text_miss_font.size = Pt(12) 1076 | cell_text_miss_font.bold = True 1077 | cell_text_miss_font.color.rgb = RGBColor(0xFF, 0x00, 0x00) 1078 | 1079 | # Write a campaign summary at the top of the report 1080 | d.add_heading("Executive Summary", 1) 1081 | p = d.add_paragraph() 1082 | run = p.add_run(f"Campaign Results For: {self.cam_name}") 1083 | run.bold = True 1084 | # Runs are basically "runs" of text and must be aligned like we want 1085 | # them aligned in the report -- thus they are pushed left 1086 | if self.cam_status == "Completed": 1087 | completed_status = f"Completed:\t{self.completed_date.split('T')[1].split('.')[0]} on {self.completed_date.split('T')[0]}" 1088 | else: 1089 | completed_status = "Still Active" 1090 | p.add_run(f""" 1091 | Status: {self.cam_status} 1092 | Created: {self.created_date.split('T')[1].split('.')[0]} on {self.created_date.split('T')[0]} 1093 | Started: {self.launch_date.split('T')[1].split('.')[0]} on {self.launch_date.split('T')[0]} 1094 | Completed: {completed_status} 1095 | 1096 | """) 1097 | if self.cam_status == "Completed": 1098 | print() 1099 | 1100 | # Write the campaign details -- email details and template settings 1101 | run = p.add_run("Campaign Details") 1102 | run.bold = True 1103 | p.add_run(f""" 1104 | From: {self.cam_from_address} 1105 | Subject: {self.cam_subject_line} 1106 | Phish URL: {self.cam_url} 1107 | Redirect URL: {self.cam_redirect_url} 1108 | Attachment(s): {self.cam_template_attachments} 1109 | Captured Credentials: {self.cam_capturing_credentials} 1110 | Stored Passwords: {self.cam_capturing_passwords} 1111 | 1112 | """) 1113 | 1114 | # Write a high level summary for stats 1115 | run = p.add_run("High Level Results") 1116 | run.bold = True 1117 | p.add_run(f""" 1118 | Total Targets: {self.total_targets} 1119 | 1120 | The following totals indicate how many events of each type Gophish recorded: 1121 | Total Open Events: {self.total_opened} 1122 | Total Click Events: {self.total_clicked} 1123 | Total Report Events: {self.total_reported} 1124 | Total Submitted Data Events: {self.total_submitted} 1125 | 1126 | The following totals indicate how many targets participated in each event type: 1127 | Individuals Who Opened: {self.total_unique_opened} 1128 | Individuals Who Clicked: {self.total_unique_clicked} 1129 | Individuals Who Reported: {self.total_unique_reported} 1130 | Individuals Who Submitted: {self.total_unique_submitted} 1131 | 1132 | """) 1133 | d.add_page_break() 1134 | 1135 | print("[+] Finished writing high level summary...") 1136 | # End of the campaign summary and beginning of the event summary 1137 | d.add_heading("Summary of Events", 1) 1138 | d.add_paragraph("The following table summarizes who opened and clicked on emails sent in this campaign.") 1139 | 1140 | # Create a table to hold the event summary results 1141 | table = d.add_table(rows=len(self.campaign_results_summary) + 1, cols=7, style="GoReport") 1142 | 1143 | header0 = table.cell(0, 0) 1144 | header0.text = "" 1145 | header0.paragraphs[0].add_run("Email Address", "Cell Text").bold = True 1146 | 1147 | header1 = table.cell(0, 1) 1148 | header1.text = "" 1149 | header1.paragraphs[0].add_run("Open", "Cell Text").bold = True 1150 | 1151 | header2 = table.cell(0, 2) 1152 | header2.text = "" 1153 | header2.paragraphs[0].add_run("Click", "Cell Text").bold = True 1154 | 1155 | header3 = table.cell(0, 3) 1156 | header3.text = "" 1157 | header3.paragraphs[0].add_run("Data", "Cell Text").bold = True 1158 | 1159 | header4 = table.cell(0, 4) 1160 | header4.text = "" 1161 | header4.paragraphs[0].add_run("Report", "Cell Text").bold = True 1162 | 1163 | header5 = table.cell(0, 5) 1164 | header5.text = "" 1165 | header5.paragraphs[0].add_run("OS", "Cell Text").bold = True 1166 | 1167 | header6 = table.cell(0, 6) 1168 | header6.text = "" 1169 | header6.paragraphs[0].add_run("Browser", "Cell Text").bold = True 1170 | 1171 | # Sort campaign summary by each dict's email entry and then create results table 1172 | target_counter = 0 1173 | counter = 1 1174 | ordered_results = sorted(self.campaign_results_summary, key=lambda k: k['email']) 1175 | for target in ordered_results: 1176 | email_cell = table.cell(counter, 0) 1177 | email_cell.text = f"{target['email']}" 1178 | 1179 | temp_cell = table.cell(counter, 1) 1180 | if target['opened']: 1181 | temp_cell.paragraphs[0].add_run(u'\u2713', "Cell Text Hit") 1182 | else: 1183 | temp_cell.paragraphs[0].add_run(u'\u2718', "Cell Text Miss") 1184 | 1185 | temp_cell = table.cell(counter, 2) 1186 | if target['clicked']: 1187 | temp_cell.paragraphs[0].add_run(u'\u2713', "Cell Text Hit") 1188 | else: 1189 | temp_cell.paragraphs[0].add_run(u'\u2718', "Cell Text Miss") 1190 | 1191 | temp_cell = table.cell(counter, 3) 1192 | if target['submitted']: 1193 | temp_cell.paragraphs[0].add_run(u'\u2713', "Cell Text Hit") 1194 | else: 1195 | temp_cell.paragraphs[0].add_run(u'\u2718', "Cell Text Miss") 1196 | 1197 | temp_cell = table.cell(counter, 4) 1198 | if target['reported']: 1199 | temp_cell.paragraphs[0].add_run(u'\u2713', "Cell Text Hit") 1200 | else: 1201 | temp_cell.paragraphs[0].add_run(u'\u2718', "Cell Text Miss") 1202 | 1203 | if target['email'] in self.targets_clicked: 1204 | for event in self.timeline: 1205 | if event.message == "Clicked Link" and event.email == target['email']: 1206 | user_agent = parse(event.details['browser']['user-agent']) 1207 | browser_details = user_agent.browser.family + " " + \ 1208 | user_agent.browser.version_string 1209 | os_details = user_agent.os.family + " " + \ 1210 | user_agent.os.version_string 1211 | temp_cell = table.cell(counter, 5) 1212 | temp_cell.text = os_details 1213 | temp_cell = table.cell(counter, 6) 1214 | temp_cell.text = browser_details 1215 | else: 1216 | temp_cell = table.cell(counter, 5) 1217 | temp_cell.text = "N/A" 1218 | temp_cell = table.cell(counter, 6) 1219 | temp_cell.text = "N/A" 1220 | counter += 1 1221 | target_counter += 1 1222 | print(f"[+] Created table entry for {target_counter} of {self.total_targets}.") 1223 | 1224 | d.add_page_break() 1225 | 1226 | # End of the event summary and beginning of the detailed results 1227 | print("[+] Finished writing events summary...") 1228 | print("[+] Detailed results analysis is next and may take some time if you had a lot of targets...") 1229 | d.add_heading("Detailed Findings", 1) 1230 | target_counter = 0 1231 | for target in self.results: 1232 | # Only create a Detailed Analysis section for targets with clicks 1233 | if target.email in self.targets_clicked: 1234 | # Create counters to track table cell locations 1235 | opened_counter = 1 1236 | clicked_counter = 1 1237 | submitted_counter = 1 1238 | # Create section starting with a header with the first and last name 1239 | position = "" 1240 | if target.position: 1241 | position = f"({target.position})" 1242 | d.add_heading(f"{target.first_name} {target.last_name} {position}", 2) 1243 | p = d.add_paragraph(target.email) 1244 | p = d.add_paragraph() 1245 | # Save a spot to record the email sent date and time in the report 1246 | email_sent_run = p.add_run() 1247 | # Go through all events to find events for this target 1248 | for event in self.timeline: 1249 | if event.message == "Email Sent" and event.email == target.email: 1250 | # Parse the timestamp into separate date and time variables 1251 | # Ex: 2017-01-30T14:31:22.534880731-05:00 1252 | temp = event.time.split('T') 1253 | sent_date = temp[0] 1254 | sent_time = temp[1].split('.')[0] 1255 | # Record the email sent date and time in the run created earlier 1256 | email_sent_run.text = f"Email sent on {sent_date} at {sent_time}" 1257 | 1258 | if event.message == "Email Opened" and event.email == target.email: 1259 | if opened_counter == 1: 1260 | # Create the Email Opened/Previewed table 1261 | p = d.add_paragraph() 1262 | p.style = d.styles['Normal'] 1263 | run = p.add_run("Email Previews") 1264 | run.bold = True 1265 | 1266 | opened_table = d.add_table(rows=1, cols=1, style="GoReport") 1267 | opened_table.autofit = True 1268 | opened_table.allow_autofit = True 1269 | 1270 | header1 = opened_table.cell(0, 0) 1271 | header1.text = "" 1272 | header1.paragraphs[0].add_run("Time", "Cell Text").bold = True 1273 | 1274 | # Begin by adding a row to the table and inserting timestamp 1275 | opened_table.add_row() 1276 | timestamp = opened_table.cell(opened_counter, 0) 1277 | temp = event.time.split('T') 1278 | timestamp.text = temp[0] + " " + temp[1].split('.')[0] 1279 | opened_counter += 1 1280 | 1281 | if event.message == "Clicked Link" and event.email == target.email: 1282 | if clicked_counter == 1: 1283 | # Create the Clicked Link table 1284 | p = d.add_paragraph() 1285 | p.style = d.styles['Normal'] 1286 | run = p.add_run("Email Link Clicked") 1287 | run.bold = True 1288 | 1289 | clicked_table = d.add_table(rows=1, cols=5, style="GoReport") 1290 | clicked_table.autofit = True 1291 | clicked_table.allow_autofit = True 1292 | 1293 | header1 = clicked_table.cell(0, 0) 1294 | header1.text = "" 1295 | header1.paragraphs[0].add_run("Time", "Cell Text").bold = True 1296 | 1297 | header2 = clicked_table.cell(0, 1) 1298 | header2.text = "" 1299 | header2.paragraphs[0].add_run("IP", "Cell Text").bold = True 1300 | 1301 | header3 = clicked_table.cell(0, 2) 1302 | header3.text = "" 1303 | header3.paragraphs[0].add_run("Location", "Cell Text").bold = True 1304 | 1305 | header4 = clicked_table.cell(0, 3) 1306 | header4.text = "" 1307 | header4.paragraphs[0].add_run("Browser", "Cell Text").bold = True 1308 | 1309 | header5 = clicked_table.cell(0, 4) 1310 | header5.text = "" 1311 | header5.paragraphs[0].add_run("Operating System", 1312 | "Cell Text").bold = True 1313 | 1314 | clicked_table.add_row() 1315 | timestamp = clicked_table.cell(clicked_counter, 0) 1316 | temp = event.time.split('T') 1317 | timestamp.text = temp[0] + " " + temp[1].split('.')[0] 1318 | 1319 | ip_add = clicked_table.cell(clicked_counter, 1) 1320 | # Check if browser IP matches the target's IP and record result 1321 | ip_add.text = self.compare_ip_addresses( 1322 | target.ip, event.details['browser']['address'], self.verbose) 1323 | 1324 | # Parse the location data 1325 | event_location = clicked_table.cell(clicked_counter, 2) 1326 | event_location.text = self.geolocate(target, event.details['browser']['address'], self.google) 1327 | 1328 | # Parse the user-agent string for browser and OS details 1329 | user_agent = parse(event.details['browser']['user-agent']) 1330 | browser_details = user_agent.browser.family + " " + \ 1331 | user_agent.browser.version_string 1332 | browser = clicked_table.cell(clicked_counter, 3) 1333 | browser.text = browser_details 1334 | self.browsers.append(browser_details) 1335 | 1336 | op_sys = clicked_table.cell(clicked_counter, 4) 1337 | os_details = user_agent.os.family + " " + user_agent.os.version_string 1338 | op_sys.text = os_details 1339 | self.operating_systems.append(os_details) 1340 | 1341 | clicked_counter += 1 1342 | 1343 | if event.message == "Submitted Data" and event.email == target.email: 1344 | if submitted_counter == 1: 1345 | # Create the Submitted Data table 1346 | p = d.add_paragraph() 1347 | p.style = d.styles['Normal'] 1348 | run = p.add_run("Data Captured") 1349 | run.bold = True 1350 | 1351 | submitted_table = d.add_table(rows=1, cols=6, style="GoReport") 1352 | submitted_table.autofit = True 1353 | submitted_table.allow_autofit = True 1354 | 1355 | header1 = submitted_table.cell(0, 0) 1356 | header1.text = "" 1357 | header1.paragraphs[0].add_run("Time", "Cell Text").bold = True 1358 | 1359 | header2 = submitted_table.cell(0, 1) 1360 | header2.text = "" 1361 | header2.paragraphs[0].add_run("IP", "Cell Text").bold = True 1362 | 1363 | header3 = submitted_table.cell(0, 2) 1364 | header3.text = "" 1365 | header3.paragraphs[0].add_run("Location", "Cell Text").bold = True 1366 | 1367 | header4 = submitted_table.cell(0, 3) 1368 | header4.text = "" 1369 | header4.paragraphs[0].add_run("Browser", "Cell Text").bold = True 1370 | 1371 | header5 = submitted_table.cell(0, 4) 1372 | header5.text = "" 1373 | header5.paragraphs[0].add_run("Operating System", 1374 | "Cell Text").bold = True 1375 | 1376 | header6 = submitted_table.cell(0, 5) 1377 | header6.text = "" 1378 | header6.paragraphs[0].add_run("Data Captured", 1379 | "Cell Text").bold = True 1380 | 1381 | submitted_table.add_row() 1382 | timestamp = submitted_table.cell(submitted_counter, 0) 1383 | temp = event.time.split('T') 1384 | timestamp.text = temp[0] + " " + temp[1].split('.')[0] 1385 | 1386 | ip_add = submitted_table.cell(submitted_counter, 1) 1387 | ip_add.text = event.details['browser']['address'] 1388 | 1389 | # Parse the location data 1390 | event_location = submitted_table.cell(submitted_counter, 2) 1391 | event_location.text = self.geolocate(target, event.details['browser']['address'], self.google) 1392 | 1393 | # Parse the user-agent string and add browser and OS details 1394 | user_agent = parse(event.details['browser']['user-agent']) 1395 | browser_details = user_agent.browser.family + " " + \ 1396 | user_agent.browser.version_string 1397 | browser = submitted_table.cell(submitted_counter, 3) 1398 | browser.text = browser_details 1399 | 1400 | op_sys = submitted_table.cell(submitted_counter, 4) 1401 | os_details = user_agent.os.family + " " + user_agent.os.version_string 1402 | op_sys.text = f"{os_details}" 1403 | 1404 | # Get just the submitted data from the event's payload 1405 | submitted_data = "" 1406 | data = submitted_table.cell(submitted_counter, 5) 1407 | data_payload = event.details['payload'] 1408 | # Get all of the submitted data 1409 | for key, value in data_payload.items(): 1410 | # To get just submitted data, we drop the 'rid' key 1411 | if not key == "rid": 1412 | submitted_data += f"{key}:{str(value).strip('[').strip(']')} " 1413 | data.text = f"{submitted_data}" 1414 | submitted_counter += 1 1415 | target_counter += 1 1416 | print(f"[+] Processed detailed analysis for {target_counter} of {self.total_targets}.") 1417 | 1418 | d.add_page_break() 1419 | else: 1420 | # This target had no clicked or submitted events so move on to next 1421 | target_counter += 1 1422 | print(f"[+] Processed detailed analysis for {target_counter} of {self.total_targets}.") 1423 | continue 1424 | 1425 | print("[+] Finished writing Detailed Analysis section...") 1426 | # End of the detailed results and the beginning of browser, location, and OS stats 1427 | d.add_heading("Statistics", 1) 1428 | p = d.add_paragraph("The following table shows the browsers seen:") 1429 | # Create browser table 1430 | browser_table = d.add_table(rows=1, cols=2, style="GoReport") 1431 | self._set_word_column_width(browser_table.columns[0], Cm(7.24)) 1432 | self._set_word_column_width(browser_table.columns[1], Cm(3.35)) 1433 | 1434 | header1 = browser_table.cell(0, 0) 1435 | header1.text = "" 1436 | header1.paragraphs[0].add_run("Browser", "Cell Text").bold = True 1437 | 1438 | header2 = browser_table.cell(0, 1) 1439 | header2.text = "" 1440 | header2.paragraphs[0].add_run("Seen", "Cell Text").bold = True 1441 | 1442 | p = d.add_paragraph("\nThe following table shows the operating systems seen:") 1443 | 1444 | # Create OS table 1445 | os_table = d.add_table(rows=1, cols=2, style="GoReport") 1446 | self._set_word_column_width(os_table.columns[0], Cm(7.24)) 1447 | self._set_word_column_width(os_table.columns[1], Cm(3.35)) 1448 | 1449 | header1 = os_table.cell(0, 0) 1450 | header1.text = "" 1451 | header1.paragraphs[0].add_run("Operating System", "Cell Text").bold = True 1452 | 1453 | header2 = os_table.cell(0, 1) 1454 | header2.text = "" 1455 | header2.paragraphs[0].add_run("Seen", "Cell Text").bold = True 1456 | 1457 | p = d.add_paragraph("\nThe following table shows the locations seen:") 1458 | 1459 | # Create geo IP table 1460 | location_table = d.add_table(rows=1, cols=2, style="GoReport") 1461 | self._set_word_column_width(location_table.columns[0], Cm(7.24)) 1462 | self._set_word_column_width(location_table.columns[1], Cm(3.35)) 1463 | 1464 | header1 = location_table.cell(0, 0) 1465 | header1.text = "" 1466 | header1.paragraphs[0].add_run("Location", "Cell Text").bold = True 1467 | 1468 | header2 = location_table.cell(0, 1) 1469 | header2.text = "" 1470 | header2.paragraphs[0].add_run("Visits", "Cell Text").bold = True 1471 | 1472 | p = d.add_paragraph("\nThe following table shows the IP addresses captured:") 1473 | 1474 | # Create IP address table 1475 | ip_add_table = d.add_table(rows=1, cols=2, style="GoReport") 1476 | self._set_word_column_width(ip_add_table.columns[0], Cm(7.24)) 1477 | self._set_word_column_width(ip_add_table.columns[1], Cm(3.35)) 1478 | 1479 | header1 = ip_add_table.cell(0, 0) 1480 | header1.text = "" 1481 | header1.paragraphs[0].add_run("IP Address", "Cell Text").bold = True 1482 | 1483 | header2 = ip_add_table.cell(0, 1) 1484 | header2.text = "" 1485 | header2.paragraphs[0].add_run("Seen", "Cell Text").bold = True 1486 | 1487 | p = d.add_paragraph("\nThe following table shows the IP addresses matched with geolocation data:") 1488 | 1489 | # Create IP address and location table 1490 | ip_loc_table = d.add_table(rows=1, cols=2, style="GoReport") 1491 | self._set_word_column_width(ip_loc_table.columns[0], Cm(7.24)) 1492 | self._set_word_column_width(ip_loc_table.columns[1], Cm(3.35)) 1493 | 1494 | header1 = ip_loc_table.cell(0, 0) 1495 | header1.text = "" 1496 | header1.paragraphs[0].add_run("IP Address", "Cell Text").bold = True 1497 | 1498 | header2 = ip_loc_table.cell(0, 1) 1499 | header2.text = "" 1500 | header2.paragraphs[0].add_run("Location", "Cell Text").bold = True 1501 | 1502 | # Counters are used here again to track rows 1503 | counter = 1 1504 | # Counter is used to count all elements in the lists to create a unique list with totals 1505 | counted_browsers = Counter(self.browsers) 1506 | for key, value in counted_browsers.items(): 1507 | browser_table.add_row() 1508 | cell = browser_table.cell(counter, 0) 1509 | cell.text = f"{key}" 1510 | 1511 | cell = browser_table.cell(counter, 1) 1512 | cell.text = f"{value}" 1513 | counter += 1 1514 | 1515 | counter = 1 1516 | counted_os = Counter(self.operating_systems) 1517 | for key, value in counted_os.items(): 1518 | os_table.add_row() 1519 | cell = os_table.cell(counter, 0) 1520 | cell.text = f"{key}" 1521 | 1522 | cell = os_table.cell(counter, 1) 1523 | cell.text = f"{value}" 1524 | counter += 1 1525 | 1526 | counter = 1 1527 | counted_locations = Counter(self.locations) 1528 | for key, value in counted_locations.items(): 1529 | location_table.add_row() 1530 | cell = location_table.cell(counter, 0) 1531 | cell.text = f"{key}" 1532 | 1533 | cell = location_table.cell(counter, 1) 1534 | cell.text = f"{value}" 1535 | counter += 1 1536 | 1537 | counter = 1 1538 | counted_ip_addresses = Counter(self.ip_addresses) 1539 | for key, value in counted_ip_addresses.items(): 1540 | ip_add_table.add_row() 1541 | cell = ip_add_table.cell(counter, 0) 1542 | cell.text = f"{key}" 1543 | 1544 | cell = ip_add_table.cell(counter, 1) 1545 | cell.text = f"{value}" 1546 | counter += 1 1547 | 1548 | counter = 1 1549 | for key, value in self.ip_and_location.items(): 1550 | ip_loc_table.add_row() 1551 | cell = ip_loc_table.cell(counter, 0) 1552 | cell.text = f"{key}" 1553 | 1554 | cell = ip_loc_table.cell(counter, 1) 1555 | cell.text = f"{value}" 1556 | counter += 1 1557 | 1558 | # Finalize document and save it as the value of output_word_report 1559 | d.save(f"{self.output_word_report}") 1560 | print(f"[+] Done! Check \"{self.output_word_report}\" for your results.") 1561 | 1562 | def config_section_map(self, config_parser, section): 1563 | """This function helps by reading accepting a config file section, from gophish.config, 1564 | and returning a dictionary object that can be referenced for configuration settings. 1565 | """ 1566 | section_dict = {} 1567 | options = config_parser.options(section) 1568 | for option in options: 1569 | try: 1570 | section_dict[option] = config_parser.get(section, option) 1571 | if section_dict[option] == -1: 1572 | print(f"[-] Skipping: {option}") 1573 | except: 1574 | print(f"[!] There was an error with: {option}") 1575 | section_dict[option] = None 1576 | return section_dict 1577 | --------------------------------------------------------------------------------