├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── config.default.py ├── gophish-cli.py ├── modules ├── creds.py ├── juniper.py ├── netscaler.py ├── owa.py └── report.py ├── requirements.txt └── tests ├── test-juniper.py └── test-netscaler.py /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | __pycache__ 3 | certs 4 | bin 5 | include 6 | lib 7 | pip-selfcheck.json 8 | .idea/ 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "api-client-python"] 2 | path = api-client-python 3 | url = https://github.com/GoSecure/api-client-python 4 | branch = stable 5 | [submodule "exchangelib"] 6 | path = exchangelib 7 | url = https://github.com/ecederstrand/exchangelib/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gophish Python CLI 2 | 3 | This tool aim to perform huge phishing campaigns by using the very respected gophish toolkit. If you need to run a campaign for more than 10 000 email addresses or need to split a batch of email addresses into smaller groups for any reasons (lower risks with anti-Spam, avoid being banned by IPS, bypass Email service limitations, etc.), that tool could help you! 4 | 5 | The tool is based the [api-client-python](https://github.com/gophish/api-client-python) library. 6 | 7 | A reporting feature was recently added (2018-03-10) allowing users to generate statistics for your report. You can configure the tool to download apache logs, access sendgrid stats (including bounces, deffered, spam reports, etc.), access empire agents and correlate other useful data. 8 | 9 | 10 | ## Installation 11 | 12 | To begin, you need a gophish instance. Find more about the gophish project [here](https://github.com/gophish/gophish). 13 | 14 | To install `gophish-cli`, simply run the command: 15 | 16 | ```bash 17 | git clone --recursive https://github.com/gosecure/gophish-cli 18 | cd gophish-cli/api-client-python 19 | pip3 install -r requirements.txt --user 20 | python3 ./setup.py install --user 21 | cd .. 22 | pip3 install -r requirements.txt --user 23 | ``` 24 | 25 | To install `gophish-cli` in a virtualenv, run: 26 | 27 | ```bash 28 | git clone --recursive https://github.com/gosecure/gophish-cli 29 | 30 | virtualenv ./gophish-cli 31 | cd gophish-cli 32 | source bin/activate 33 | 34 | cd api-client-python 35 | pip3 install -r requirements.txt 36 | python3 ./setup.py install 37 | cd .. 38 | pip3 install -r requirements.txt 39 | ``` 40 | 41 | To test harvested credentials on OWA, you will also need to install the [exchangelib](https://github.com/ecederstrand/exchangelib/) submodule. 42 | 43 | ```bash 44 | cd exchangelib 45 | python3 ./setup.py install --user 46 | ``` 47 | 48 | It is currently using a forked version of the [API library](https://github.com/gosecure/api-client-python/) for development purpose. 49 | 50 | 51 | ## Configuration 52 | 53 | You need the API key found in the [Settings page](https://gophish.gitbooks.io/user-guide/content/documentation/changing_user_settings.html#changing-your-password--updating-settings). 54 | 55 | Then run `cp config.default.py config.py` and edit the `config.py` file using your favourite text editor. 56 | 57 | ### Step 1 - Connection to the gophish instance 58 | 59 | ```python 60 | API_KEY = '' 61 | API_URL = 'http://127.0.0.1:3333 62 | ``` 63 | 64 | 65 | ### Step 2 - Define the campaign parameters 66 | 67 | As mentionned earlier, `gophish-cli` true power is for campaign spliting. Thus, the objects below must be created using the webUI: 68 | 69 | * Landing Page 70 | * Email Template 71 | * Sending Profile 72 | 73 | The tool will take care of the email groups and campaigns creation. It will also let you retrieve statistics and credentials from the same batch. 74 | 75 | The three parameters below should be configured based on the number of email addresses: 76 | 77 | * GROUP_SIZE: Number of email addresses per group. 78 | * START_INTERVAL: Interval before starting the first batch. 79 | * BATCH_INTERVAL: Interval between each batches. 80 | 81 | 82 | ### Step 3 - Spam! 83 | 84 | The most automated way to run the tool is as follow: 85 | 86 | ```bash 87 | $ python3 ./gophish-cli.py campaign --start --new-groups 88 | [-] Preparing new groups creation. 89 | [-] Campaign Name: JohnDoe 90 | [-] File Path: /path/to/test_emails.txt 91 | [-] Batch size: 30 92 | [-] Group count: 4 93 | Do you want to continue? [y/N] y 94 | [-] Creating group "JohnDoe - Group 1" with 30 targets. First email is johndoe1@trash-mail.com 95 | [-] Creating group "JohnDoe - Group 2" with 30 targets. First email is johndoe31@trash-mail.com 96 | [-] Creating group "JohnDoe - Group 3" with 30 targets. First email is johndoe61@trash-mail.com 97 | [-] Creating group "JohnDoe - Group 4" with 10 targets. First email is johndoe91@trash-mail.com 98 | [-] Preparing to launch campaigns 99 | [-] Campaign Name: JohnDoe 100 | [-] Landing Page: LP - EN - aCampaign - JohnDoe 101 | [-] Email Template: ET - EN - aCampaign - JohnDoe 102 | [-] Sending Profile: imgonahackyou.com (provider X) 103 | [-] URL: https://johndoe.imgonahackyou.com 104 | [-] Group count: 4 105 | [-] Launch Date: 2017-02-06 17:54:46.813515-05:00 106 | [-] Time interval: 1 minute(s) 107 | Do you want to continue? [y/N] y 108 | [-] Launching campaign "JohnDoe - Group 1" at 2017-02-06 17:54:46.813515-05:00 109 | [-] Launching campaign "JohnDoe - Group 2" at 2017-02-06 17:55:46.813515-05:00 110 | [-] Launching campaign "JohnDoe - Group 3" at 2017-02-06 17:56:46.813515-05:00 111 | [-] Launching campaign "JohnDoe - Group 4" at 2017-02-06 17:57:46.813515-05:00 112 | ``` 113 | 114 | 115 | ## Help 116 | 117 | ``` 118 | $ python3 ./gophish-cli.py -h 119 | usage: gophish-cli.py [-h] [-v] [-c CONFIG] [-d] 120 | {group,campaign,creds,stats,report} ... 121 | 122 | Gophish cli. Use this tool to quickly setup a phishing campaign using your 123 | gophish infrastructure. 124 | 125 | positional arguments: 126 | {group,campaign,creds,stats,report} 127 | group Manage groups. 128 | campaign Manage campaigns. 129 | creds Manage credentials. 130 | stats Manage statss. 131 | report Generate report (currently multiple CSV files). 132 | 133 | optional arguments: 134 | -h, --help show this help message and exit 135 | -v, --version show program's version number and exit 136 | -c CONFIG, --config CONFIG 137 | Alternative config file. Default is config.py (Not 138 | implemented yet) 139 | -d, --debug Run the tool in debug mode 140 | ``` 141 | 142 | Every positional arguments have its own help page. For example: `./gophish-cli.py campaign -h`. Read them for more details. 143 | 144 | 145 | ## Post-campaign useful commands 146 | 147 | To get results 148 | 149 | ``` 150 | $ python3 ./gophish-cli.py campaign --results 151 | [-] Exported 2492 timeline entries to /some/path/campaign_results_JohnDoe.csv 152 | [-] Exported 40 credentials to /some/path/campaign_creds_JohnDoe.csv. 153 | ``` 154 | 155 | To print credentials 156 | 157 | ``` 158 | $ python3 ./gophish-cli.py creds --print 159 | +---------------------+--------------+------------+ 160 | | Email | User | Pass | 161 | +---------------------+--------------+------------+ 162 | | mdube@gosecure.ca | mdube | P@$$w0rd1! | 163 | | somebody@gouv.qc.ca | lddoei | Winter2017 | 164 | | ... | ... | ... | 165 | +---------------------+--------------+------------+ 166 | ``` 167 | 168 | 169 | To test credentials by using OWA 170 | 171 | ``` 172 | $ python3 ./gophish-cli.py creds --test-owa 173 | [-] **WARNING** 174 | [-] Too many attempts could lock accounts. Be easy with this feature. 175 | [-] 176 | [-] Preparing to test credentials on OWA 177 | [-] Campaign Name: Mart 178 | [-] OWA Domain: LAB 179 | [-] OWA Server: owa.lab.local 180 | [-] Credentials count: 123 181 | Do you want to continue? [y/N] y 182 | LAB\user1 - Fall2008: Successful login 183 | LAB\user2 - Milhouse44$: Failed login 184 | ... 185 | ``` 186 | 187 | 188 | To get source IP addresses 189 | 190 | ``` 191 | $ python3 ./gophish-cli.py stats --targets-ip 192 | 193 | +------------------------+-----------+ 194 | | IP Address | Hit Count | 195 | +------------------------+-----------+ 196 | | No IP. Email Sent Only | 1251 | 197 | | 31.10.39.30 | 50 | 198 | | 2.21.14.65 | 2 | 199 | | ... | ... | 200 | +------------------------+-----------+ 201 | ``` 202 | 203 | ## Reporting 204 | 205 | Phishing is fun but often comes with a reporting phase. Once configured, the reporting feature is a great kickstarter for your report. 206 | 207 | ``` 208 | $ ./gophish-cli.py -d report 209 | 210 | DEBUG Arguments: Namespace(action='report', config=None, debug=True) 211 | INFO Getting 1 campaign timelines for Client X 212 | DEBUG Got 7605 events 213 | INFO Getting 1 campaign results for Client X 214 | DEBUG Got 4820 events 215 | INFO Generating report. 216 | INFO Setting up folders 217 | DEBUG Creating folder: /home/user/client/phishing/report_20180310-133544/ 218 | DEBUG Creating folder: /home/user/client/phishing/report_20180310-133544/apache_logs/ 219 | INFO Downloading apache logs 220 | DEBUG [SSH] Attempting to authenticate 221 | DEBUG [SSH] Trying ssh-agent key 222 | DEBUG [SSH]... success! 223 | DEBUG [SSH] Authentication successful 224 | DEBUG [SSH] Changing directory: /var/log/apache2/ 225 | DEBUG [SSH] Downloading: meh.domain.com.access.log 226 | DEBUG [SSH] Downloading: meh.domain.com.access.log.1 227 | DEBUG [SSH] Downloading: meh.domain.com.error.log 228 | DEBUG Got 119 lines 229 | INFO Getting Sendgrid Stats 230 | INFO Getting Empire Agents 231 | DEBUG Got 82 agents 232 | INFO Extracting stats 233 | INFO Printing Report 234 | Raw Data: 235 | 236 | Timeline: 237 | First Event: 2018-03-09 08:09:46 238 | Last Event: 2018-03-10 09:20:15 239 | Email sent: 6165 240 | Email opened: 1052 241 | Clicked Link: 238 242 | Submitted Data: 149 243 | Unique Email opened: 188 244 | Unique Clicked Link: 158 245 | Unique Submitted Data: 103 246 | Source IPs: 247 | None (6166) 248 | 1.2.3.4 (800) 249 | 5.6.7.8 (225) 250 | ... 251 | 252 | Sendgrid stats: 253 | Blocks: 13 254 | Bounce Drops: 188 255 | Bounces: 98 256 | Clicks: 0 257 | Deffered: 10 258 | Delivered: 5101 259 | Invalid Emails: 0 260 | Open: 1227 261 | Processed: 6014 262 | Requests: 6068 263 | Spam Report Drops: 0 264 | Spam Reports: 0 265 | Unique Clicks: 0 266 | Unique Opens: 193 267 | Subscribe Drops: 0 268 | Unsubscribes: 0 269 | 270 | Apache: 271 | Malware Download: 149 272 | Source IPs: 273 | 1.2.3.4 (123) 274 | 5.6.7.8 (45) 275 | 276 | Empire: 277 | Agents count: 82 278 | Agents HighPriv count: 10 279 | Unique Agents username count: 78 280 | Unique Agents Hostnames count: 76 281 | OS Details: 282 | Microsoft Windows 7 Professional (75) 283 | Microsoft Windows 10 Enterprise (4) 284 | Microsoft Windows 10 Home (1) 285 | Microsoft Windows 7 Ultimate (1) 286 | Microsoft Windows 10 Pro (1) 287 | Source IPs: 288 | 1.2.3.4 (40) 289 | 5.6.7.8 (10) 290 | ... 291 | 292 | Analyzed Data: 293 | 294 | Conversion Percentage: 295 | Email Received (6165) -> Email Opened (1052): 17.06 296 | Email Open (1052) -> Link Clicked (158): 15.02 297 | Page Visit (158) -> Send Credentials (103): 65.19 298 | Malware Download (149) -> Malware Execution (78) (Empire): 52.35 299 | Malware Download (149) -> Malware Execution (0) (Msf): 0.0 300 | Malware Download (149) -> Malware Execution (0) (Cobalt): 0.0 301 | ``` 302 | 303 | ## Known issues & Troubleshooting 304 | 305 | ### Issues with Outlook 365 306 | 307 | Outlook 365 limit the number of email sent per connection to 30. `GROUP_SIZE` must be set to 30 when using this provider. 308 | 309 | ### Emails keep the status "Sending" 310 | 311 | On previous version of gophish (< 0.5), some email addresses were stuck with status "Sending" if too many emails were sent at once. The fix was to split in groups of 100 and put a delay between each waves. 312 | 313 | Example: `GROUP_SIZE=100` and `BATCH_INTERVAL=5` 314 | 315 | ### My emails are flagged as SPAM. 316 | 317 | Test your campaign here: [https://www.mail-tester.com/](https://www.mail-tester.com/) 318 | -------------------------------------------------------------------------------- /config.default.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Config file of the Gophish command line interface. 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2017, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | import datetime 34 | 35 | # 36 | # Step 1: Gophish configuration 37 | # 38 | # Just the basic configuration for basic features 39 | # 40 | API_KEY = '' 41 | API_URL = 'http://127.0.0.1:3333' 42 | 43 | # 44 | # Step 2: Campaign configuration 45 | # 46 | # Information regarding your campaign. Most comes from the gophish WebUI. 47 | CAMPAIGN_NAME = 'John Doe' 48 | CAMPAIGN_URL = 'https://path.toyourwebsite.com' 49 | 50 | WORKING_DIR = '/path/to/working/dir' 51 | EMAILS_PATH = WORKING_DIR + 'emails.txt' 52 | 53 | # Landing Pages 54 | LP_NAME = 'Landing Page Name' 55 | 56 | # Two specific fields required by --print-creds to properly parse the JSON payloads. 57 | # Update the fields based on your landing pages user and password fields. 58 | LP_USER_FIELD = 'cUser' 59 | LP_PWD_FIELD = 'cPass' 60 | 61 | # Email Template 62 | ET_NAME = 'Email Template Name' 63 | 64 | # Sending Profiles 65 | SP_NAME = 'Sending Profile Name' 66 | 67 | # Batch Management Settings 68 | GROUP_SIZE = 50 69 | START_INTERVAL = 1 # Unit = minutes. Default=1. Increase when you have more than 10 batch. 70 | BATCH_INTERVAL = 1 # Unit = minutes 71 | 72 | # Verify TLS when testing credentials 73 | # Default is True 74 | VERIFY_TLS = True 75 | 76 | # Owa login testing settings 77 | OWA_DOMAIN = 'DOMAIN' 78 | OWA_SERVER = 'outlook.example.com' 79 | 80 | # Netscaler login testing settings 81 | NETSCALER_SERVER = 'vpn.example.com' 82 | 83 | # Juniper (Secure Access SSL VPN) 84 | JUNIPER_DOMAIN = 'DOMAIN' 85 | JUNIPER_SERVER = 'vpn.example.com' 86 | # HINT: Consider verifying the URI as some organizations have multiple 87 | # URIs which are 2FA or 1FA. The default one is often 2FA. 88 | # For istance, /url/ can become /url_XX/, where XX is a number. 89 | JUNIPER_URI = '/dana-na/auth/url/login.cgi' 90 | # HINT: Find it in the source code of the login page. Look for a hidden 91 | # input field named "realm". 92 | JUNIPER_REALM = 'bla' 93 | # 94 | # Step 3: Things that should not change for most users 95 | # 96 | FILE_DATE_FMT = '%Y%m%d_%H%M%S' 97 | FILE_DATE = datetime.datetime.now().strftime(FILE_DATE_FMT) 98 | 99 | CAMPAIGN_NAME_TPL = '%s - Group %i' 100 | CAMPAIGN_PREFIX = CAMPAIGN_NAME_TPL[:-2] % CAMPAIGN_NAME 101 | RESULTS_PATH = WORKING_DIR + 'campaign_results_%s.csv' % CAMPAIGN_NAME 102 | CREDS_PATH = WORKING_DIR + 'campaign_creds_%s_%s.csv' % (FILE_DATE, CAMPAIGN_NAME) 103 | JSON_PATH = WORKING_DIR + 'campaign_raw_%s.json' % CAMPAIGN_NAME 104 | GEOIP_PATH = WORKING_DIR + 'campaign_geoip_%s.csv' % CAMPAIGN_NAME 105 | 106 | # Reporting 107 | 108 | EXCLUDED_IP = [] 109 | 110 | GOPHISH_HOST = '' 111 | GOPHISH_SSH_PORT = 22 112 | GOPHISH_SSH_USER = 'root' 113 | GOPHISH_SSH_PASS = None 114 | GOPHISH_SSH_KEY = '/path/to/key' 115 | GOPHISH_SSH_KEY_PASSPHRASE = 'some_pass' 116 | 117 | # Gophish timestamps are in UTC. This will put dates as this timezone. 118 | GOPHISH_TIMEZONE = "America/Toronto" 119 | 120 | APACHE_HOST = GOPHISH_HOST 121 | APACHE_SSH_PORT = GOPHISH_SSH_PORT 122 | APACHE_SSH_USER = GOPHISH_SSH_USER 123 | APACHE_SSH_PASS = GOPHISH_SSH_PASS 124 | APACHE_SSH_KEY = GOPHISH_SSH_KEY 125 | APACHE_SSH_KEY_PASSPHRASE = GOPHISH_SSH_KEY_PASSPHRASE 126 | APACHE_LOGS_FOLDER = '/var/log/apache2/' 127 | APACHE_LOGS_PREFIX = 'path.toyourwebsite.com' 128 | # Take if from /etc/apache2/apache2.conf. The line starts with LogFormat. Currently using the "combined" one. 129 | APACHE_LOGS_FORMAT = "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" 130 | APACHE_MALWARE_NAME = 'malware.zip' 131 | 132 | EMPIRE_API_URL = 'https://127.0.0.1:1337' 133 | EMPIRE_API_KEY = 'some_key' 134 | 135 | SENDGRID_API_KEY = 'some_key' 136 | 137 | # 138 | # By default, we disable SSL verification as gophish uses a self-signed cert. 139 | # 140 | import gophish.client 141 | import requests 142 | from requests.packages import urllib3 143 | 144 | class GophishClient(gophish.client.GophishClient): 145 | """ A standard HTTP REST client used by Gophish """ 146 | 147 | def __init__(self, api_key, host, **kwargs): 148 | super(GophishClient, self).__init__(api_key, host, **kwargs) 149 | 150 | def execute(self, method, path, **kwargs): 151 | """ Executes a request to a given endpoint, returning the result """ 152 | 153 | url = "{}{}".format(self.host, path) 154 | kwargs.update(self._client_kwargs) 155 | response = requests.request( 156 | method, url, params={"api_key": self.api_key}, verify=False, **kwargs) 157 | return response 158 | 159 | # Just to remove a SubjectAltNameWarning. 160 | urllib3.disable_warnings() 161 | 162 | # 163 | # Step 4: Advanced TLS settings 164 | # 165 | # 166 | # 167 | # Uncomment to configure TLS Client certificates or other TLS settings. 168 | # 169 | # 170 | #import ssl 171 | #import gophish.client 172 | #from requests import Session 173 | #from requests.adapters import HTTPAdapter 174 | #from requests.packages.urllib3.poolmanager import PoolManager 175 | #from requests.packages import urllib3 176 | # 177 | #class TLSHttpAdapter(HTTPAdapter): 178 | # '''An HTTPS Transport Adapter that uses an arbitrary SSL version.''' 179 | # 180 | # def init_poolmanager(self, connections, maxsize, block=False): 181 | # self.poolmanager = PoolManager(num_pools=connections, 182 | # maxsize=maxsize, 183 | # block=block, 184 | # ssl_version=ssl.PROTOCOL_TLSv1_2, 185 | # cert_reqs='CERT_REQUIRED') 186 | # 187 | #class GophishClient(gophish.client.GophishClient): 188 | # """ A standard HTTP REST client used by Gophish """ 189 | # def __init__(self, api_key, host, cert_file=None, ca_file=None, **kwargs): 190 | # super(GophishClient, self).__init__(api_key, host, **kwargs) 191 | # self.session = Session() 192 | # self.session.mount(API_URL, TLSHttpAdapter()) 193 | # self.cert_file = '/path/to/client_cert.pem' 194 | # self.ca_file = '/path/to/root_ca.crt' 195 | # 196 | # def execute(self, method, path, **kwargs): 197 | # """ Executes a request to a given endpoint, returning the result """ 198 | # 199 | # url = "{}{}".format(self.host, path) 200 | # kwargs.update(self._client_kwargs) 201 | # response = self.session.request(method, url, params={"api_key": self.api_key}, 202 | # cert=(self.cert_file), verify=self.ca_file, **kwargs) 203 | # return response 204 | # 205 | -------------------------------------------------------------------------------- /gophish-cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Gophish command line interface to quickly setup a campaign. 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2018, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | 34 | import sys 35 | import csv 36 | import json 37 | import time 38 | import logging 39 | import colorlog 40 | import argparse 41 | from datetime import datetime,timedelta 42 | from urllib.request import urlopen 43 | 44 | from gophish import Gophish 45 | from gophish.models import * 46 | from prettytable import PrettyTable 47 | 48 | try: 49 | import config 50 | except: 51 | print("config.py file not created. Please RTFM.") 52 | exit(1) 53 | 54 | from modules.creds import Credentials 55 | from modules.netscaler import NetscalerCredsTester 56 | from modules.juniper import JuniperCredsTester 57 | from modules.report import GophishReporter 58 | 59 | try: 60 | from modules.owa import OwaCredsTester 61 | except: 62 | print("exchangelib is not installed. --test-owa will fail.") 63 | 64 | # Make sure you run python3 65 | if sys.version_info < (3, 2, 0): 66 | print('Python version 3.2 or later is needed for this script') 67 | exit(1) 68 | 69 | # Logging settings 70 | formatter = colorlog.ColoredFormatter( 71 | "%(log_color)s%(levelname)-8s%(reset)s %(white)s%(message)s", 72 | datefmt=None, 73 | reset=True, 74 | log_colors={ 75 | 'DEBUG': 'cyan', 76 | 'INFO': 'green', 77 | 'WARNING': 'yellow', 78 | 'ERROR': 'red', 79 | 'CRITICAL': 'red,bg_white', 80 | }, 81 | secondary_log_colors={}, 82 | style='%' 83 | ) 84 | handler = logging.StreamHandler() 85 | handler.setFormatter(formatter) 86 | logger = logging.getLogger('gophish-cli') 87 | logger.addHandler(handler) 88 | logger.setLevel('INFO') 89 | 90 | DEBUG = False 91 | 92 | if hasattr(config,'GophishClient'): 93 | api = Gophish(config.API_KEY,host=config.API_URL,client=config.GophishClient) 94 | else: 95 | api = Gophish(config.API_KEY,host=config.API_URL) 96 | 97 | # Some constants 98 | BROWSER_MSG = ['Email Opened', 'Clicked Link', 'Submitted Data'] 99 | 100 | class EventsFilter(): 101 | def __init__(self, email=None, ip=None, group=None): 102 | self.email = email 103 | self.ip = ip 104 | self.group = group 105 | 106 | def query_yes_no(question, default="yes"): 107 | """Ask a yes/no question via raw_input() and return their answer. 108 | 109 | "question" is a string that is presented to the user. 110 | "default" is the presumed answer if the user just hits . 111 | It must be "yes" (the default), "no" or None (meaning 112 | an answer is required of the user). 113 | 114 | The "answer" return value is True for "yes" or False for "no". 115 | """ 116 | valid = {"yes": True, "y": True, "ye": True, 117 | "no": False, "n": False} 118 | if default is None: 119 | prompt = " [y/n] " 120 | elif default == "yes": 121 | prompt = " [Y/n] " 122 | elif default == "no": 123 | prompt = " [y/N] " 124 | else: 125 | raise ValueError("invalid default answer: '%s'" % default) 126 | 127 | while True: 128 | sys.stdout.write(question + prompt) 129 | choice = input().lower() 130 | if default is not None and choice == '': 131 | return valid[default] 132 | elif choice in valid: 133 | return valid[choice] 134 | else: 135 | sys.stdout.write("Please respond with 'yes' or 'no' " 136 | "(or 'y' or 'n').\n") 137 | 138 | # Currently using ip-api.com. ISSUE: Ban IP if doing more than 150 requests per minutes. 139 | # To unban: http://ip-api.com/docs/unban 140 | # 141 | # Output format 142 | #{ 143 | # "as": "ASxxx Rogers Cable Communications Inc.", 144 | # "city": "Toronto", 145 | # "country": "Canada", 146 | # "countryCode": "CA", 147 | # "isp": "Rogers Cable", 148 | # "lat": 43.xxx5, 149 | # "lon": -79.xxx4, 150 | # "org": "Rogers Cable", 151 | # "query": "1.2.3.4", 152 | # "region": "ON", 153 | # "regionName": "Ontario", 154 | # "status": "success", 155 | # "timezone": "America/Toronto", 156 | # "zip": "G1Q" 157 | #} 158 | # To avoid issues 159 | def get_geoip(ip_addr): 160 | get_geoip.counter += 1 161 | if get_geoip.counter % 149 == 0: 162 | logger.warning('We have hit the GEOIP api %i times (Max is 150/min). Waiting 1 minute to avoid ban.' 163 | % get_geoip.counter) 164 | time.sleep(60) 165 | url = 'http://ip-api.com/json/%s' % ip_addr 166 | out = urlopen(url).read() 167 | js_out = json.loads(out) 168 | return js_out 169 | get_geoip.counter = 0 170 | 171 | def timeline_to_csv(filePath, timeline): 172 | fields = ['email', 'time', 'message'] 173 | 174 | # Setup csv writer 175 | csvfile = open(filePath, 'w', newline='') 176 | writer = csv.DictWriter(csvfile, fieldnames=fields) 177 | writer.writeheader() 178 | 179 | # Create dict with only desired fields 180 | # Write a row with this dict 181 | for entry in timeline: 182 | row = {} 183 | row['email'] = entry.email 184 | row['time'] = entry.time 185 | row['message'] = entry.message 186 | writer.writerow(row) 187 | row = None 188 | 189 | csvfile.flush() 190 | logger.info('Exported %i timeline entries to %s' % (len(timeline),filePath)) 191 | 192 | def creds_to_csv(filePath, creds_list): 193 | fields = ['email', 'username', 'password', 'is_valid'] 194 | 195 | # Setup csv writer 196 | csvfile = open(filePath, 'w', newline='') 197 | writer = csv.DictWriter(csvfile, fieldnames=fields) 198 | writer.writeheader() 199 | 200 | # Write a row with this dict 201 | for creds in creds_list: 202 | writer.writerow(creds.to_dict()) 203 | creds = None 204 | 205 | csvfile.flush() 206 | logger.info('Exported %s credentials to %s' % (len(creds_list), filePath)) 207 | 208 | def ips_to_csv(filePath, ips): 209 | fields = ['IP Address', 'Hit Count', 'City', 'Region', 'Timezone', 'ISP'] 210 | 211 | # Setup csv writer 212 | csvfile = open(filePath, 'w', newline='') 213 | writer = csv.DictWriter(csvfile, fieldnames=fields) 214 | writer.writeheader() 215 | 216 | # Convert IPs 217 | # TODO: Ugly! Create a TargetIP class. 218 | for ip,ip_info in ips.items(): 219 | row = {} 220 | row['IP Address'] = ip 221 | row['Hit Count'] = ip_info['count'] 222 | row['City'] = ip_info['geoip_city'] 223 | row['Region'] = ip_info['geoip_region'] 224 | row['Timezone'] = ip_info['geoip_timezone'] 225 | row['ISP'] = ip_info['geoip_isp'] 226 | 227 | writer.writerow(row) 228 | row = None 229 | 230 | csvfile.flush() 231 | logger.info('Exported %s targets IP to %s' % (len(ips), filePath)) 232 | 233 | def create_group(i, batch_ct, campaign_name, targets): 234 | batch_num = (int)(i/batch_ct) 235 | batch_name = config.CAMPAIGN_NAME_TPL % (campaign_name,batch_num) 236 | logger.info('Creating group "%s" with %i targets. First email is %s' 237 | % (batch_name,len(targets),targets[0].email)) 238 | group = Group(name=batch_name, targets=targets) 239 | group = api.groups.post(group) 240 | 241 | def create_groups(): 242 | group_to_create = [] 243 | i = 0 244 | targets = [] 245 | VALID_HEADER = ['First Name', 'Last Name', 'Position', 'Email'] 246 | MANDATORY_HEADER = ['Email'] 247 | header_pos = {} 248 | with open(config.EMAILS_PATH, 'r', newline='', encoding='utf-8') as fh: 249 | reader = csv.reader(fh, delimiter=',') 250 | header_row = reader.__next__() 251 | 252 | # Check header validity and position 253 | for header in header_row: 254 | if header not in VALID_HEADER: 255 | print("Invalid headers. The first line should be one or more item(s) of this list: ['First Name', 'Last Name', 'Position', 'Email']") 256 | return 1 257 | 258 | header_pos[header] = header_row.index(header) 259 | 260 | # Check mandatory headers 261 | for m_header in MANDATORY_HEADER: 262 | if m_header not in header_row: 263 | print("Missing Mandatory Header. Make sure the Email header exist.") 264 | return 1 265 | 266 | for row in reader: 267 | # Create file when enough email are read 268 | if (i % config.GROUP_SIZE) == 0 and i != 0: 269 | group_to_create.append([i, config.GROUP_SIZE, config.CAMPAIGN_NAME, targets]) 270 | targets = [] 271 | 272 | fname = '' 273 | lname = '' 274 | position = '' 275 | if hasattr(header_pos, 'First Name'): 276 | fname = row[header_pos['First Name']] 277 | if hasattr(header_pos, 'Last Name'): 278 | lname = row[header_pos['Last Name']] 279 | if hasattr(header_pos, 'Position'): 280 | position = row[header_pos['Position']] 281 | email = row[header_pos['Email']] 282 | 283 | targets.append(User(email=email,first_name=fname,last_name=lname,position=position)) 284 | i=i+1 285 | 286 | if len(targets) > 0: 287 | group_to_create.append([i+config.GROUP_SIZE, config.GROUP_SIZE, config.CAMPAIGN_NAME, targets]) 288 | 289 | logger.info('Preparing new groups creation.') 290 | logger.info(' Campaign Name: %s' % config.CAMPAIGN_NAME) 291 | logger.info(' File Path: %s' % config.EMAILS_PATH) 292 | logger.info(' Batch size: %i' % config.GROUP_SIZE) 293 | logger.info(' Group count: %i' % len(group_to_create)) 294 | logger.info(' Email count: %i' % i) 295 | ret = query_yes_no('Do you want to continue?',default='no') 296 | 297 | if not ret: 298 | return 299 | 300 | for row in group_to_create: 301 | create_group(row[0], row[1], row[2], row[3]) 302 | 303 | def delete_groups(): 304 | groups = get_groups(config.CAMPAIGN_PREFIX) 305 | 306 | logger.info('Preparing to delete groups') 307 | logger.info(' Campaign Name: %s' % config.CAMPAIGN_NAME) 308 | logger.info(' Group count: %i' % len(groups)) 309 | ret = query_yes_no('Do you want to continue?',default='no') 310 | if not ret: 311 | return 312 | 313 | for g in groups: 314 | logger.info('Deleting group %i' % g.id) 315 | api.groups.delete(group_id=g.id) 316 | 317 | def print_groups(prefix=None): 318 | title = ['ID', 'Name', 'First User', 'Count'] 319 | item_list = get_groups(prefix) 320 | x = PrettyTable(title) 321 | x.align['Name'] = 'l' 322 | x.align['First User'] = 'l' 323 | x.padding_width = 1 324 | x.max_width = 40 325 | for row in item_list: 326 | x.add_row([row.id,row.name,row.targets[0].email,len(row.targets)]) 327 | print(x.get_string()) 328 | 329 | def get_groups(prefix=None): 330 | groups_out = [] 331 | groups = api.groups.get() 332 | for c in groups: 333 | if prefix is None or c.name.startswith(prefix): 334 | groups_out.append(c) 335 | return groups_out 336 | 337 | def create_campaign(campaign_id, group_name, launch_date): 338 | campaign_name = config.CAMPAIGN_NAME_TPL % (config.CAMPAIGN_NAME, campaign_id) 339 | groups = [Group(name=group_name)] 340 | page = Page(name=config.LP_NAME) 341 | template = Template(name=config.ET_NAME) 342 | smtp = SMTP(name=config.SP_NAME) 343 | campaign = Campaign( 344 | name=campaign_name, groups=groups, page=page, 345 | template=template, smtp=smtp, url=config.CAMPAIGN_URL, launch_date=launch_date) 346 | 347 | logger.info('Launching campaign "%s" at %s' % (campaign_name,launch_date)) 348 | campaign = api.campaigns.post(campaign) 349 | 350 | def create_campaigns(): 351 | group_ct = len(get_groups(config.CAMPAIGN_PREFIX)) 352 | launch_date = datetime.now(tzlocal()) + timedelta(minutes=config.START_INTERVAL) 353 | interval = config.BATCH_INTERVAL 354 | 355 | logger.info('Preparing to launch campaigns') 356 | logger.info(' Campaign Name: %s' % config.CAMPAIGN_NAME) 357 | logger.info(' Landing Page: %s' % config.LP_NAME) 358 | logger.info(' Email Template: %s' % config.ET_NAME) 359 | logger.info(' Sending Profile: %s' % config.SP_NAME) 360 | logger.info(' URL: %s' % config.CAMPAIGN_URL) 361 | logger.info(' Group count: %i' % group_ct) 362 | logger.info(' Launch Date: %s' % launch_date) 363 | logger.info(' Time interval: %i minute(s)' % interval) 364 | ret = query_yes_no('Do you want to continue?',default='no') 365 | if not ret: 366 | return 367 | 368 | for i in range(1, group_ct+1): 369 | group_name = config.CAMPAIGN_NAME_TPL % (config.CAMPAIGN_NAME, i) 370 | create_campaign(i, group_name, launch_date) 371 | launch_date += timedelta(minutes=interval) 372 | 373 | def complete_campaign(campaign_id): 374 | logger.info('Completing campaign %i' % campaign_id) 375 | api.campaigns.complete(campaign_id) 376 | 377 | def complete_campaigns(): 378 | campaigns = get_campaigns(config.CAMPAIGN_PREFIX) 379 | 380 | logger.info('Preparing to complete campaigns') 381 | logger.info(' Campaign Name: %s' % config.CAMPAIGN_NAME) 382 | logger.info(' Campaign Count: %i' % len(campaigns)) 383 | ret = query_yes_no('Do you want to continue?',default='no') 384 | if not ret: 385 | return 386 | 387 | for c in campaigns: 388 | complete_campaign(c.id) 389 | 390 | def print_campaigns(): 391 | title = ['ID', 'Name', 'Status', 'Timeline Entries', 'Creds'] 392 | item_list = api.campaigns.get() 393 | x = PrettyTable(title) 394 | x.align['Name'] = 'l' 395 | x.padding_width = 1 396 | x.max_width = 40 397 | for row in item_list: 398 | creds_ct = sum([1 for entry in row.timeline if entry.message == 'Submitted Data']) 399 | x.add_row([row.id,row.name,row.status,len(row.timeline),creds_ct]) 400 | print(x.get_string()) 401 | 402 | def delete_campaigns(): 403 | campaigns = get_campaigns(config.CAMPAIGN_PREFIX) 404 | 405 | logger.info('Preparing to delete campaigns') 406 | logger.info(' Campaign Name: %s' % config.CAMPAIGN_NAME) 407 | logger.info(' Group count: %i' % len(campaigns)) 408 | ret = query_yes_no('Do you want to continue?',default='no') 409 | if not ret: 410 | return 411 | 412 | for c in campaigns: 413 | logger.info('Deleting campaign %i' % c.id) 414 | api.campaigns.delete(campaign_id=c.id) 415 | 416 | def get_campaigns(prefix=None): 417 | campaigns_out = [] 418 | campaigns = api.campaigns.get() 419 | for c in campaigns: 420 | if c.name.startswith(prefix): 421 | campaigns_out.append(c) 422 | return campaigns_out 423 | 424 | # Timeline entry format: 425 | #{ 426 | # 'payload': { 427 | # '__original_url': ['https://someurl.com/'], 428 | # 'btnSubmit': ['Log In'], 429 | # 'hidLang': ['E'], 430 | # 'rid': ['ea6612b9d939ffa1aaaaacc0a7bb4991b38aa3b60db2a541cab7d32a4f600b19'], 431 | # 'selLanguage': ['E'], 432 | # 'selRegion': ['2'], 433 | # 'txtPassword': ['somepass'], 434 | # 'txtUsername': ['someuser'] 435 | # }, 436 | # 'browser': { 437 | # 'address': '1.2.3.4', 438 | # 'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36' 439 | # } 440 | #} 441 | def filter_timeline(timeline, events_filter): 442 | out = [] 443 | for entry in timeline: 444 | if events_filter.email and entry.email == events_filter.email: 445 | out.append(entry) 446 | if events_filter.ip: 447 | if entry.message in BROWSER_MSG and type(entry.details) is dict: 448 | if entry.details['browser']['address'] == events_filter.ip: 449 | out.append(entry) 450 | # TODO: Support other type of filtering. 451 | return out 452 | 453 | def filter_results(results, events_filter): 454 | out = [] 455 | for entry in results: 456 | if events_filter.email and entry.email == events_filter.email: 457 | out.append(entry) 458 | # TODO: Support other type of filtering. 459 | return out 460 | 461 | def get_timelines(events_filter=None): 462 | timeline = [] 463 | campaigns = get_campaigns(config.CAMPAIGN_PREFIX) 464 | logger.info('Getting %i campaign timelines for %s' % (len(campaigns),config.CAMPAIGN_NAME)) 465 | for c in campaigns: 466 | timeline += c.timeline 467 | if events_filter is not None: 468 | timeline = filter_timeline(timeline, events_filter) 469 | logger.debug(' Got %i events' % (len(timeline))) 470 | return timeline 471 | 472 | def get_results(events_filter=None): 473 | results = [] 474 | campaigns = get_campaigns(config.CAMPAIGN_PREFIX) 475 | logger.info('Getting %i campaign results for %s' % (len(campaigns),config.CAMPAIGN_NAME)) 476 | for c in campaigns: 477 | results += c.results 478 | if events_filter is not None: 479 | results = filter_results(results, events_filter) 480 | logger.debug(' Got %i events' % (len(results))) 481 | return results 482 | 483 | def print_timeline(events_filter=None): 484 | timeline = get_timelines(events_filter) 485 | title = ['Email', 'Time', 'Message', 'Source IP'] 486 | x = PrettyTable(title) 487 | x.padding_width = 1 488 | x.max_width = 40 489 | x.align['Email'] = 'l' 490 | x.align['Time'] = 'l' 491 | x.align['Message'] = 'l' 492 | for entry in timeline: 493 | if entry.message in BROWSER_MSG and type(entry.details) is dict: 494 | source_ip = entry.details['browser']['address'] 495 | else: 496 | source_ip = None 497 | x.add_row([entry.email, entry.time, entry.message, source_ip]) 498 | print(x.get_string(sortby='Time')) 499 | 500 | def get_creds_from_timeline(timeline, userField=config.LP_USER_FIELD, 501 | passField=config.LP_PWD_FIELD): 502 | creds_list = [] 503 | for entry in timeline: 504 | if entry.message == 'Submitted Data' \ 505 | and userField in entry.details['payload'] \ 506 | and passField in entry.details['payload']: 507 | creds = Credentials(entry.email, 508 | entry.details['payload'][userField][0], 509 | entry.details['payload'][passField][0]) 510 | creds_list.append(creds) 511 | elif entry.message == 'Submitted Data': 512 | logger.warning('Invalid submitted data found. Check LP_USER_FIELD and LP_PWD_FIELD in config.py') 513 | return creds_list 514 | 515 | def save_campaigns(): 516 | timeline = get_timelines() 517 | timeline_to_csv(config.RESULTS_PATH, timeline) 518 | 519 | creds = get_creds_from_timeline(timeline) 520 | creds_to_csv(config.CREDS_PATH, creds) 521 | 522 | campaigns = get_campaigns(config.CAMPAIGN_PREFIX) 523 | campaigns_dict = [] 524 | for campaign in campaigns: 525 | campaigns_dict.append(campaign.as_dict()) 526 | with open(config.JSON_PATH, 'w') as outfile: 527 | json.dump(campaigns_dict, outfile) 528 | logger.info('Exported %i campaigns to %s.' 529 | % (len(campaigns), config.JSON_PATH)) 530 | 531 | def print_creds(events_filter=None): 532 | title = ['Email', 'User', 'Pass', 'Is Valid'] 533 | creds_list = get_creds_from_timeline(get_timelines(events_filter)) 534 | x = PrettyTable(title) 535 | x.align['Email'] = 'l' 536 | x.align['User'] = 'l' 537 | x.align['Pass'] = 'l' 538 | x.align['Is Valid'] = 'l' 539 | x.padding_width = 1 540 | x.max_width = 40 541 | for creds in creds_list: 542 | x.add_row(creds.to_list()) 543 | print(x.get_string()) 544 | 545 | def test_creds_owa(events_filter=None): 546 | creds_list = get_creds_from_timeline(get_timelines(events_filter)) 547 | 548 | logger.info('**WARNING**') 549 | logger.info('Too many attempts could lock accounts. Be easy with this feature.') 550 | logger.info('') 551 | logger.info('Preparing to test credentials on OWA') 552 | logger.info(' Campaign Name: %s' % config.CAMPAIGN_NAME) 553 | logger.info(' OWA Domain: %s' % config.OWA_DOMAIN) 554 | logger.info(' OWA Server: %s' % config.OWA_SERVER) 555 | logger.info(' Credentials count: %i' % len(creds_list)) 556 | ret = query_yes_no('Do you want to continue?',default='no') 557 | if not ret: 558 | return 559 | 560 | owa = OwaCredsTester(creds_list, config.OWA_DOMAIN, config.OWA_SERVER) 561 | owa.test_logins() 562 | owa.print_results() 563 | creds_to_csv(config.CREDS_PATH, creds_list) 564 | 565 | def test_creds_netscaler(events_filter=None): 566 | creds_list = get_creds_from_timeline(get_timelines(events_filter)) 567 | 568 | logger.info('**WARNING**') 569 | logger.info('Too many attempts could lock accounts. Be easy with this feature.') 570 | logger.info('') 571 | logger.info('Preparing to test credentials on NetScaler') 572 | logger.info(' Campaign Name: %s' % config.CAMPAIGN_NAME) 573 | logger.info(' Netscaler Server: %s' % config.NETSCALER_SERVER) 574 | logger.info(' Credentials count: %i' % len(creds_list)) 575 | ret = query_yes_no('Do you want to continue?',default='no') 576 | if not ret: 577 | return 578 | 579 | nsc = NetscalerCredsTester(creds_list, config.NETSCALER_SERVER, 580 | verify_tls=config.VERIFY_TLS) 581 | nsc.test_logins() 582 | nsc.print_results() 583 | creds_to_csv(config.CREDS_PATH, creds_list) 584 | 585 | def test_creds_juniper(events_filter=None): 586 | creds_list = get_creds_from_timeline(get_timelines(events_filter)) 587 | 588 | logger.info('**WARNING**') 589 | logger.info('Too many attempts could lock accounts. Be easy with this feature.') 590 | logger.info('') 591 | logger.info('Preparing to test credentials on Juniper') 592 | logger.info(' Campaign Name: %s' % config.CAMPAIGN_NAME) 593 | logger.info(' Juniper Domain: %s' % config.JUNIPER_DOMAIN) 594 | logger.info(' Juniper Server: %s' % config.JUNIPER_SERVER) 595 | logger.info(' Juniper Uri: %s' % config.JUNIPER_URI) 596 | logger.info(' Juniper Realm: %s' % config.JUNIPER_REALM) 597 | logger.info(' Credentials count: %i' % len(creds_list)) 598 | ret = query_yes_no('Do you want to continue?',default='no') 599 | if not ret: 600 | return 601 | 602 | nsc = JuniperCredsTester(creds_list, config.JUNIPER_SERVER, 603 | config.JUNIPER_URI, config.JUNIPER_DOMAIN, 604 | config.JUNIPER_REALM, 605 | verify_tls=config.VERIFY_TLS) 606 | nsc.test_logins() 607 | nsc.print_results() 608 | creds_to_csv(config.CREDS_PATH, creds_list) 609 | 610 | def get_ips_from_timeline(timeline, incl_geoip=False): 611 | ips_list = {} 612 | for entry in timeline: 613 | # Check only messages that have an IP address. 614 | # e.g. Not "Email Sent" events. 615 | if entry.message in BROWSER_MSG and type(entry.details) is dict: 616 | ip = entry.details['browser']['address'] 617 | 618 | if ip in ips_list.keys(): 619 | ips_list[ip]['count']+=1 620 | 621 | else: 622 | ips_list[ip] = {} 623 | ips_list[ip]['count'] = 1 624 | 625 | if incl_geoip: 626 | if ip != '': 627 | js_out = get_geoip(ip) 628 | ips_list[ip]['geoip_city'] = js_out['city'] 629 | ips_list[ip]['geoip_region'] = js_out['regionName'] 630 | ips_list[ip]['geoip_timezone'] = js_out['timezone'] 631 | ips_list[ip]['geoip_isp'] = js_out['isp'] 632 | else: 633 | ips_list[ip]['geoip_city'] = '' 634 | ips_list[ip]['geoip_region'] = '' 635 | ips_list[ip]['geoip_timezone'] = '' 636 | ips_list[ip]['geoip_isp'] = '' 637 | 638 | 639 | return ips_list 640 | 641 | def get_ips_from_results(results, incl_geoip=False, incl_users=False): 642 | ips_list = {} 643 | for r in results: 644 | if r.ip in ips_list.keys(): 645 | ips_list[r.ip]['count']+=1 646 | 647 | if incl_users: 648 | ips_list[r.ip]['emails'].append(r.email) 649 | else: 650 | ips_list[r.ip] = {} 651 | ips_list[r.ip]['count'] = 1 652 | 653 | if incl_geoip: 654 | if r.ip != '': 655 | js_out = get_geoip(r.ip) 656 | ips_list[r.ip]['geoip_city'] = js_out['city'] 657 | ips_list[r.ip]['geoip_region'] = js_out['regionName'] 658 | ips_list[r.ip]['geoip_timezone'] = js_out['timezone'] 659 | ips_list[r.ip]['geoip_isp'] = js_out['isp'] 660 | else: 661 | ips_list[r.ip]['geoip_city'] = '' 662 | ips_list[r.ip]['geoip_region'] = '' 663 | ips_list[r.ip]['geoip_timezone'] = '' 664 | ips_list[r.ip]['geoip_isp'] = '' 665 | 666 | if incl_users: 667 | ips_list[r.ip]['emails'] = [r.email] 668 | 669 | return ips_list 670 | 671 | # -- Warning -- 672 | # Parameter "from_timeline" will determine if the stats are generated from the 673 | # timeline instead of the results. Here is what you need to know. 674 | # 675 | # timeline: May contains duplicate so it is undesirable for global statistics 676 | # results: Will not contains duplicate. Seems to be the last IP used by the user. 677 | # Not useful for analysis of a single user 678 | # 679 | def print_targets_ip(events_filter=None, from_timeline=False, show_geoip=False, show_users=False): 680 | if show_geoip: 681 | title = ['IP Address', 'Hit Count', 'City', 'Region', 'Timezone', 'ISP'] 682 | else: 683 | title = ['IP Address', 'Hit Count'] 684 | if show_users: 685 | title.append('Users') 686 | 687 | if from_timeline: 688 | ips = get_ips_from_timeline(get_timelines(events_filter), incl_geoip=show_geoip) 689 | else: 690 | ips = get_ips_from_results(get_results(events_filter), incl_geoip=show_geoip, 691 | incl_users=show_users) 692 | x = PrettyTable(title) 693 | x.align['IP Address'] = 'l' 694 | if show_geoip: 695 | x.align['City'] = 'l' 696 | x.align['Region'] = 'l' 697 | x.align['Timezone'] = 'l' 698 | x.align['ISP'] = 'l' 699 | if show_users: 700 | x.align['Users'] = 'l' 701 | x.padding_width = 1 702 | x.max_width = 100 703 | for ip,ip_info in ips.items(): 704 | row = [] 705 | if ip == '': 706 | ip = 'No IP. Email Sent Only' 707 | 708 | row.append(ip) 709 | for key,value in ip_info.items(): 710 | if key == 'emails': 711 | row.append(' '.join(value)) 712 | else: 713 | row.append(value) 714 | 715 | if len(row) > 0: 716 | x.add_row(row) 717 | print(x.get_string(sortby='Hit Count',reversesort=True)) 718 | 719 | # Also save to a file for later reporting. 720 | # TODO: This is unclean. 721 | if show_geoip and not show_users: 722 | ips_to_csv(config.GEOIP_PATH, ips) 723 | 724 | 725 | def print_email_stats(email, show_geoip=False): 726 | ef = EventsFilter(email=email) 727 | 728 | # Print all user timeline 729 | logger.info('User timeline.') 730 | print_timeline(ef) 731 | 732 | # Print IP addresses used 733 | logger.info('IP addresses used by this user.') 734 | print_targets_ip(ef,from_timeline=True,show_geoip=show_geoip) 735 | 736 | # Print submitted credentials 737 | logger.info('Credentials sent by this user.') 738 | print_creds(ef) 739 | 740 | def generate_report(): 741 | timelines = get_timelines() 742 | results = get_results() 743 | reporter = GophishReporter(timelines, results) 744 | reporter.generate() 745 | 746 | 747 | # Get args 748 | usage = 'usage: %prog action [options]' 749 | description = 'Gophish cli. Use this tool to quickly setup a phishing campaign using your gophish infrastructure.' 750 | parser = argparse.ArgumentParser(description=description) 751 | parser.add_argument('-v','--version', action='version', version='%(prog)s 0.01 (2017-02-06)') 752 | parser.add_argument('-c', '--config', action='store', dest='config', default=None, \ 753 | help='Alternative config file. Default is config.py (Not implemented yet)') 754 | parser.add_argument('-d', '--debug', action='store_true', dest='debug', default=False, \ 755 | help='Run the tool in debug mode') 756 | 757 | # Groups 758 | subparsers = parser.add_subparsers(dest='action') 759 | p_group_desc = '''\ 760 | types: 761 | UINT Unsigned Integer value. 762 | STR String. 763 | FILE Path to a file. 764 | ''' 765 | p_group_epilog = '''\ 766 | Example: 767 | --add --name Group_Name --targets-csv '/path/to/csv' # Create a group and import a CSV file containing targets (users). 768 | 769 | --delete # Delete all groups based on config.py. 770 | --delete --id 1 # Delete group id 1. 771 | --delete --name JohnDoe # Delete group name "JohnDoe". 772 | --delete --prefix 'meh_' # Delete all groups that starts with 'meh_'. 773 | 774 | --list # List all groups of the database 775 | --list --prefix 'meh_' # List groups that starts with 'meh_' 776 | ''' 777 | p_group = subparsers.add_parser('group', description=p_group_desc, epilog=p_group_epilog, 778 | formatter_class=argparse.RawDescriptionHelpFormatter, 779 | help='Manage groups.') 780 | p_group_action = p_group.add_argument_group("Action") 781 | p_group_action.add_argument('--add', action='store_true', dest='add', \ 782 | help='Add a group.') 783 | p_group_action.add_argument('--delete', action='store_true', dest='delete', \ 784 | help='Delete a group.') 785 | p_group_action.add_argument('--list', '-l', action='store_true', dest='list', \ 786 | help='List groups.') 787 | 788 | p_group_param = p_group.add_argument_group("Parameters") 789 | p_group_param.add_argument('--name', action='store', dest='name', default=None, \ 790 | type=str, metavar='STR', \ 791 | help='For --add and --delete only. A group name. Mandatory for --add.') 792 | p_group_param.add_argument('--targets-csv', action='store', dest='targets_csv', default=None, \ 793 | type=str, metavar='FILE', \ 794 | help='A CSV file with a list of users. Mandatory for --add.') 795 | p_group_param.add_argument('--id', action='store', dest='id', default=None, \ 796 | type=int, metavar='UINT', \ 797 | help='For --delete only. A group id to delete. NOT IMPLEMENTED YET.') 798 | p_group_param.add_argument('--prefix', action='store', dest='prefix', default=None, \ 799 | type=str, metavar='STR', \ 800 | help='A prefix filter. Can be used with --list and --delete. NOT IMPLEMENTED YET.') 801 | 802 | # Campaign 803 | p_campaign_desc = '''\ 804 | types: 805 | UINT Unsigned Integer value. 806 | IP IP address 807 | STR String. 808 | FILE Path to a file. 809 | ''' 810 | p_campaign_epilog = '''\ 811 | Example: 812 | --start # Start a campaign. All parameters are in the config.py file. 813 | --start --new-groups # Upload groups and then start the campaign. See config.py. 814 | --complete # End a campaign. All parameters are in the config.py file. 815 | 816 | --delete # Delete all batches based on config.py 817 | --delete --delete-groups # Delete all campaigns and groups based on config.py 818 | --delete --id 1 # Delete campaign id 1. 819 | --delete --name JohnDoe # Delete campaign name "JohnDoe". 820 | --delete --prefix 'meh_' # Delete all campaigns that starts with 'meh_'. 821 | 822 | --list # List all campaigns of the database 823 | --list --prefix 'meh_' # List campaigns that starts with 'meh_' 824 | 825 | --results # Download and save Phishing results (Timeline + Credentials). 826 | --ip-timeline 1.2.3.4 # Print the timeline of a single IP address 827 | ''' 828 | p_campaign = subparsers.add_parser('campaign', description=p_campaign_desc, epilog=p_campaign_epilog, 829 | formatter_class=argparse.RawDescriptionHelpFormatter, 830 | help='Manage campaigns.') 831 | p_campaign_action = p_campaign.add_argument_group("Action") 832 | p_campaign_action.add_argument('--start', action='store_true', dest='start', \ 833 | help='Start a campaign.') 834 | p_campaign_action.add_argument('--complete', action='store_true', dest='complete', \ 835 | help='Complete a campaign.') 836 | p_campaign_action.add_argument('--delete', action='store_true', dest='delete', \ 837 | help='Delete a campaign.') 838 | p_campaign_action.add_argument('--list', '-l', action='store_true', dest='list', \ 839 | help='List campaigns.') 840 | p_campaign_action.add_argument('--results', action='store_true', dest='results', \ 841 | help='Download and save results.') 842 | p_campaign_action.add_argument('--ip-timeline', action='store', dest='ip_timeline', \ 843 | type=str, metavar='IP', \ 844 | help='Print timeline from a specific IP address.') 845 | 846 | p_campaign_param = p_campaign.add_argument_group("Parameters") 847 | p_campaign_param.add_argument('--name', action='store', dest='name', default=None, \ 848 | type=str, metavar='STR', \ 849 | help='For --delete only. A campaign name to delete. NOT IMPLEMENTED YET.') 850 | p_campaign_param.add_argument('--id', action='store', dest='id', default=None, \ 851 | type=int, metavar='UINT', \ 852 | help='For --delete only. A campaign id to delete. NOT IMPLEMENTED YET') 853 | p_campaign_param.add_argument('--prefix', action='store', dest='prefix', default=None, \ 854 | type=str, metavar='STR', \ 855 | help='A prefix filter. Can be used with --list and --delete. NOT IMPLEMENTED YET.') 856 | p_campaign_param.add_argument('--new-groups', action='store_true', dest='new_groups', 857 | help='Import new groups for the campaign.') 858 | p_campaign_param.add_argument('--delete-groups', action='store_true', dest='delete_groups', 859 | help='Delete all groups with the same prefix.') 860 | 861 | # Creds 862 | p_creds_epilog = '''\ 863 | Example: 864 | --print # Print the credentials. 865 | 866 | --test-owa # Test credentials on OWA. 867 | --test-netscaler # Test credentials on a NetScaler. 868 | --test-juniper # Test credentials on a Juniper. 869 | ''' 870 | p_creds = subparsers.add_parser('creds', epilog=p_creds_epilog, 871 | formatter_class=argparse.RawDescriptionHelpFormatter, 872 | help='Manage credentials.') 873 | p_creds_action = p_creds.add_argument_group("Action") 874 | p_creds_action.add_argument('--print', action='store_true', dest='print_creds', \ 875 | help='Print the credentials.') 876 | p_creds_action.add_argument('--test-owa', action='store_true', dest='test_creds_owa', \ 877 | help='Test the credentials on OWA.') 878 | p_creds_action.add_argument('--test-netscaler', action='store_true', dest='test_creds_netscaler', \ 879 | help='Test the credentials on NetScaler.') 880 | p_creds_action.add_argument('--test-juniper', action='store_true', dest='test_creds_juniper', \ 881 | help='Test the credentials on juniper.') 882 | 883 | 884 | # Stats 885 | p_stats_desc = '''\ 886 | types: 887 | UINT Unsigned Integer value. 888 | STR String. 889 | FILE Path to a file. 890 | ''' 891 | p_stats_epilog = '''\ 892 | Example: 893 | --targets-ip # Dump the list of IP addresses so you can do geolocalisation stats. 894 | --targets-ip --geoip # Dump the list of IP addresses with geolocation information for each item. 895 | --targets-ip --users # Dump the list of IP addresses and their corresponding users. 896 | 897 | --email someone@example.org # Print statistics of this user. 898 | --email someone@example.org --geoip # Print statistics + geolocation info of this user. 899 | ''' 900 | p_stats = subparsers.add_parser('stats', description=p_stats_desc, epilog=p_stats_epilog, 901 | formatter_class=argparse.RawDescriptionHelpFormatter, 902 | help='Manage statss.') 903 | p_stats_action = p_stats.add_argument_group("Action") 904 | p_stats_action.add_argument('--targets-ip', action='store_true', dest='targets_ip', \ 905 | help='Get a list of targets IP addresses.') 906 | p_stats_action.add_argument('--email', action='store', dest='email', \ 907 | help='Get statistics of a single email address') 908 | 909 | p_stats_param = p_stats.add_argument_group("Parameters") 910 | p_stats_param.add_argument('--geoip', action='store_true', dest='geoip', default=None, \ 911 | help='Show geolocation information.') 912 | p_stats_param.add_argument('--users', action='store_true', dest='users', default=None, \ 913 | help='Show associated users.') 914 | 915 | 916 | # Report 917 | p_report_desc = '''\ 918 | types: 919 | UINT Unsigned Integer value. 920 | STR String. 921 | FILE Path to a file. 922 | ''' 923 | p_report_epilog = '''\ 924 | Example: 925 | ''' 926 | p_report = subparsers.add_parser('report', description=p_report_desc, epilog=p_report_epilog, 927 | formatter_class=argparse.RawDescriptionHelpFormatter, 928 | help='Generate report (currently multiple CSV files).') 929 | #p_report_action = p_report.add_argument_group("Action") 930 | 931 | #p_report_param = p_report.add_argument_group("Parameters") 932 | 933 | args = parser.parse_args() 934 | 935 | # Overwrite config variables 936 | if hasattr(args, 'targets_csv') and args.targets_csv is not None: 937 | print('Overwritting config.EMAILS_PATH from --targets-csv') 938 | config.EMAILS_PATH = args.targets_csv 939 | 940 | if hasattr(args, 'name') and args.name is not None: 941 | print('Overwritting config.CAMPAIGN_NAME from --name') 942 | config.CAMPAIGN_NAME = args.name 943 | 944 | DEBUG = args.debug 945 | if DEBUG == True: 946 | logger.setLevel('DEBUG') 947 | 948 | logger.debug('Arguments: ' + str(args)) 949 | 950 | if args.action == 'group': 951 | if args.add: 952 | create_groups() 953 | elif args.delete: 954 | delete_groups() 955 | elif args.list: 956 | print_groups(args.prefix) 957 | else: 958 | parser.print_help() 959 | elif args.action == 'campaign': 960 | if args.start: 961 | if args.new_groups: 962 | create_groups() 963 | create_campaigns() 964 | elif args.complete: 965 | complete_campaigns() 966 | elif args.delete: 967 | delete_campaigns() 968 | if args.delete_groups: 969 | delete_groups() 970 | elif args.list: 971 | print_campaigns() 972 | elif args.results: 973 | save_campaigns() 974 | elif args.ip_timeline: 975 | print_timeline(EventsFilter(ip=args.ip_timeline)) 976 | else: 977 | parser.print_help() 978 | elif args.action == 'creds': 979 | if args.print_creds: 980 | print_creds() 981 | elif args.test_creds_owa: 982 | test_creds_owa() 983 | elif args.test_creds_netscaler: 984 | test_creds_netscaler() 985 | elif args.test_creds_juniper: 986 | test_creds_juniper() 987 | else: 988 | parser.print_help() 989 | elif args.action == 'stats': 990 | if args.targets_ip: 991 | print_targets_ip(show_geoip=args.geoip, show_users=args.users) 992 | elif args.email: 993 | print_email_stats(args.email, show_geoip=args.geoip) 994 | else: 995 | parser.print_help() 996 | elif args.action == 'report': 997 | generate_report() 998 | save_campaigns() 999 | print_targets_ip(show_geoip=True, show_users=False) 1000 | else: 1001 | parser.print_help() 1002 | 1003 | -------------------------------------------------------------------------------- /modules/creds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Basic Credentials Testing class 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2017, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | 34 | from urllib.request import HTTPRedirectHandler,HTTPCookieProcessor,\ 35 | HTTPSHandler, HTTPErrorProcessor, \ 36 | build_opener, install_opener 37 | import http.cookiejar 38 | import ssl 39 | 40 | class NoRedirection(HTTPErrorProcessor): 41 | def http_response(self, request, response): 42 | return response 43 | https_response = http_response 44 | 45 | class Credentials(): 46 | def __init__(self, email=None, username=None, password=None): 47 | self.email = email 48 | self.username = username 49 | self.password = password 50 | self.is_valid = None 51 | 52 | def get_validity(self): 53 | if self.is_valid is None: 54 | return 'unknown' 55 | elif self.is_valid == True: 56 | return 'Success' 57 | else: 58 | return 'Failed' 59 | 60 | def to_list(self): 61 | return [self.email, self.username, self.password, self.get_validity()] 62 | 63 | def to_dict(self): 64 | return {'email': self.email, 65 | 'username': self.username, 66 | 'password': self.password, 67 | 'is_valid': self.get_validity()} 68 | 69 | class CredsTester(object): 70 | 71 | def __init__(self, creds_list, server, verbose=True, debug=False): 72 | self.creds_list = creds_list 73 | self.server = server 74 | self.verbose = verbose 75 | self.debug = debug 76 | 77 | def _sanitize_username(self, username): 78 | # If username contains a \, take only the last part 79 | # Example: DOMAIN\user1 become user1 80 | if '\\' in username: 81 | a = username.split('\\') 82 | if len(a) > 1: 83 | username = a[-1] 84 | else: 85 | username = None 86 | 87 | # Ignore empty or very small usernames 88 | if len(username) < 3: 89 | username = None 90 | 91 | return username 92 | 93 | def _sanitize_password(self, password): 94 | # Ignore empty or very small passwords 95 | if len(password) < 5: 96 | return None 97 | 98 | return password 99 | 100 | def _print_login_result(self, valid, username, password): 101 | if self.verbose: 102 | if valid: 103 | print('%s - %s - Successful login' % (username, password)) 104 | else: 105 | print('%s - %s - Failed login' % (username, password)) 106 | 107 | def print_results(self): 108 | creds_success = sum([1 for creds in self.creds_list if creds.is_valid]) 109 | creds_fail = sum([1 for creds in self.creds_list if creds.is_valid == False]) 110 | creds_unknown = sum([1 for creds in self.creds_list if creds.is_valid is None]) 111 | print('[-] Test results: %s successful, %s fail and %s unknown (duplicate) out of %s.' 112 | % (creds_success, creds_fail, creds_unknown, len(self.creds_list))) 113 | 114 | def test_logins(self): 115 | pass 116 | 117 | class WebCredsTester(CredsTester): 118 | def __init__(self, creds_list, server, uri='/', verify_tls=True, debug=False): 119 | super(WebCredsTester, self).__init__(creds_list, server, debug=debug) 120 | self.uri = uri 121 | self.verify_tls = verify_tls 122 | self._init_urllib() 123 | 124 | def _init_urllib(self): 125 | # Initialize a SSL context for all HTTPS calls 126 | if self.verify_tls: 127 | context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1) 128 | context.verify_mode = ssl.CERT_REQUIRED 129 | context.check_hostname = True 130 | context.load_default_certs() 131 | else: 132 | context = ssl.create_default_context() # Should we enforce TLS 1.1 here? 133 | context.check_hostname = False 134 | context.verify_mode = ssl.CERT_NONE 135 | 136 | # Cookie Jar 137 | self.cj = http.cookiejar.CookieJar() 138 | 139 | # Debugging 140 | if self.debug: 141 | debuglevel=1 142 | else: 143 | debuglevel=0 144 | 145 | opener = build_opener(HTTPSHandler(debuglevel=debuglevel, context=context), \ 146 | HTTPCookieProcessor(self.cj), 147 | NoRedirection) 148 | 149 | install_opener(opener) 150 | 151 | def _get_cookie(self, name): 152 | for cookie in self.cj: 153 | if cookie.name == name: 154 | return cookie 155 | return None 156 | 157 | # The actual login test, called for every pair of credentials 158 | def _test_login(self, username, password): 159 | return False 160 | 161 | # To test the list of credentials 162 | def test_logins(self): 163 | pass 164 | -------------------------------------------------------------------------------- /modules/juniper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Class to test juniper authentication. 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2017, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | 34 | from urllib.request import urlopen 35 | from urllib.parse import urlencode 36 | import urllib.error 37 | 38 | from modules.creds import WebCredsTester 39 | 40 | class JuniperCredsTester(WebCredsTester): 41 | 42 | def __init__(self, creds_list, server, uri, domain, realm, verify_tls=True, 43 | debug=False): 44 | super(JuniperCredsTester, self).__init__(creds_list, server, uri, 45 | verify_tls, debug=debug) 46 | self.domain = domain 47 | self.realm = realm 48 | 49 | def _test_login(self, username, password, realm): 50 | self.cj.clear_session_cookies() 51 | 52 | # TODO: Get realm directly from the login page. 53 | data = urlencode({'username': username, 'password': password, 54 | 'realm': realm, 'btnSubmit': 'Sign In - Connexion'}).encode('UTF-8') 55 | 56 | url = 'https://%s%s' % (self.server,self.uri) 57 | #print('URL: %s' % url) 58 | 59 | try: 60 | response = urlopen(url, data=data, timeout=2) 61 | 62 | if self.debug: 63 | print(response.info()) 64 | 65 | # According to my tests, three cookies are obtained when successful 66 | # login and that there is no current session 67 | # 68 | # DSASSERTREF=x; path=/; expires=Thu, 01 Jan 1970 22:00:00 GMT; secure 69 | # DSID=d4f163fb7181541234aa8b170bb1913e; path=/; secure 70 | # DSFirstAccess=1487947220; path=/; secure 71 | # 72 | # Here we will only check for DSID. 73 | # 74 | dsid_cookie = self._get_cookie('DSID') 75 | if dsid_cookie is not None: 76 | return True 77 | 78 | # A redirection to a user confirmation page also means successful login. 79 | # Ex: 80 | #location: https://vpn.example.com/dana-na/auth/url/welcome.cgi 81 | # ?p=user-confirm&id=state_29c26b3530e9e540ae3e961bcb612134 82 | # 83 | # Here, we must analyze the location field. 84 | # 85 | headers = response.info() 86 | if 'location' in headers \ 87 | and 'welcome.cgi?p=user-confirm' in headers['location']: 88 | print('%s is currently connected.' % username) 89 | return True 90 | 91 | return False 92 | 93 | except urllib.error.HTTPError as e: 94 | # If return code is 500, consider it as a failure 95 | # but warn the user. 96 | if e.code == 500: 97 | print('Warning: Got a 500 response. Something went wrong.') 98 | return False 99 | else: 100 | print(e) 101 | 102 | def test_logins(self): 103 | tested_usernames = [] # Keep a track of tested usernames to avoid 104 | # testing twice and eventually lock accounts. 105 | for creds in self.creds_list: 106 | username = self._sanitize_username(creds.username) 107 | password = self._sanitize_password(creds.password) 108 | if username is not None and password is not None: 109 | username = '%s\\%s' % (self.domain, username) 110 | if not (username.lower() in tested_usernames): 111 | #print('Testing: %s - %s - %s' % (username,creds.password)) 112 | valid = self._test_login(username, password, self.realm) 113 | self._print_login_result(valid, username, password) 114 | creds.is_valid = valid 115 | tested_usernames.append(username.lower()) 116 | else: 117 | print('Username already tested: %s. Skipping.' % username) 118 | else: 119 | print('Invalid username or password: (%s - %s). Skipping.' % (username,password)) 120 | 121 | 122 | -------------------------------------------------------------------------------- /modules/netscaler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Class that test netscaler authentication. 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2017, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | 34 | from urllib.request import HTTPRedirectHandler,HTTPCookieProcessor,\ 35 | HTTPHandler, HTTPSHandler, \ 36 | build_opener, install_opener, urlopen 37 | from urllib.parse import urlencode 38 | import urllib.error 39 | import http.cookiejar 40 | import ssl 41 | 42 | from modules.creds import CredsTester 43 | 44 | class NetscalerCredsTester(CredsTester): 45 | 46 | def __init__(self, creds_list, server, uri='/cgi/login',verify_tls=True): 47 | super(NetscalerCredsTester, self).__init__(creds_list, server) 48 | self.uri = uri 49 | self.verify_tls = verify_tls 50 | self._init_urllib() 51 | 52 | def _init_urllib(self): 53 | # Initialize a SSL context for all HTTPS calls 54 | if self.verify_tls: 55 | context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1) 56 | context.verify_mode = ssl.CERT_REQUIRED 57 | context.check_hostname = True 58 | context.load_default_certs() 59 | else: 60 | context = ssl.create_default_context() # Should we enforce TLS 1.1 here? 61 | context.check_hostname = False 62 | context.verify_mode = ssl.CERT_NONE 63 | 64 | # Cookie Jar 65 | self.cj = http.cookiejar.CookieJar() 66 | 67 | opener = build_opener(HTTPSHandler(context=context), \ 68 | HTTPHandler(), \ 69 | HTTPCookieProcessor(self.cj)) 70 | 71 | install_opener(opener) 72 | 73 | def _get_cookie(self, name): 74 | for cookie in self.cj: 75 | if cookie.name == name: 76 | return cookie 77 | return None 78 | 79 | # 80 | # WARNING 81 | # 82 | # This is very experimental... (and ugly) 83 | # 84 | def _test_login(self, username, password): 85 | self.cj.clear_session_cookies() 86 | data = urlencode({'login': username, 'passwd': password}).encode('UTF-8') 87 | url = 'https://%s%s' % (self.server,self.uri) 88 | #print('URL: %s' % url) 89 | 90 | try: 91 | response = urlopen(url, data=data, timeout=2) 92 | 93 | # If the NSC_VPNERR cookie is set, an error occured. 94 | nsc_error_cookie = self._get_cookie('NSC_VPNERR') 95 | if nsc_error_cookie is not None: 96 | return False 97 | 98 | # If the returning code is not 500, it may be a 302 with 2 cookies. 99 | nsc_tmaa_cookie = self._get_cookie('NSC_TMAA') 100 | nsc_tmas_cookie = self._get_cookie('NSC_TMAS') 101 | if nsc_tmaa_cookie is not None \ 102 | and nsc_tmas_cookie is not None: 103 | return True 104 | 105 | # Otherwise, consider failed login. 106 | return False 107 | 108 | except urllib.error.HTTPError as e: 109 | # In several case, a 500 means successful, meaning the login was successful 110 | # but the redirection (or else) failed. 111 | if e.code == 500: 112 | return True 113 | 114 | def test_logins(self): 115 | tested_usernames = [] # Keep a track of tested usernames to avoid 116 | # testing twice and eventually lock accounts. 117 | for creds in self.creds_list: 118 | username = self._sanitize_username(creds.username) 119 | password = self._sanitize_password(creds.password) 120 | if username is not None and password is not None: 121 | if not (username.lower() in tested_usernames): 122 | #print('Testing: %s - %s - %s' % (username,creds.password)) 123 | valid = self._test_login(username, password) 124 | self._print_login_result(valid, username, password) 125 | creds.is_valid = valid 126 | tested_usernames.append(username.lower()) 127 | else: 128 | print('Username already tested: %s. Skipping.' % username) 129 | else: 130 | print('Invalid username or password: (%s - %s). Skipping.' % (username,password)) 131 | 132 | -------------------------------------------------------------------------------- /modules/owa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Class to test harvested usernames and passwords on a Outlook Web Access (OWA) 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2017, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | 34 | from exchangelib import DELEGATE, IMPERSONATION, Account, Credentials, \ 35 | EWSDateTime, EWSTimeZone, Configuration, NTLM, CalendarItem, Message, \ 36 | Mailbox, Attendee, Q 37 | from exchangelib.folders import Calendar, ExtendedProperty, FileAttachment, ItemAttachment, \ 38 | HTMLBody 39 | 40 | import signal 41 | import time 42 | 43 | import logging 44 | 45 | from modules.creds import CredsTester 46 | 47 | class Timeout(): 48 | """Timeout class using ALARM signal.""" 49 | class Timeout(Exception): 50 | pass 51 | 52 | def __init__(self, sec): 53 | self.sec = sec 54 | 55 | def __enter__(self): 56 | signal.signal(signal.SIGALRM, self.raise_timeout) 57 | signal.alarm(self.sec) 58 | 59 | def __exit__(self, *args): 60 | signal.alarm(0) # disable alarm 61 | 62 | def raise_timeout(self, *args): 63 | raise Timeout.Timeout() 64 | 65 | class OwaCredsTester(CredsTester): 66 | 67 | def __init__(self, creds_list, domain, server, autodiscover=False, timeout=2, 68 | verbose=True): 69 | super(OwaCredsTester, self).__init__(creds_list, server, verbose) 70 | self.domain = domain 71 | self.autodiscover=autodiscover 72 | self.timeout = timeout 73 | 74 | # This is a temporary hack because the library print too much 75 | # unwanted details 76 | logging.disable(logging.CRITICAL) 77 | 78 | def _sanitize_username(self, username): 79 | username = super(OwaCredsTester, self)._sanitize_username(username) 80 | 81 | # We force the use of our domain as people could do typo or write pure shit. 82 | # TODO: Make this behavior optional as some companies have multiple domains. 83 | if username is not None: 84 | return '%s\\%s' % (self.domain, username) 85 | else: 86 | return None 87 | 88 | def _test_login_time_based(self, email, username, password): 89 | try: 90 | with Timeout(self.timeout): 91 | config = Configuration( 92 | server=self.server, 93 | credentials=Credentials(username=username, password=password), 94 | auth_type=NTLM 95 | ) 96 | # account = Account(primary_smtp_address=email, config=config, 97 | # access_type=DELEGATE) 98 | return True 99 | except Timeout.Timeout: 100 | return False 101 | except AttributeError: 102 | return False 103 | 104 | # Autodiscover 105 | # NOT TESTED 106 | # def _test_login_time_based_ad(self, email, user, pass): 107 | # credentials = Credentials(username='some_name', password='some_pass') 108 | # 109 | # # If your credentials have been given impersonation access to the target account, use 110 | # # access_type=IMPERSONATION 111 | # account = Account(primary_smtp_address='email@example.com', credentials=credentials, 112 | # autodiscover=True, access_type=DELEGATE) 113 | 114 | def test_logins(self): 115 | tested_usernames = [] # Keep a track of tested usernames to avoid 116 | # testing twice and eventually lock accounts. 117 | for creds in self.creds_list: 118 | username = self._sanitize_username(creds.username) 119 | password = self._sanitize_password(creds.password) 120 | if username is not None and password is not None: 121 | if not (username.lower() in tested_usernames): 122 | #print('Testing: %s - %s - %s' % (creds.email,username,creds.password)) 123 | valid = self._test_login_time_based(creds.email, username, password) 124 | self._print_login_result(valid, username, password) 125 | creds.is_valid = valid 126 | tested_usernames.append(username.lower()) 127 | else: 128 | print('Username already tested: %s. Skipping.' % username) 129 | else: 130 | print('Invalid username or password: (%s - %s). Skipping.' % (username,password)) 131 | 132 | 133 | -------------------------------------------------------------------------------- /modules/report.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Reporting class to gather information from the campaign 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2018, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | 34 | import os 35 | import re 36 | import ssl 37 | import time 38 | import json 39 | import config 40 | import pytz 41 | import logging 42 | import datetime 43 | import dateutil.parser 44 | import paramiko 45 | import sendgrid 46 | import urllib.request 47 | import apache_log_parser 48 | from prettytable import PrettyTable 49 | 50 | BROWSER_MSG = ['Email Opened', 'Clicked Link', 'Submitted Data'] 51 | 52 | logger = logging.getLogger('gophish-cli') 53 | 54 | class GophishReporter(): 55 | timeline = None 56 | results = None 57 | empire_agents = None 58 | excluded_ip = [] 59 | stats = {} 60 | 61 | out_folder = config.WORKING_DIR + 'report_%s/' % time.strftime("%Y%m%d-%H%M%S") 62 | apache_folder = out_folder + 'apache_logs/' 63 | 64 | # Flags to easily enable/disable features. 65 | enable_apache = True 66 | enable_sendgrid = True 67 | enable_empire = False 68 | enable_msf = False 69 | enable_cobalt = False 70 | 71 | def __init__(self, timeline, results): 72 | self.timeline = timeline 73 | self.results = results 74 | self.excluded_ip = config.EXCLUDED_IP 75 | self.timezone = pytz.timezone(config.GOPHISH_TIMEZONE) 76 | 77 | def _setup_out_folder(self): 78 | if not os.path.exists(self.out_folder): 79 | logger.debug("Creating folder: %s" % self.out_folder) 80 | os.makedirs(self.out_folder) 81 | 82 | if not os.path.exists(self.apache_folder): 83 | logger.debug("Creating folder: %s" % self.apache_folder) 84 | os.makedirs(self.apache_folder) 85 | 86 | def _ssh_agent_auth(self, transport, username): 87 | """ 88 | Attempt to authenticate to the given transport using any of the private 89 | keys available from an SSH agent 90 | """ 91 | 92 | logger.debug('[SSH] Attempting to authenticate') 93 | agent = paramiko.Agent() 94 | agent_keys = agent.get_keys() 95 | if len(agent_keys) == 0: 96 | return 97 | 98 | for key in agent_keys: 99 | logger.debug('[SSH] Trying ssh-agent key %s' % key.get_fingerprint().hex()) 100 | try: 101 | transport.auth_publickey(username, key) 102 | logger.debug('[SSH]... success!') 103 | return 104 | except paramiko.SSHException as e: 105 | logger.debug('[SSH]... failed!', e) 106 | 107 | # Extract specific keys and return a list of their values. 108 | # Useful to extract empire unique users, workstations or operating systems 109 | def _json_get_unique_key(self, json_obj, key): 110 | return list(set([obj[key] for obj in json_obj])) 111 | 112 | def _json_get_count_key(self, json_obj, key): 113 | d = dict() 114 | for obj in json_obj: 115 | key_value = obj[key] 116 | d[key_value] = d.get(key_value, 0) + 1 117 | return d 118 | 119 | def _get_timeline_unique_key(self, key): 120 | return list(set([getattr(obj,key) for obj in self.timeline])) 121 | 122 | def _get_timeline_key_count(self, key): 123 | d = dict() 124 | for obj in self.timeline: 125 | key_value = getattr(obj,key) 126 | d[key_value] = d.get(key_value, 0) + 1 127 | return d 128 | 129 | def _get_results_unique_key(self, key): 130 | return list(set([getattr(obj,key) for obj in self.results])) 131 | 132 | def _get_results_key_count(self, key): 133 | d = dict() 134 | for obj in self.results: 135 | key_value = getattr(obj,key) 136 | d[key_value] = d.get(key_value, 0) + 1 137 | return d 138 | 139 | def _get_apache_key_count(self, key): 140 | d = dict() 141 | line_parser = apache_log_parser.make_parser(config.APACHE_LOGS_FORMAT) 142 | for log_line in self.apache_malware_dl: 143 | log_line_data = line_parser(log_line) 144 | key_value = log_line_data[key] 145 | d[key_value] = d.get(key_value, 0) + 1 146 | return d 147 | 148 | def _grep_files(self, path, search): 149 | res = [] 150 | for root, dirs, fnames in os.walk(path): 151 | for fname in fnames: 152 | filepath = os.path.join(root, fname) 153 | with open(filepath, 'r', encoding = "ISO-8859-1") as f: 154 | for line in f: 155 | if search in line: 156 | res.append(line) 157 | return res 158 | 159 | def get_apache_logs(self): 160 | ssh = paramiko.Transport((config.APACHE_HOST, config.APACHE_SSH_PORT)) 161 | ssh.start_client() 162 | self._ssh_agent_auth(ssh, config.APACHE_SSH_USER) 163 | 164 | if not ssh.is_authenticated(): 165 | logger.error("[SSH] Authentication failed. Make sure that your key is added to SSH agent. If not, use ssh-add.") 166 | sys.exit(1) 167 | else: 168 | logger.debug("[SSH] Authentication successful") 169 | 170 | #ssh.set_missing_host_key_policy(AllowAnythingPolicy()) 171 | sftp = ssh.open_session() 172 | sftp = paramiko.SFTPClient.from_transport(ssh) 173 | logger.debug('[SSH] Changing directory: %s' % config.APACHE_LOGS_FOLDER) 174 | sftp.chdir(config.APACHE_LOGS_FOLDER) 175 | for filename in sorted(sftp.listdir()): 176 | if filename.startswith(config.APACHE_LOGS_PREFIX): 177 | logger.debug('[SSH] Downloading: %s' % filename) 178 | sftp.get(filename, self.apache_folder + filename) 179 | 180 | sftp.close() 181 | ssh.close() 182 | 183 | # TODO: Unzip *.gz 184 | self.apache_malware_dl = self._grep_files(self.apache_folder, config.APACHE_MALWARE_NAME) 185 | logger.debug(' Got %s malware download' % len(self.apache_malware_dl)) 186 | 187 | def get_empire_agents(self): 188 | header={'Content-Type': 'application/json'} 189 | url = '%s/api/agents?token=%s' % (config.EMPIRE_API_URL, config.EMPIRE_API_KEY) 190 | 191 | ctx = ssl.create_default_context() 192 | ctx.check_hostname = False 193 | ctx.verify_mode = ssl.CERT_NONE 194 | 195 | req = urllib.request.Request(url=url, headers=header, method='GET') 196 | res = urllib.request.urlopen(req, timeout=5, context=ctx) 197 | res_body = res.read() 198 | self.empire_agents = json.loads(res_body.decode('utf-8'))['agents'] 199 | logger.debug(' Got %s agents' % len(self.empire_agents)) 200 | 201 | def get_sendgrid_stats(self): 202 | sg = sendgrid.SendGridAPIClient(apikey=config.SENDGRID_API_KEY) 203 | start_date = self.get_first_event_ts().strftime('%Y-%m-%d') 204 | params = {'aggregated_by': 'day', 'limit': 1, 'start_date': start_date, 'end_date': start_date, 'offset': 1} 205 | response = sg.client.stats.get(query_params=params) 206 | if response.status_code == 200: 207 | self.sendgrid_stats = json.loads(response.body.decode('utf-8'))[0]['stats'][0]['metrics'] 208 | else: 209 | self.sendgrid_stats = [] 210 | 211 | def get_msf_shells(self): 212 | pass 213 | 214 | def get_cobaltstrike_shells(self): 215 | pass 216 | 217 | def get_first_event_ts(self): 218 | return dateutil.parser.parse(self.timeline[0].time).astimezone(self.timezone) 219 | 220 | def get_last_event_ts(self): 221 | return dateutil.parser.parse(self.timeline[-1].time).astimezone(self.timezone) 222 | 223 | def extract_gophish_stats(self): 224 | self.stats['first_event_ts'] = self.get_first_event_ts().strftime("%Y-%m-%d %H:%M:%S") 225 | self.stats['last_event_ts'] = self.get_last_event_ts().strftime("%Y-%m-%d %H:%M:%S") 226 | 227 | self.stats['email_sent_ct'] = len([x.email for x in self.timeline if x.message == 'Email Sent']) 228 | self.stats['email_opened_ct'] = len([x.email for x in self.timeline if x.message == 'Email Opened']) 229 | self.stats['clicked_link_ct'] = len([x.email for x in self.timeline if x.message == 'Clicked Link']) 230 | self.stats['submitted_data_ct'] = len([x.email for x in self.timeline if x.message == 'Submitted Data']) 231 | 232 | self.stats['unique_email_sent_ct'] = len(list(set([x.email for x in self.timeline if x.message == 'Email Sent']))) 233 | self.stats['unique_email_opened_ct'] = len(list(set([x.email for x in self.timeline if x.message == 'Email Opened']))) 234 | self.stats['unique_clicked_link_ct'] = len(list(set([x.email for x in self.timeline if x.message == 'Clicked Link']))) 235 | self.stats['unique_submitted_data_ct'] = len(list(set([x.email for x in self.timeline if x.message == 'Submitted Data']))) 236 | 237 | # source IP 238 | for i in range(0, len(self.timeline)): 239 | if self.timeline[i].message in BROWSER_MSG and type(self.timeline[i].details) is dict: 240 | self.timeline[i].source_ip = self.timeline[i].details['browser']['address'] 241 | else: 242 | self.timeline[i].source_ip = None 243 | self.stats['source_ip'] = self._get_timeline_key_count('source_ip') 244 | 245 | def extract_apache_stats(self): 246 | self.stats['apache_malware_dl_ct'] = len(self.apache_malware_dl) 247 | self.stats['apache_source_ip'] = self._get_apache_key_count('remote_host') 248 | 249 | def extract_empire_stats(self): 250 | self.stats['empire_agents_ct'] = len(self.empire_agents) 251 | self.stats['empire_agents_highpriv_ct'] = len([x for x in self.empire_agents if x['high_integrity'] == 1]) 252 | self.stats['empire_agents_unique_usernames_ct'] = len(self._json_get_unique_key(self.empire_agents, 'username')) 253 | self.stats['empire_agents_unique_hostnames_ct'] = len(self._json_get_unique_key(self.empire_agents, 'hostname')) 254 | 255 | self.stats['empire_os'] = self._json_get_count_key(self.empire_agents, 'os_details') 256 | self.stats['empire_source_ip'] = self._json_get_count_key(self.empire_agents, 'external_ip') 257 | 258 | def extract_msf_stats(self): 259 | self.stats['msf_agents_ct'] = 0 260 | self.stats['msf_agents_highpriv_ct'] = 0 261 | self.stats['msf_agents_unique_usernames_ct'] = 0 262 | self.stats['msf_agents_unique_hostnames_ct'] = 0 263 | 264 | self.stats['msf_os'] = [] 265 | self.stats['msf_source_ip'] = [] 266 | 267 | def extract_cobaltstrike_stats(self): 268 | self.stats['cs_agents_ct'] = 0 269 | self.stats['cs_agents_highpriv_ct'] = 0 270 | self.stats['cs_agents_unique_usernames_ct'] = 0 271 | self.stats['cs_agents_unique_hostnames_ct'] = 0 272 | 273 | self.stats['cs_os'] = [] 274 | self.stats['cs_source_ip'] = [] 275 | 276 | def extract_delivered_emails(self): 277 | pass 278 | 279 | def extract_conversion_stats(self): 280 | try: 281 | self.stats['conversion_receive_to_open'] = None 282 | self.stats['conversion_email_to_click'] = None 283 | self.stats['conversion_page_to_creds'] = None 284 | 285 | self.stats['conversion_receive_to_open'] = round(self.stats['unique_email_opened_ct'] / self.stats['unique_email_sent_ct'] * 100, 2) 286 | self.stats['conversion_email_to_click'] = round(self.stats['unique_clicked_link_ct'] / self.stats['unique_email_opened_ct'] * 100, 2) 287 | self.stats['conversion_page_to_creds'] = round(self.stats['unique_submitted_data_ct'] / self.stats['unique_clicked_link_ct'] * 100, 2) 288 | except ZeroDivisionError: 289 | pass 290 | 291 | if self.enable_apache and self.enable_empire: 292 | self.stats['conversion_dl_to_empire_exec'] = round(self.stats['empire_agents_unique_usernames_ct'] / \ 293 | self.stats['apache_malware_dl_ct'] * 100, 2) 294 | else: 295 | self.stats['conversion_dl_to_empire_exec'] = None 296 | 297 | if self.enable_apache and self.enable_msf: 298 | self.stats['conversion_dl_to_msf_exec'] = round(self.stats['msf_agents_unique_usernames_ct'] / \ 299 | self.stats['apache_malware_dl_ct'] * 100, 2) 300 | else: 301 | self.stats['conversion_dl_to_msf_exec'] = None 302 | 303 | if self.enable_apache and self.enable_cobalt: 304 | self.stats['conversion_dl_to_cs_exec'] = round(self.stats['cs_agents_unique_usernames_ct'] / \ 305 | self.stats['apache_malware_dl_ct'] * 100, 2) 306 | else: 307 | self.stats['conversion_dl_to_cs_exec'] = None 308 | 309 | # Extract statistics of each position (often used as a department) 310 | # The objective is to extract stats based on departments. 311 | def extract_position_stats(self): 312 | self.stats['position'] = {} 313 | position_list = self._get_results_unique_key('position') 314 | 315 | for pos in position_list: 316 | position_results = [obj for obj in self.results if obj.position == pos] 317 | pos_total = len(position_results) 318 | pos_scheduled = len([obj for obj in position_results if obj.status == 'Scheduled']) 319 | pos_email_sent = len([obj for obj in position_results if obj.status == 'Email Sent']) 320 | pos_email_open = len([obj for obj in position_results if obj.status == 'Email Opened']) 321 | pos_link_clicked = len([obj for obj in position_results if obj.status == 'Clicked Link']) 322 | pos_submitted_data = len([obj for obj in position_results if obj.status == 'Submitted Data']) 323 | 324 | self.stats['position'][pos] = {'total': pos_total, \ 325 | 'scheduled': pos_scheduled, \ 326 | 'email_sent': pos_email_sent, \ 327 | 'email_open': pos_email_open, \ 328 | 'link_clicked': pos_link_clicked, \ 329 | 'submitted_data': pos_submitted_data} 330 | 331 | def extract_stats_percentage(self): 332 | self.stats['stats_email_opened'] = round(self.stats['unique_email_opened_ct'] / \ 333 | self.sendgrid_stats['delivered'] * 100, 2) 334 | self.stats['stats_clicked_link'] = round(self.stats['unique_clicked_link_ct'] / \ 335 | self.sendgrid_stats['delivered'] * 100, 2) 336 | self.stats['stats_submitted_data'] = round(self.stats['unique_submitted_data_ct'] / \ 337 | self.sendgrid_stats['delivered'] * 100, 2) 338 | 339 | def print_position_stats(self): 340 | title = ['Position', 'Scheduled', 'Email Sent', 'Email Open', \ 341 | 'Link Clicked', 'Submitted Data', 'Total'] 342 | x = PrettyTable(title) 343 | x.align['Position'] = 'l' 344 | x.align['Scheduled'] = 'c' 345 | x.align['Email Sent'] = 'c' 346 | x.align['Email Open'] = 'c' 347 | x.align['Link Clicked'] = 'c' 348 | x.align['Submitted Data'] = 'c' 349 | x.align['Total'] = 'c' 350 | x.padding_width = 1 351 | x.max_width = 40 352 | 353 | position_list = self._get_results_unique_key('position') 354 | for pos in position_list: 355 | row = self.stats['position'][pos] 356 | x.add_row([ pos, row['scheduled'], row['email_sent'], \ 357 | row['email_open'], row['link_clicked'], \ 358 | row['submitted_data'], row['total'] ]) 359 | print(x.get_string()) 360 | 361 | def generate(self): 362 | logger.info("Generating report.") 363 | 364 | logger.info("Setting up folders") 365 | self._setup_out_folder() 366 | 367 | if self.enable_apache: 368 | logger.info("Downloading apache logs") 369 | self.get_apache_logs() 370 | 371 | if self.enable_sendgrid: 372 | logger.info("Getting Sendgrid Stats") 373 | self.get_sendgrid_stats() 374 | 375 | if self.enable_empire: 376 | logger.info("Getting Empire Agents") 377 | self.get_empire_agents() 378 | 379 | logger.info("Extracting stats") 380 | self.extract_gophish_stats() 381 | if self.enable_apache: self.extract_apache_stats() 382 | if self.enable_empire: self.extract_empire_stats() 383 | if self.enable_msf: self.extract_msf_stats() 384 | if self.enable_cobalt: self.extract_cobaltstrike_stats() 385 | self.extract_conversion_stats() 386 | self.extract_position_stats() 387 | if self.enable_sendgrid: self.extract_stats_percentage() 388 | 389 | logger.info("Printing Report") 390 | print("Raw Data: ") 391 | print("") 392 | print(" Timeline: ") 393 | print(" First Event: %s" % self.stats['first_event_ts']) 394 | print(" Last Event: %s" % self.stats['last_event_ts']) 395 | print(" Email sent: %s" % self.stats['email_sent_ct']) 396 | print(" Email opened: %s" % self.stats['email_opened_ct']) 397 | print(" Clicked Link: %s" % self.stats['clicked_link_ct']) 398 | print(" Submitted Data: %s" % self.stats['submitted_data_ct']) 399 | print(" Unique Email sent: %s" % self.stats['unique_email_sent_ct']) 400 | print(" Unique Email opened: %s" % self.stats['unique_email_opened_ct']) 401 | print(" Unique Clicked Link: %s" % self.stats['unique_clicked_link_ct']) 402 | print(" Unique Submitted Data: %s" % self.stats['unique_submitted_data_ct']) 403 | print(" Source IPs: ") 404 | for key, count in self.stats['source_ip'].items(): 405 | print(" %s (%s)" % (key,count)) 406 | print("") 407 | 408 | print(" Position stats:") 409 | self.print_position_stats() 410 | print("") 411 | 412 | if self.enable_sendgrid: 413 | print(" Sendgrid stats:") 414 | print(" Blocks: %s" % self.sendgrid_stats['blocks']) 415 | print(" Bounce Drops: %s" % self.sendgrid_stats['bounce_drops']) 416 | print(" Bounces: %s" % self.sendgrid_stats['bounces']) 417 | print(" Clicks: %s" % self.sendgrid_stats['clicks']) 418 | print(" Deffered: %s" % self.sendgrid_stats['deferred']) 419 | print(" Delivered: %s" % self.sendgrid_stats['delivered']) 420 | print(" Invalid Emails: %s" % self.sendgrid_stats['invalid_emails']) 421 | print(" Open: %s" % self.sendgrid_stats['opens']) 422 | print(" Processed: %s" % self.sendgrid_stats['processed']) 423 | print(" Requests: %s" % self.sendgrid_stats['requests']) 424 | print(" Spam Report Drops: %s" % self.sendgrid_stats['spam_report_drops']) 425 | print(" Spam Reports: %s" % self.sendgrid_stats['spam_reports']) 426 | print(" Unique Clicks: %s" % self.sendgrid_stats['unique_clicks']) 427 | print(" Unique Opens: %s" % self.sendgrid_stats['unique_opens']) 428 | print(" Subscribe Drops: %s" % self.sendgrid_stats['unsubscribe_drops']) 429 | print(" Unsubscribes: %s" % self.sendgrid_stats['unsubscribes']) 430 | print("") 431 | 432 | if self.enable_apache: 433 | print(" Apache: ") 434 | print(" Malware Download: %s" % self.stats['apache_malware_dl_ct']) 435 | print(" Source IPs: ") 436 | for key, count in self.stats['apache_source_ip'].items(): 437 | print(" %s (%s)" % (key,count)) 438 | print("") 439 | 440 | if self.enable_empire and self.stats['empire_agents_ct'] > 0: 441 | print(" Empire: ") 442 | print(" Agents count: %s" % self.stats['empire_agents_ct']) 443 | print(" Agents HighPriv count: %s" % self.stats['empire_agents_highpriv_ct']) 444 | print(" Unique Agents username count: %s" % self.stats['empire_agents_unique_usernames_ct']) 445 | print(" Unique Agents Hostnames count: %s" % self.stats['empire_agents_unique_hostnames_ct']) 446 | print(" OS Details: ") 447 | for key, count in self.stats['empire_os'].items(): 448 | print(" %s (%s)" % (key,count)) 449 | print(" Source IPs: ") 450 | for key, count in self.stats['empire_source_ip'].items(): 451 | print(" %s (%s)" % (key,count)) 452 | print("") 453 | 454 | if self.enable_msf and self.stats['msf_agents_ct'] > 0: 455 | print(" Metasploit: ") 456 | print(" Agents count: %s" % self.stats['msf_agents_ct']) 457 | print(" Agents HighPriv count: %s" % self.stats['msf_agents_highpriv_ct']) 458 | print(" Unique Agents username count: %s" % self.stats['msf_agents_unique_usernames_ct']) 459 | print(" Unique Agents Hostnames count: %s" % self.stats['msf_agents_unique_hostnames_ct']) 460 | print(" OS Details: ") 461 | for key, count in self.stats['msf_os'].items(): 462 | print(" %s (%s)" % (key,count)) 463 | print(" Source IPs: ") 464 | for key, count in self.stats['msf_source_ip'].items(): 465 | print(" %s (%s)" % (key,count)) 466 | print("") 467 | 468 | if self.enable_cobalt and self.stats['cs_agents_ct'] > 0: 469 | print(" Cobalt Strike: ") 470 | print(" Agents count: %s" % self.stats['cs_agents_ct']) 471 | print(" Agents HighPriv count: %s" % self.stats['cs_agents_highpriv_ct']) 472 | print(" Unique Agents username count: %s" % self.stats['cs_agents_unique_usernames_ct']) 473 | print(" Unique Agents Hostnames count: %s" % self.stats['cs_agents_unique_hostnames_ct']) 474 | print(" OS Details: ") 475 | for key, count in self.stats['cs_os'].items(): 476 | print(" %s (%s)" % (key,count)) 477 | print(" Source IPs: ") 478 | for key, count in self.stats['cs_source_ip'].items(): 479 | print(" %s (%s)" % (key,count)) 480 | print("") 481 | 482 | print("Analyzed Data: ") 483 | print("") 484 | print(" Conversion Percentage:") 485 | print(" Email Received (%s) -> Email Opened (%s): %s" % (self.stats['unique_email_sent_ct'], 486 | self.stats['unique_email_opened_ct'], 487 | self.stats['conversion_receive_to_open'])) 488 | print(" Email Open (%s) -> Link Clicked (%s): %s" % (self.stats['unique_email_opened_ct'], 489 | self.stats['unique_clicked_link_ct'], 490 | self.stats['conversion_email_to_click'])) 491 | print(" Page Visit (%s) -> Send Credentials (%s): %s" % (self.stats['unique_clicked_link_ct'], 492 | self.stats['unique_submitted_data_ct'], 493 | self.stats['conversion_page_to_creds'])) 494 | print(" Malware Download (%s) -> Malware Execution (%s) (Empire): %s" % (self.stats.get('apache_malware_dl_ct', None), 495 | self.stats.get('empire_agents_unique_usernames_ct', None), 496 | self.stats.get('conversion_dl_to_empire_exec', None))) 497 | print(" Malware Download (%s) -> Malware Execution (%s) (Msf): %s" % (self.stats.get('apache_malware_dl_ct', None), 498 | self.stats.get('msf_agents_unique_usernames_ct', None), 499 | self.stats.get('conversion_dl_to_msf_exec', None))) 500 | print(" Malware Download (%s) -> Malware Execution (%s) (Cobalt): %s" % (self.stats.get('apache_malware_dl_ct', None), 501 | self.stats.get('cs_agents_unique_usernames_ct', None), 502 | self.stats.get('conversion_dl_to_cs_exec', None))) 503 | print("") 504 | if self.enable_sendgrid: 505 | print(" Events statistics (based on the number of delivered email from Sendgrid): ") 506 | print(" Unique Email Opened (gophish unique opened (%s) / sendgrid delivered (%s) * 100): %s" % (self.stats.get('unique_email_opened_ct'), 507 | self.sendgrid_stats['delivered'], 508 | self.stats.get('stats_email_opened', None))) 509 | print(" Unique Clicked Link (gophish unique clicked link (%s) / sendgrid delivered (%s) * 100): %s" % (self.stats.get('unique_clicked_link_ct', None), 510 | self.sendgrid_stats['delivered'], 511 | self.stats.get('stats_clicked_link', None))) 512 | print(" Unique Submitted Data (gophish unique submitted data (%s) / sendgrid delivered (%s) * 100): %s" % (self.stats.get('unique_submitted_data_ct', None), 513 | self.sendgrid_stats['delivered'], 514 | self.stats.get('stats_submitted_data', None))) 515 | print("") 516 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | prettytable 2 | paramiko 3 | sendgrid 4 | apache_log_parser 5 | colorlog 6 | pytz 7 | -------------------------------------------------------------------------------- /tests/test-juniper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Script that test juniper authentication. 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2017, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | 34 | 35 | import sys 36 | sys.path.append('../') # I'm sure there's a better approach for that... 37 | 38 | from modules.creds import Credentials 39 | from modules.juniper import JuniperCredsTester 40 | 41 | server = 'vpn.example.com' 42 | domain = 'DOMAIN' 43 | uri = '/dana-na/auth/url/login.cgi' 44 | realm = 'meh' 45 | 46 | creds = [Credentials(username='user1', password='Password1'), # good 47 | Credentials(username='user2', password='failure')] # bad 48 | 49 | jct = JuniperCredsTester(creds, server, uri, domain, realm, verify_tls=False, debug=False) 50 | jct.test_logins() 51 | -------------------------------------------------------------------------------- /tests/test-netscaler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Script that test netscaler authentication. 6 | 7 | @author: Martin Dubé 8 | @organization: Gosecure inc. 9 | @license: MIT License 10 | @contact: mdube@gosecure.ca 11 | 12 | Copyright (c) 2017, Gosecure 13 | All rights reserved. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | ''' 33 | 34 | 35 | import sys 36 | sys.path.append('../') # I'm sure there's a better approach for that... 37 | 38 | from modules.creds import Credentials 39 | from modules.netscaler import NetscalerCredsTester 40 | 41 | server = 'vpn.example.com' 42 | creds = [Credentials(username='user1', password='January2017'), # good 43 | Credentials(username='user2', password='failure')] # bad 44 | 45 | nct = NetscalerCredsTester(creds, server) 46 | nct.test_logins() 47 | --------------------------------------------------------------------------------