├── .gitignore ├── client ├── totp_enrollment │ ├── user_query.graphql │ ├── totp_enrollment.ui │ └── __init__.py ├── phishery_docx │ ├── __main__.py │ ├── README.md │ └── __init__.py ├── kpm_export_on_exit.py ├── request_redirect │ ├── schema.json │ ├── README.md │ ├── request_redirect.ui │ └── __init__.py ├── spell_check.py ├── gtube_header.py ├── sftp_client │ ├── sftp_utilities.py │ ├── __init__.py │ ├── editor.py │ └── tasks.py ├── message_plaintext.py ├── domain_check.py ├── clockwork_sms.py ├── file_logging.py ├── pdf_generator │ ├── README.md │ └── __init__.py ├── mime_headers.py ├── hello_world.py ├── office_metadata_remover.py ├── uri_spoof_generator.py ├── sample_set_generator.py ├── blink1.py ├── kpm_export_on_send.py ├── dmarc.py ├── message_padding.py └── campaign_message_configuration.py ├── .github └── stale.yml ├── server ├── hello_world.py ├── alerts_sms_via_email.py ├── alerts_sms_via_clockwork.py ├── slack_notifications.py ├── ifttt_on_campaign_success.py ├── pushbullet_notifications.py ├── alerts_email_via_smtp2go │ ├── __init__.py │ └── template.html ├── postfix_message_info.py ├── xmpp_notifications.py ├── alerts_email_via_smtp │ ├── template.html │ └── __init__.py └── request_redirect.py ├── catalog.json ├── LICENSE ├── README.jnj ├── README.md └── pre-commit /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.ui~ 3 | .venv/* 4 | Pipfile 5 | Pipfile.lock 6 | -------------------------------------------------------------------------------- /client/totp_enrollment/user_query.graphql: -------------------------------------------------------------------------------- 1 | query getUser($name: String!) { 2 | db { 3 | user(name: $name) { 4 | id 5 | name 6 | otpSecret 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 21 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /server/hello_world.py: -------------------------------------------------------------------------------- 1 | import king_phisher.server.plugins as plugins 2 | import king_phisher.server.signals as signals 3 | 4 | EXAMPLE_CONFIG = """\ 5 | # This section should offer some insight into what this plugin expects for inputs 6 | example_var_1: test_1 7 | example_var_2: test_2 8 | """ 9 | 10 | class Plugin(plugins.ServerPlugin): 11 | authors = ['Spencer McIntyre'] 12 | classifiers = ['Plugin :: Server'] 13 | title = 'Hello World!' 14 | description = """ 15 | A 'hello world' plugin to serve as a basic template and demonstration. This 16 | plugin will log simple messages to show that it is functioning. 17 | """ 18 | homepage = 'https://github.com/securestate/king-phisher-plugins' 19 | def initialize(self): 20 | signals.server_initialized.connect(self.on_server_initialized) 21 | self.logger.info('hello-world: the plugin has been initialized') 22 | return True 23 | 24 | def on_server_initialized(self, server): 25 | self.logger.info('hello-world: the server has been initialized') 26 | -------------------------------------------------------------------------------- /client/phishery_docx/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | from . import Plugin, phishery_inject 4 | 5 | PARSER_EPILOG = """\ 6 | If no output file is specified, the input file will be modified in place. 7 | """ 8 | 9 | def main(): 10 | parser = argparse.ArgumentParser( 11 | prog='phishery_docx', 12 | description='Phishery DOCX URL Injector Utility', 13 | conflict_handler='resolve' 14 | ) 15 | parser.add_argument('input_file', help='the input file to inject into') 16 | parser.add_argument('-o', '--output', dest='output_file', help='the output file to write') 17 | parser.add_argument('target_urls', nargs='+', help='the target URL(s) to inject into the input file') 18 | parser.add_argument('-v', '--version', action='version', version='%(prog)s Version: ' + Plugin.version) 19 | parser.epilog = PARSER_EPILOG 20 | arguments = parser.parse_args() 21 | 22 | phishery_inject(arguments.input_file, arguments.target_urls, output_file=arguments.output_file) 23 | 24 | if __name__ == '__main__': 25 | main() 26 | -------------------------------------------------------------------------------- /client/kpm_export_on_exit.py: -------------------------------------------------------------------------------- 1 | import king_phisher.client.plugins as plugins 2 | import king_phisher.client.gui_utilities as gui_utilities 3 | 4 | class Plugin(plugins.ClientPlugin): 5 | authors = ['Spencer McIntyre'] 6 | classifiers = ['Plugin :: Client :: Tool :: Data Management'] 7 | title = 'Save KPM On Exit' 8 | description = 'Prompt to save the message data as a KPM file when King Phisher exits.' 9 | homepage = 'https://github.com/securestate/king-phisher-plugins' 10 | def initialize(self): 11 | if self.application.rpc is None: 12 | self.signal_connect('server-connected', self.signal_server_connected) 13 | else: 14 | self.signal_connect('exit-confirm', self.signal_exit_confirm) 15 | return True 16 | 17 | def signal_exit_confirm(self, app): 18 | if not gui_utilities.show_dialog_yes_no('Save KPM File?', app.get_active_window()): 19 | return 20 | mailer_tab = app.main_window.tabs['mailer'] 21 | mailer_tab.export_message_data() 22 | 23 | def signal_server_connected(self, app): 24 | self.signal_connect('exit-confirm', self.signal_exit_confirm) 25 | -------------------------------------------------------------------------------- /catalog.json: -------------------------------------------------------------------------------- 1 | { 2 | "created": "2019-05-01T15:04:19.092613+00:00", 3 | "created-by": "github.com/zeroSteiner", 4 | "id": "securestate/king-phisher-plugins", 5 | "maintainers": [ 6 | { 7 | "id": "github.com/wolfthefallen" 8 | }, 9 | { 10 | "id": "github.com/zeroSteiner" 11 | } 12 | ], 13 | "repositories": [ 14 | { 15 | "collections-include": [ 16 | { 17 | "path-source": "catalog-collections.json", 18 | "types": [ 19 | "plugins/client", 20 | "plugins/server" 21 | ] 22 | } 23 | ], 24 | "homepage": "https://github.com/securestate/king-phisher-plugins/tree/master", 25 | "id": "master", 26 | "title": "Offical King Phisher Plugins", 27 | "url-base": "https://raw.githubusercontent.com/securestate/king-phisher-plugins/master" 28 | } 29 | ], 30 | "signature": "Aes3uaB96Lm4P+4SC/mkhgcjgTyIdok8AyLMckbw11omB4db54nNJvTLxzxERfLY/C7J4PC7J24xamByRMxyIx2mAVRA2NrvHNIMfKW3L4soEsxsAu8KNOLB5Vi+NLh3ClggvcOZHN2oSZ+CEQe8FMIAL5xWhr+xaETPvxIzAeb82wHV", 31 | "signed-by": "github.com/zeroSteiner" 32 | } -------------------------------------------------------------------------------- /client/request_redirect/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": {}, 4 | "id": "king-phisher.client.plugin.request_redirect.data", 5 | "properties": { 6 | "created": { 7 | "id": "/properties/created", 8 | "type": "string" 9 | }, 10 | "entries": { 11 | "id": "/properties/entries", 12 | "items": { 13 | "id": "/properties/entries/items", 14 | "properties": { 15 | "permanent": { 16 | "id": "/properties/entries/items/properties/permanent", 17 | "type": "boolean" 18 | }, 19 | "rule": { 20 | "id": "/properties/entries/items/properties/rule", 21 | "type": "string" 22 | }, 23 | "source": { 24 | "id": "/properties/entries/items/properties/source", 25 | "type": "string" 26 | }, 27 | "target": { 28 | "id": "/properties/entries/items/properties/target", 29 | "type": "string" 30 | } 31 | }, 32 | "required": [ 33 | "permanent", 34 | "target" 35 | ], 36 | "type": "object" 37 | }, 38 | "type": "array" 39 | } 40 | }, 41 | "required": [ 42 | "created", 43 | "entries" 44 | ], 45 | "type": "object" 46 | } 47 | -------------------------------------------------------------------------------- /client/spell_check.py: -------------------------------------------------------------------------------- 1 | import king_phisher.client.plugins as plugins 2 | 3 | import gi 4 | try: 5 | gi.require_version('GtkSpell', '3.0') 6 | from gi.repository import GtkSpell 7 | except (ImportError, ValueError): 8 | has_gtkspell = False 9 | else: 10 | has_gtkspell = True 11 | 12 | class Plugin(plugins.ClientPlugin): 13 | authors = ['Spencer McIntyre'] 14 | classifiers = ['Plugin :: Client :: Tool'] 15 | title = 'Spell Check' 16 | description = """ 17 | Add spell check capabilities to the message editor. This requires GtkSpell 18 | to be available with the correct Python GObject Introspection bindings. On 19 | Ubuntu and Debian based systems, this is provided by the 20 | 'gir1.2-gtkspell3-3.0' package. 21 | 22 | After being loaded, the language can be changed from the default of en_US 23 | via the context menu (available when right clicking in the text view). 24 | """ 25 | homepage = 'https://github.com/securestate/king-phisher-plugins' 26 | req_packages = { 27 | 'gi.repository.GtkSpell': has_gtkspell 28 | } 29 | version = '1.1' 30 | def initialize(self): 31 | self.checker = GtkSpell.Checker() 32 | self.checker.set_language(self.config.get('language', 'en_US')) 33 | 34 | window = self.application.main_window 35 | mailer_tab = window.tabs['mailer'] 36 | edit_tab = mailer_tab.tabs['edit'] 37 | self.checker.attach(edit_tab.textview) 38 | return True 39 | 40 | def finalize(self): 41 | self.config['language'] = self.checker.get_language() 42 | self.checker.detach() 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2017, SecureState LLC 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of the project nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /client/gtube_header.py: -------------------------------------------------------------------------------- 1 | import king_phisher.client.plugins as plugins 2 | 3 | GTUBE = 'XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X' 4 | WARNING_BANNER = """\ 5 | ************************************************** 6 | * The GTUBE Plugin Is Enabled! * 7 | ************************************************** 8 | """ 9 | 10 | class Plugin(plugins.ClientPlugin): 11 | authors = ['Spencer McIntyre'] 12 | classifiers = ['Plugin :: Client :: Email'] 13 | title = 'GTUBE Header' 14 | description = """ 15 | Add the Generic Test for Unsolicited Bulk Email (GTUBE) string as a X-GTUBE 16 | header and append it to the end of all text/* parts of the MIME messages 17 | that are sent. 18 | 19 | This will cause messages to be identified as SPAM. 20 | """ 21 | homepage = 'https://github.com/securestate/king-phisher-plugins' 22 | reference_urls = ['https://spamassassin.apache.org/gtube/'] 23 | req_min_version = '1.10.0' 24 | version = '1.0' 25 | def initialize(self): 26 | mailer_tab = self.application.main_tabs['mailer'] 27 | self.signal_connect('message-create', self.signal_message_create, gobject=mailer_tab) 28 | self.signal_connect('send-precheck', self.signal_send_precheck, gobject=mailer_tab) 29 | return True 30 | 31 | def signal_message_create(self, mailer_tab, target, message): 32 | message['X-GTUBE'] = GTUBE 33 | for part in message.walk(): 34 | if not part.get_content_type().startswith('text/'): 35 | continue 36 | part.payload_string = part.payload_string + '\n' + GTUBE + '\n' 37 | 38 | def signal_send_precheck(self, mailer_tab): 39 | # we're just going to print the warning banner so the user is aware 40 | mailer_tab.tabs['send_messages'].text_insert(WARNING_BANNER) 41 | return True 42 | -------------------------------------------------------------------------------- /client/phishery_docx/README.md: -------------------------------------------------------------------------------- 1 | ## HTTP URL 2 | The Jinja variable `{{ url.webserver }}` can be used for an HTTP URL to track 3 | when documents are opened. 4 | 5 | Note that to only track opened documents, **do not** put a URL link into the 6 | phishing email to the landing page (`{{ url.webserver }}`). This will ensure 7 | that visits are only registered for instances where the document is opened. 8 | 9 | ## HTTPS URL 10 | The Jinja variable `{{ url.webserver }}` can be used for an HTTPS landing page 11 | that requires basic authentication. 12 | 13 | Note that for HTTPS URLs, the King Phisher server needs to be configured with a 14 | proper, trusted SSL certificate for the user to be presented with the basic 15 | authentication prompt. The [LetsEncrypt project](https://letsencrypt.org/) 16 | project can be used for this purpose. 17 | 18 | ### Setting Up Basic Authentication 19 | The landing page on the King Phisher server must be configured to require 20 | [Basic Authentication][1] in order to prompt for and collect credentials. This 21 | involves creating a special landing page using Jinja to set the 22 | `require_basic_auth` variable to `True`. 23 | 24 | The following is an example of such a landing page. The contents can be copied 25 | into a `login` file and placed in the web root to be used as a landing page. 26 | 27 | ```html 28 | {% set require_basic_auth = True %} 29 | {% set basic_auth_realm = 'Please Authenticate' %} 30 | 31 | 32 | Thanks for authenticating! 33 | 34 | 35 | ``` 36 | 37 | ## FILE URL 38 | Utilizing the `file://yourtargetserver/somepath` URL format will capture SMB 39 | credentials. 40 | 41 | Note that King Phisher does not support SMB, and utilization of SMB requires 42 | that a separate capture/sniffer application such as Metasploit's 43 | `auxiliary/server/capture/smb` module will have to be used to capture NTLM 44 | hashes. The plugin and King Phisher will only support injecting the URL path 45 | into the document. 46 | 47 | [1]: https://github.com/securestate/king-phisher/wiki/Server-Pages-With-Jinja#requiring-basic-authentication -------------------------------------------------------------------------------- /server/alerts_sms_via_email.py: -------------------------------------------------------------------------------- 1 | import king_phisher.sms as sms 2 | import king_phisher.server.plugins as plugins 3 | import king_phisher.server.signals as signals 4 | 5 | class Plugin(plugins.ServerPlugin): 6 | authors = ['Spencer McIntyre'] 7 | classifiers = ['Plugin :: Server :: Notifications :: Alerts'] 8 | title = 'Campaign Alerts: via Carrier SMS Email Gateways' 9 | description = """ 10 | Send campaign alerts as SMS messages through cell carrier's email gateways. 11 | This requires that users supply both their cell phone number and specify a 12 | supported carrier through the King Phisher client. 13 | """ 14 | homepage = 'https://github.com/securestate/king-phisher-plugins' 15 | version = '1.1' 16 | req_min_version = '1.12.0b2' 17 | def initialize(self): 18 | signals.campaign_alert.connect(self.on_campaign_alert) 19 | signals.campaign_alert_expired.connect(self.on_campaign_alert_expired) 20 | return True 21 | 22 | def on_campaign_alert(self, table, alert_subscription, count): 23 | message = "Campaign '{0}' has reached {1:,} {2}".format(alert_subscription.campaign.name, count, table.replace('_', ' ')) 24 | return self.send_alert(alert_subscription, message) 25 | 26 | def on_campaign_alert_expired(self, camapign, alert_subscription): 27 | message = "Campaign '{0}' has expired".format(alert_subscription.campaign.name) 28 | return self.send_alert(alert_subscription, message) 29 | 30 | def send_alert(self, alert_subscription, message): 31 | user = alert_subscription.user 32 | if not user.phone_carrier: 33 | self.logger.debug("user {0} has no cell phone carrier specified, skipping SMS alert".format(user.name)) 34 | return False 35 | if not user.phone_number: 36 | self.logger.debug("user {0} has no cell phone number specified, skipping SMS alert".format(user.name)) 37 | return False 38 | try: 39 | sms.send_sms(message, user.phone_number, user.phone_carrier) 40 | except Exception: 41 | self.logger.error("failed to send the SMS alert to {0} ({1} / {2})".format(user.name, user.phone_number, user.phone_carrier), exc_info=True) 42 | return False 43 | self.logger.debug("sent an SMS alert to user {0} ({1} / {2})".format(user.name, user.phone_number, user.phone_carrier)) 44 | return True 45 | -------------------------------------------------------------------------------- /README.jnj: -------------------------------------------------------------------------------- 1 | ![alt text](https://github.com/securestate/king-phisher/raw/master/data/king-phisher-logo.png "King Phisher") 2 | 3 | # King Phisher Plugins 4 | Plugins to extend the [King Phisher][king-phisher-repo] Phishing Campaign 5 | Toolkit. For more information regarding King Phisher, see the project's 6 | [wiki page][king-phisher-wiki]. 7 | 8 | ## Client Plugins 9 | | Name | Description | 10 | |:------------------------------------------|:------------------| 11 | {% for plugin in plugins.client %} 12 | | [{{ plugin.title }}](/client/{{ plugin.name }}.py) | {{ plugin.description | replace('\n', '
') }} | 13 | {% endfor %} 14 | 15 | ## Server Plugins 16 | | Name | Description | 17 | |:------------------------------------------|:------------------| 18 | {% for plugin in plugins.server %} 19 | | [{{ plugin.title }}](/server/{{ plugin.name }}.py) | {{ plugin.description }} | 20 | {% endfor %} 21 | 22 | ## Plugin Installation 23 | ### Client Plugin Installation 24 | Client plugins can be placed in the `$HOME/.config/king-phisher/plugins` 25 | directory, then loaded and enabled with the plugin manager. 26 | 27 | ### Server Plugin Installation 28 | Server plugins can be placed in the `data/server/king_phisher/plugins` 29 | directory of the King Phisher installation. Additional search paths can be 30 | defined using the `plugin_directories` option in the server's configuration 31 | file. After being copied into the necessary directory, the server's 32 | configuration file needs to be updated to enable the plugin. 33 | 34 | ### Dependency Installation 35 | Some plugins require additional Python packages to be installed in order to 36 | function. These packages must be installed in the King Phisher environment by 37 | running `pipenv install $package` from within the King Phisher installation 38 | directory. 39 | 40 | ## License 41 | King Phisher Plugins are released under the BSD 3-clause license, for more 42 | details see the [LICENSE][license-file] file. 43 | 44 | [king-phisher-repo]: https://github.com/securestate/king-phisher 45 | [king-phisher-wiki]: https://github.com/securestate/king-phisher/wiki 46 | [license-file]: /LICENSE 47 | -------------------------------------------------------------------------------- /server/alerts_sms_via_clockwork.py: -------------------------------------------------------------------------------- 1 | import king_phisher.plugins as plugin_opts 2 | import king_phisher.server.plugins as plugins 3 | import king_phisher.server.signals as signals 4 | 5 | try: 6 | import clockwork 7 | except ImportError: 8 | has_clockwork = False 9 | else: 10 | has_clockwork = True 11 | 12 | EXAMPLE_CONFIG = """\ 13 | api_key: 14 | """ 15 | 16 | class Plugin(plugins.ServerPlugin): 17 | authors = ['Spencer McIntyre'] 18 | classifiers = ['Plugin :: Server :: Notifications :: Alerts'] 19 | title = 'Campaign Alerts: via Clockwork SMS' 20 | description = """ 21 | Send campaign alerts via the Clockwork SMS API. This requires that users 22 | specify their cell phone number through the King Phisher client. 23 | """ 24 | homepage = 'https://github.com/securestate/king-phisher-plugins' 25 | version = '1.1' 26 | options = [ 27 | plugin_opts.OptionString( 28 | name='api_key', 29 | description='Clockwork SMS API Key' 30 | ) 31 | ] 32 | req_packages = { 33 | 'clockwork': has_clockwork 34 | } 35 | req_min_version = '1.12.0b2' 36 | def initialize(self): 37 | signals.campaign_alert.connect(self.on_campaign_alert) 38 | signals.campaign_alert_expired.connect(self.on_campaign_alert_expired) 39 | return True 40 | 41 | def on_campaign_alert(self, table, alert_subscription, count): 42 | message = "Campaign '{0}' has reached {1:,} {2}".format(alert_subscription.campaign.name, count, table.replace('_', ' ')) 43 | return self.send_alert(alert_subscription, message) 44 | 45 | def on_campaign_alert_expired(self, camapign, alert_subscription): 46 | message = "Campaign '{0}' has expired".format(alert_subscription.campaign.name) 47 | return self.send_alert(alert_subscription, message) 48 | 49 | def send_alert(self, alert_subscription, message): 50 | user = alert_subscription.user 51 | if not user.phone_number: 52 | self.logger.debug("user {0} has no cell phone number specified, skipping SMS alert".format(user.name)) 53 | return False 54 | api = clockwork.API(self.config['api_key']) 55 | 56 | response = api.send(clockwork.SMS(user.phone_number, message)) 57 | if not response.success: 58 | self.logger.error("received error {0} ({1})".format(response.error_code, response.error_message)) 59 | return False 60 | self.logger.debug("sent an SMS alert to user {0}".format(user.name)) 61 | return True 62 | -------------------------------------------------------------------------------- /client/sftp_client/sftp_utilities.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | import os 4 | 5 | from king_phisher.client import gui_utilities 6 | 7 | from gi.repository import Gtk 8 | from gi.repository import GLib 9 | from gi.repository import GObject 10 | 11 | logger = logging.getLogger('KingPhisher.Plugins.SFTPClient.utilities') 12 | GTYPE_LONG = GObject.type_from_name('glong') 13 | GTYPE_ULONG = GObject.type_from_name('gulong') 14 | gtk_builder_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'sftp_client.ui') 15 | _gtk_objects = {} 16 | _builder = None 17 | 18 | def get_object(gtk_object): 19 | """ 20 | Used to maintain a diction of GTK objects to share through the SFTP Client 21 | 22 | :param str gtk_object: The name of the GTK Object to fetch 23 | :return: The requested gtk object 24 | """ 25 | global _builder 26 | if not _builder: 27 | _builder = Gtk.Builder() 28 | if not _gtk_objects: 29 | _builder.add_from_file(gtk_builder_file) 30 | if gtk_object in _gtk_objects: 31 | return _gtk_objects[gtk_object] 32 | else: 33 | _gtk_objects[gtk_object] = _builder.get_object(gtk_object) 34 | return _gtk_objects[gtk_object] 35 | 36 | class DelayedChangedSignal(object): 37 | def __init__(self, handler, delay=500): 38 | self._handler = handler 39 | self.delay = delay 40 | self._lock = threading.RLock() 41 | self._event_id = None 42 | 43 | def __call__(self, *args): 44 | return self.changed(*args) 45 | 46 | def _changed(self, args): 47 | with self._lock: 48 | self._handler(*args) 49 | self._event_id = None 50 | return False 51 | 52 | def changed(self, *args): 53 | with self._lock: 54 | if self._event_id is not None: 55 | GLib.source_remove(self._event_id) 56 | self._event_id = GLib.timeout_add(self.delay, self._changed, args) 57 | 58 | def handle_permission_denied(function, *args, **kwargs): 59 | """ 60 | Handles Permissions Denied errors when performing actions on files or folders. 61 | 62 | :param function: A function to be tested for IOErrors and OSErrors. 63 | :return: True if the function did not raise an error and false if it did. 64 | """ 65 | def wrapper(self, *args, **kwargs): 66 | try: 67 | function(self, *args, **kwargs) 68 | except (IOError, OSError) as error: 69 | logger.error('an exception occurred during an operation', exc_info=True) 70 | err_message = "An error occured: {0}".format(error) 71 | gui_utilities.show_dialog_error( 72 | 'Error', 73 | self.application.get_active_window(), 74 | err_message 75 | ) 76 | return False 77 | return True 78 | return wrapper 79 | -------------------------------------------------------------------------------- /client/message_plaintext.py: -------------------------------------------------------------------------------- 1 | import king_phisher.client.plugins as plugins 2 | import king_phisher.client.gui_utilities as gui_utilities 3 | 4 | try: 5 | from bs4 import BeautifulSoup 6 | except ImportError: 7 | has_bs4 = False 8 | else: 9 | has_bs4 = True 10 | 11 | class Plugin(plugins.ClientPlugin): 12 | authors = ['Mike Stringer'] 13 | classifiers = ['Plugin :: Client :: Email :: Spam Evasion'] 14 | title = 'Message Plaintext' 15 | description = """ 16 | Parse and include a plaintext version of an email based on the HTML version. 17 | """ 18 | homepage = 'https://github.com/securestate/king-phisher-plugins' 19 | options = [] 20 | req_min_version = '1.10.0' 21 | version = '1.0' 22 | req_packages = { 23 | 'bs4' : has_bs4 24 | } 25 | def initialize(self): 26 | mailer_tab = self.application.main_tabs['mailer'] 27 | self.signal_connect('message-create', self.signal_message_create, gobject=mailer_tab) 28 | self.signal_connect('send-precheck', self.signal_send_precheck, gobject=mailer_tab) 29 | return True 30 | 31 | def signal_message_create(self, mailer_tab, target, message): 32 | html_part = next((part for part in message.walk() if part.get_content_type().startswith('text/html')), None) 33 | if html_part is None: 34 | self.logger.error('unable to generate plaintext message from HTML (failed to find text/html part)') 35 | return False 36 | text_part = next((part for part in message.walk() if part.get_content_type().startswith('text/plain')), None) 37 | if text_part is None: 38 | self.logger.error('unable to generate plaintext message from HTML (failed to find text/plain part)') 39 | return False 40 | 41 | soup = BeautifulSoup(html_part.payload_string, 'html.parser') 42 | plaintext_payload_string = soup.get_text() 43 | for a in soup.find_all('a', href=True): 44 | if 'mailto:' not in a.string: 45 | plaintext_payload_string = plaintext_payload_string.replace(a.string, a['href']) 46 | text_part.payload_string = plaintext_payload_string 47 | self.logger.debug('plaintext modified from html successfully') 48 | 49 | def signal_send_precheck(self, mailer_tab): 50 | if 'message_padding' not in self.application.plugin_manager.enabled_plugins: 51 | return True 52 | proceed = gui_utilities.show_dialog_yes_no( 53 | 'Warning: You are running a conflicting plugin!', 54 | self.application.get_active_window(), 55 | 'The "message_padding" plugin conflicts with "message_plaintext" in such a way '\ 56 | + 'that will cause the message padding to be revealed in the plaintext version '\ 57 | + 'of the email. It is recommended you disable one of these plugins, or append '\ 58 | + 'additional line breaks in the HTML to conceal it.\n\n' \ 59 | + 'Do you wish to continue?' 60 | ) 61 | return proceed 62 | -------------------------------------------------------------------------------- /client/domain_check.py: -------------------------------------------------------------------------------- 1 | import king_phisher.client.plugins as plugins 2 | import king_phisher.client.gui_utilities as gui_utilities 3 | 4 | import dns.resolver 5 | 6 | try: 7 | import whois 8 | except ImportError: 9 | has_python_whois = False 10 | else: 11 | has_python_whois = True 12 | 13 | def domain_has_mx_record(domain): 14 | try: 15 | dns.resolver.query(domain, 'MX') 16 | except dns.exception.DNSException: 17 | return False 18 | return True 19 | 20 | class Plugin(plugins.ClientPlugin): 21 | authors = ['Jeremy Schoeneman'] 22 | classifiers = ['Plugin :: Client :: Email :: Spam Evasion'] 23 | title = 'Domain Validator' 24 | description = """ 25 | Checks to see if a domain can be resolved and then looks up the WHOIS 26 | information for it. Good for email spoofing and 27 | bypassing some spam filters. 28 | """ 29 | homepage = 'https://github.com/securestate/king-phisher-plugins' 30 | version = '1.0.2' 31 | req_packages = { 32 | 'python-whois': has_python_whois 33 | } 34 | def initialize(self): 35 | mailer_tab = self.application.main_tabs['mailer'] 36 | self.signal_connect('send-precheck', self.signal_precheck, gobject=mailer_tab) 37 | return True 38 | 39 | def signal_precheck(self, mailer_tab): 40 | email = str(self.application.config['mailer.source_email']) 41 | user, _, domain = email.partition('@') 42 | self.logger.debug("checking email domain: {0}".format(domain)) 43 | 44 | if not domain_has_mx_record(domain): 45 | response = gui_utilities.show_dialog_yes_no( 46 | 'Invalid Email Domain', 47 | self.application.get_active_window(), 48 | 'The source email domain does not exist. Continue?' 49 | ) 50 | if not response: 51 | return False 52 | 53 | text_insert = mailer_tab.tabs['send_messages'].text_insert 54 | text_insert("Checking the WHOIS record for domain '{0}'... ".format(domain)) 55 | try: 56 | info = whois.whois(domain) 57 | except Exception as error: 58 | text_insert("done, encountered exception: {0}.\n".format(error.__class__.__name__)) 59 | self.logger.error("whois lookup failed for domain: {0}".format(domain), exc_info=True) 60 | response = gui_utilities.show_dialog_info( 61 | 'Whois Lookup Failed', 62 | self.application.get_active_window(), 63 | 'The domain is valid, however the whois lookup failed. Continue?' 64 | ) 65 | return response 66 | 67 | if any(info.values()): 68 | text_insert('done, record found.\nWHOIS Record Overview:\n') 69 | text_insert(" Domain registered to: {0!r}\n".format(info.name)) 70 | if info.name_servers: 71 | text_insert(" Name Servers: {0}\n".format(', '.join(info.name_servers))) 72 | if info.emails: 73 | text_insert(" Contact Email: {0}\n".format(info.emails)) 74 | else: 75 | text_insert('done, no record found.\n') 76 | return True 77 | -------------------------------------------------------------------------------- /client/clockwork_sms.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import king_phisher.client.plugins as plugins 4 | 5 | import requests 6 | 7 | class Plugin(plugins.ClientPlugin): 8 | authors = ['Spencer McIntyre'] 9 | classifiers = ['Plugin :: Client'] 10 | title = 'Clockwork SMS' 11 | description = """ 12 | Send SMS messages using the Clockwork SMS API's email gateway. While 13 | enabled, this plugin will automatically update phone numbers into email 14 | addresses for sending using the service. 15 | """ 16 | homepage = 'https://github.com/securestate/king-phisher-plugins' 17 | options = [ 18 | plugins.ClientOptionString( 19 | 'api_key', 20 | 'Clockwork API Key', 21 | display_name='Clockwork SMS API Key' 22 | ) 23 | ] 24 | req_min_version = '1.10.0' 25 | version = '1.0.1' 26 | _sms_number_regex = re.compile(r'^[0-9]{10,12}') 27 | def initialize(self): 28 | mailer_tab = self.application.main_tabs['mailer'] 29 | self.signal_connect('send-precheck', self.signal_send_precheck, gobject=mailer_tab) 30 | self.signal_connect('target-create', self.signal_target_create, gobject=mailer_tab) 31 | return True 32 | 33 | def _get_balance(self): 34 | api_key = self.config['api_key'] 35 | try: 36 | resp = requests.get('https://api.clockworksms.com/http/balance?key=' + api_key) 37 | except requests.exceptions.RequestException: 38 | self.logger.warning('failed to check the clockwork sms balance', exc_info=True) 39 | return None 40 | resp = resp.text.strip() 41 | message, details = resp.split(':', 1) 42 | details = details.lstrip() 43 | return message, details 44 | 45 | def signal_send_precheck(self, mailer_tab): 46 | api_key = self.config['api_key'] 47 | text_insert = mailer_tab.tabs['send_messages'].text_insert 48 | if not api_key: 49 | text_insert('Invalid Clockwork SMS API key.\n') 50 | return False 51 | 52 | resp = self._get_balance() 53 | if resp is None: 54 | text_insert('Failed to check the Clockwork SMS API key.\n') 55 | return False 56 | message, details = resp 57 | 58 | if message.lower().startswith('error'): 59 | self.logger.warning('received ' + message + ' (' + details + ') from clockwork api') 60 | text_insert("Received {0}: ({1}) from Clockwork SMS API.\n".format(message, details)) 61 | return False 62 | if message.lower() != 'balance': 63 | self.logger.warning('received unknown response from clockwork api') 64 | text_insert('Received an unknown response from the Clockwork SMS API.\n') 65 | return False 66 | text_insert('Current Clockwork SMS API balance: ' + details + '\n') 67 | return True 68 | 69 | def signal_target_create(self, mailer_tab, target): 70 | if self._sms_number_regex.match(target.email_address) is None: 71 | return 72 | target.email_address = target.email_address + '@' + self.config['api_key'] + '.clockworksms.com' 73 | -------------------------------------------------------------------------------- /client/file_logging.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import os 4 | 5 | import king_phisher.client.dialogs.exception as exception 6 | import king_phisher.client.plugins as plugins 7 | 8 | # logger name value 9 | LOGGER_NAME = '' 10 | 11 | # log file size, in MB 12 | LOG_FILE_SIZE = 10 13 | 14 | class Plugin(plugins.ClientPlugin): 15 | authors = ['Zach Janice', 'Spencer McIntyre'] 16 | classifiers = ['Plugin :: Client :: Tool'] 17 | title = 'File Logging' 18 | description = """ 19 | Write the client's logs to a file in the users data directory. Additionally 20 | if an unhandled exception occurs, the details will be written to a dedicated 21 | directory. 22 | """ 23 | homepage = 'https://github.com/securestate/king-phisher-plugins' 24 | req_min_version = '1.6.0' 25 | version = '2.1' 26 | # this is the primary plugin entry point which is executed when the plugin is enabled 27 | def initialize(self): 28 | # ensure the directory for the logs exists 29 | log_dir = self.application.user_data_path 30 | if not os.path.exists(log_dir): 31 | os.mkdir(log_dir) 32 | 33 | self.exception_dir = os.path.join(log_dir, 'exceptions') 34 | # ensure that the directory for exceptions exists 35 | if not os.path.exists(self.exception_dir): 36 | os.mkdir(self.exception_dir) 37 | 38 | # convert the specified log file size (MB) to bytes for use by the logger 39 | file_size = LOG_FILE_SIZE * 1024 * 1024 40 | 41 | # grab the logger in use by the client (root logger) 42 | logger = logging.getLogger(LOGGER_NAME) 43 | 44 | # set up the handler and formatter for the logger, and attach the components 45 | handler = logging.handlers.RotatingFileHandler(os.path.join(log_dir, 'king-phisher.log'), maxBytes=file_size, backupCount=2) 46 | formatter = logging.Formatter('%(asctime)s %(name)-50s %(levelname)-8s %(message)s') 47 | handler.setFormatter(formatter) 48 | logger.addHandler(handler) 49 | 50 | # keep reference of handler as an attribute 51 | self.handler = handler 52 | self.signal_connect('unhandled-exception', self.signal_kpc_unhandled_exception) 53 | return True 54 | 55 | # this is a cleanup method to allow the plugin to close any open resources 56 | def finalize(self): 57 | # remove the logging handler from the logger and close it 58 | logger = logging.getLogger(LOGGER_NAME) 59 | logger.removeHandler(self.handler) 60 | self.handler.flush() 61 | self.handler.close() 62 | 63 | def signal_kpc_unhandled_exception(self, _, exc_info, error_uid): 64 | exc_type, exc_value, exc_traceback = exc_info 65 | details = exception.format_exception_details(exc_type, exc_value, exc_traceback, error_uid=error_uid) 66 | filename = os.path.join(self.exception_dir, "{0:%Y-%m-%d_%H:%M}_{1}.txt".format(datetime.datetime.now(), error_uid)) 67 | with open(filename, 'w') as file_h: 68 | file_h.write(details) 69 | -------------------------------------------------------------------------------- /server/slack_notifications.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import king_phisher.plugins as plugin_opts 4 | import king_phisher.server.database.manager as db_manager 5 | import king_phisher.server.database.models as db_models 6 | import king_phisher.server.plugins as plugins 7 | import king_phisher.server.signals as signals 8 | import king_phisher.utilities as utilities 9 | 10 | try: 11 | import requests 12 | except ImportError: 13 | has_requests = False 14 | else: 15 | has_requests = True 16 | 17 | EXAMPLE_CONFIG = """\ 18 | # Documentation on obtaining a slack webhook url can be found here https://api.slack.com/messaging/webhooks. 19 | webhookurl: https://hooks.slack.com/services/.... 20 | channel: 21 | """ 22 | 23 | class Plugin(plugins.ServerPlugin): 24 | authors = ['Sebastian Reitenbach'] 25 | classifiers = ['Plugin :: Server :: Notifications'] 26 | title = 'Slack Notifications' 27 | description = """ 28 | A plugin that uses Slack Webhooks to send notifications 29 | on new website visits and submitted credentials to a slack channel. 30 | Notifications about credentials are sent with @here. 31 | """ 32 | homepage = 'https://github.com/securestate/king-phisher-plugins' 33 | options = [ 34 | plugin_opts.OptionString( 35 | name='webhookurl', 36 | description='The slack webhook URL to use' 37 | ), 38 | plugin_opts.OptionString( 39 | name='channel', 40 | description='the channel were notifications are supposed to go to' 41 | ) 42 | ] 43 | req_min_version = '1.4.0' 44 | req_packages = { 45 | 'requests': has_requests 46 | } 47 | version = '0.1' 48 | def initialize(self): 49 | signals.server_initialized.connect(self.on_server_initialized) 50 | return True 51 | 52 | def on_server_initialized(self, server): 53 | signals.db_session_inserted.connect(self.on_kp_db_event, sender='visits') 54 | signals.db_session_inserted.connect(self.on_kp_db_event, sender='credentials') 55 | self.send_notification('King-Phisher Slack notifications are now active') 56 | 57 | def on_kp_db_event(self, sender, targets, session): 58 | for event in targets: 59 | message = db_manager.get_row_by_id(session, db_models.Message, event.message_id) 60 | 61 | if sender == 'visits': 62 | message = "New visit from {0} for campaign '{1}'".format(message.target_email, message.campaign.name) 63 | elif sender == 'credentials': 64 | message = " New credentials received from {0} for campaign '{1}'".format(message.target_email, message.campaign.name) 65 | else: 66 | return 67 | self.send_notification(message) 68 | 69 | def send_notification(self, message): 70 | slack_data = {'text': message, 'channel': self.config['channel']} 71 | response = requests.post( 72 | self.config['webhookurl'], data=json.dumps(slack_data), 73 | headers={'Content-Type': 'application/json'} 74 | ) 75 | -------------------------------------------------------------------------------- /client/pdf_generator/README.md: -------------------------------------------------------------------------------- 1 | This plugin wil take an HTML attachment and turn it into a PDF attachment when 2 | sending the email to a target. 3 | 4 | Inline CSS style is respected during generation of the PDF. You can also specify 5 | multiple `css_stylesheets` in the plugin options. **Note:** Any inline CSS will 6 | have priority over settings in the CSS stylesheet. 7 | 8 | When working on and rendering the HTML file, it is recommend to use the message 9 | editor and preview tabs in the King Phisher client. This will cause any Jinja 10 | tags to be properly rendered. Users may then select the "Create PDF Preview" 11 | option from the Tools menu to verify the PDF format is correct. **Remember to 12 | switch the editor bach over to the email's message template when done.** 13 | 14 | ## HTML 15 | 16 | ### Elements 17 | Many of the HTML elements are implemented in CSS through the default HTML 18 | (User-Agent) stylesheet. Some of these elements will need special treatment and 19 | consideration. 20 | 21 | Special Consideration Elements: 22 | 23 | * The `` element, if present, determines the base for relatives URLs. 24 | * CSS Stylesheets can be embedded in `")' 91 | 92 | return '%' + '%'.join([ "%x" % ord(x) for x in full_url]).strip() 93 | 94 | def expand_path(self, output_file, *args, **kwargs): 95 | expanded_path = _expand_path(output_file, *args, **kwargs) 96 | try: 97 | expanded_path = mailer.render_message_template(expanded_path, self.application.config) 98 | except jinja2.exceptions.TemplateSyntaxError as error: 99 | self.logger.error("jinja2 syntax error ({0}) in directory: {1}".format(error.message, output_file)) 100 | gui_utilities.show_dialog_error('Error', self.application.get_active_window(), 'Error creating the HTML file.') 101 | return None 102 | except ValueError as error: 103 | self.logger.error("value error ({0}) in directory: {1}".format(error, output_file)) 104 | gui_utilities.show_dialog_error('Error', self.application.get_active_window(), 'Error creating the HTML file.') 105 | return None 106 | return expanded_path 107 | 108 | -------------------------------------------------------------------------------- /client/sample_set_generator.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import os 3 | import random 4 | 5 | import king_phisher.client.gui_utilities as gui_utilities 6 | import king_phisher.client.mailer as mailer 7 | import king_phisher.client.plugins as plugins 8 | 9 | def _expand_path(output_file, *joins, pathmod=os.path): 10 | output_file = pathmod.expandvars(output_file) 11 | output_file = pathmod.expanduser(output_file) 12 | output_file.join(output_file, *joins) 13 | return output_file 14 | 15 | class Plugin(plugins.ClientPlugin): 16 | authors = ['Jeremy Schoeneman'] 17 | title = 'Sample Set Generator' 18 | classifiers = ['Plugin :: Client :: Tool'] 19 | description = """ 20 | Brings in a master list and generates a sample set from said list. 21 | """ 22 | homepage = 'https://github.com/securestate/king-phisher-plugins' 23 | options = [ 24 | plugins.ClientOptionPath( 25 | 'master_csv', 26 | 'Master list of targets to sample', 27 | display_name='Master CSV File', 28 | path_type='file-open' 29 | ), 30 | plugins.ClientOptionPath( 31 | 'sample_file', 32 | 'CSV file to write the sample set to', 33 | display_name='Sample CSV File', 34 | default='~/sampled.csv', 35 | path_type='file-save' 36 | ), 37 | plugins.ClientOptionInteger( 38 | 'sample_size', 39 | 'How many targets to sample', 40 | display_name='Sample Size', 41 | default=1 42 | ) 43 | ] 44 | version = '1.0' 45 | def initialize(self): 46 | self.add_menu_item('Tools > Create Sample Set', self.sample_setup) 47 | return True 48 | 49 | def sample_setup(self, _): 50 | self.logger.info('sample_setup reached') 51 | if not self.config['master_csv']: 52 | gui_utilities.show_dialog_error( 53 | 'Missing Option', 54 | self.application.get_active_window(), 55 | 'Please configure the "Master CSV File" option.' 56 | ) 57 | return 58 | if not self.config['sample_file']: 59 | gui_utilities.show_dialog_error( 60 | 'Missing Option', 61 | self.application.get_active_window(), 62 | 'Please configure the "Sample CSV File" option.' 63 | ) 64 | return 65 | if not self.config['sample_size']: 66 | gui_utilities.show_dialog_error( 67 | 'Missing Option', 68 | self.application.get_active_window(), 69 | 'Please configure the "Sample Size" option.' 70 | ) 71 | return 72 | self.logger.info('configuration check passed') 73 | 74 | outfile = self.expand_path(self.config['sample_file']) 75 | 76 | try: 77 | # Reads line locations into memory. Takes less memory than reading the whole file at once 78 | linelocs = collections.deque([0]) 79 | with open(self.config['master_csv'], 'r') as in_file_h, open(outfile, 'w') as out_file_h: 80 | # Reads line locations into memory. Takes less memory than reading the whole file at once 81 | for line in in_file_h: 82 | linelocs.append(linelocs[-1] + len(line)) 83 | 84 | # Pulls the random samples based off the line locations and random 85 | chosen = random.sample(linelocs, self.config['sample_size']) 86 | random.shuffle(chosen) 87 | for offset in chosen: 88 | in_file_h.seek(offset) 89 | out_file_h.write(in_file_h.readline()) 90 | except IOError: 91 | self.logger.error('encountered io error while generating the sample set', exc_info=True) 92 | return 93 | 94 | def expand_path(self, output_file, *args, **kwargs): 95 | expanded_path = _expand_path(output_file, *args, **kwargs) 96 | try: 97 | expanded_path = mailer.render_message_template(expanded_path, self.application.config) 98 | except jinja2.exceptions.TemplateSyntaxError as error: 99 | self.logger.error("jinja2 syntax error ({0}) in directory: {1}".format(error.message, output_file)) 100 | gui_utilities.show_dialog_error('Error', self.application.get_active_window(), 'Error creating the CSV file.') 101 | return None 102 | except ValueError as error: 103 | self.logger.error("value error ({0}) in directory: {1}".format(error, output_file)) 104 | gui_utilities.show_dialog_error('Error', self.application.get_active_window(), 'Error creating the CSV file.') 105 | return None 106 | return expanded_path 107 | 108 | -------------------------------------------------------------------------------- /client/blink1.py: -------------------------------------------------------------------------------- 1 | import king_phisher.client.gui_utilities as gui_utilities 2 | import king_phisher.client.plugins as plugins 3 | import king_phisher.client.server_events as server_events 4 | 5 | from gi.repository import GLib 6 | 7 | try: 8 | from blink1 import blink1 9 | import usb.core 10 | except ImportError: 11 | has_blink1 = False 12 | else: 13 | has_blink1 = True 14 | 15 | COLORS = ('blue', 'cyan', 'green', 'orange', 'pink', 'purple', 'red', 'violet', 'white', 'yellow') 16 | 17 | class Plugin(plugins.ClientPlugin): 18 | authors = ['Spencer McIntyre'] 19 | classifiers = ['Plugin :: Client :: Tool'] 20 | title = 'Blink(1) Notifications' 21 | description = """ 22 | A plugin which will flash a Blink(1) peripheral based on campaign events 23 | such as when a new visit is received or new credentials have been submitted. 24 | """ 25 | homepage = 'https://github.com/securestate/king-phisher-plugins' 26 | options = [ 27 | plugins.ClientOptionBoolean( 28 | 'filter_campaigns', 29 | 'Only show events for the current campaign.', 30 | default=True, 31 | display_name='Current Campaign Only' 32 | ), 33 | plugins.ClientOptionEnum( 34 | 'color_visits', 35 | 'The color to flash the Blink(1) for new visits.', 36 | choices=COLORS, 37 | default='yellow', 38 | display_name='Visits Flash Color' 39 | ), 40 | plugins.ClientOptionEnum( 41 | 'color_credentials', 42 | 'The color to flash the Blink(1) for new credentials.', 43 | choices=COLORS, 44 | default='red', 45 | display_name='Credentials Flash Color' 46 | ), 47 | ] 48 | reference_urls = ['https://blink1.thingm.com/'] 49 | req_min_version = '1.6.0' 50 | req_packages = { 51 | 'blink1': has_blink1 52 | } 53 | req_platforms = ('Linux',) 54 | version = '1.1' 55 | def initialize(self): 56 | self._color = None 57 | try: 58 | self._blink1 = blink1.Blink1() 59 | self._blink1_off() 60 | except usb.core.USBError as error: 61 | gui_utilities.show_dialog_error( 62 | 'Connection Error', 63 | self.application.get_active_window(), 64 | 'Unable to connect to the Blink(1) device.' 65 | ) 66 | return False 67 | except blink1.BlinkConnectionFailed: 68 | gui_utilities.show_dialog_error( 69 | 'Connection Error', 70 | self.application.get_active_window(), 71 | 'Unable to find the Blink(1) device.' 72 | ) 73 | return False 74 | self._gsrc_id = None 75 | if self.application.server_events is None: 76 | self.signal_connect('server-connected', lambda app: self._connect_server_events()) 77 | else: 78 | self._connect_server_events() 79 | return True 80 | 81 | def finalize(self): 82 | self._blink1_off() 83 | self._blink1.close() 84 | self._blink1 = None 85 | 86 | def _blink1_set_color(self, color): 87 | try: 88 | self._blink1.fade_to_color(375, color) 89 | except usb.core.USBError as error: 90 | self.logger.warning("encountered a USB error '{0}' while setting the color of the blink(1)".format(error.strerror)) 91 | return 92 | if color != 'black': 93 | if self._gsrc_id is not None: 94 | GLib.source_remove(self._gsrc_id) 95 | self._gsrc_id = GLib.timeout_add(1625, self._blink1_off) 96 | self._color = color 97 | 98 | def _blink1_off(self): 99 | self._blink1_set_color('black') 100 | self._color = None 101 | 102 | def _blink1_off_timeout(self): 103 | self._gsrc_id = None 104 | self._blink1_off() 105 | return False 106 | 107 | def _connect_server_events(self): 108 | self.signal_connect_server_event( 109 | 'db-credentials', 110 | self.signal_db_credentials, 111 | ('inserted',), 112 | ('id', 'campaign_id') 113 | ) 114 | self.signal_connect_server_event( 115 | 'db-visits', 116 | self.signal_db_visits, 117 | ('inserted',), 118 | ('id', 'campaign_id') 119 | ) 120 | return True 121 | 122 | def _signal_db(self, color, rows): 123 | if self.config['filter_campaigns']: 124 | if all(str(row.campaign_id) != self.application.config['campaign_id'] for row in rows): 125 | return 126 | self._blink1_set_color(color) 127 | 128 | @server_events.event_type_filter('inserted', is_method=True) 129 | def signal_db_credentials(self, _, event_type, rows): 130 | self._signal_db(self.config['color_credentials'], rows) 131 | 132 | @server_events.event_type_filter('inserted', is_method=True) 133 | def signal_db_visits(self, _, event_type, rows): 134 | self._signal_db(self.config['color_visits'], rows) 135 | -------------------------------------------------------------------------------- /client/sftp_client/editor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from . import sftp_utilities 4 | 5 | from king_phisher.client.widget import completion_providers 6 | from king_phisher import utilities 7 | 8 | from gi.repository import GtkSource 9 | from gi.repository import Pango 10 | 11 | logger = logging.getLogger('KingPhisher.Plugins.SFTPClient') 12 | 13 | class SFTPEditor(object): 14 | """ 15 | Handles the editor tab functions 16 | """ 17 | def __init__(self, application, file_path, directory): 18 | """ 19 | This class is used to set up the Gtk.SourceView instance to edit the file 20 | 21 | :param application: The main client application instance. 22 | :type application: :py:class:`Gtk.Application` 23 | :param str file_path: the path of the file to edit 24 | :param directory: the local or remote directory instance 25 | """ 26 | self.application = application 27 | # get editor tab objects 28 | self.file_location = directory.location 29 | self.file_path = file_path 30 | self.file_contents = None 31 | self.directory = directory 32 | 33 | config = self.application.config 34 | self.sourceview_editor = sftp_utilities.get_object('SFTPClient.notebook.page_editor.sourceview') 35 | self.save_button = sftp_utilities.get_object('SFTPClient.notebook.page_editor.toolbutton_save_html_file') 36 | self.template_button = sftp_utilities.get_object('SFTPClient.notebook.page_editor.toolbutton_template_wiki') 37 | self.template_button.connect('clicked', self.signal_template_help) 38 | self.statusbar = sftp_utilities.get_object('SFTPClient.notebook.page_editor.statusbar') 39 | 40 | # set up sourceview for editing 41 | self.sourceview_buffer = GtkSource.Buffer() 42 | self.sourceview_buffer.connect('changed', self.signal_buff_changed) 43 | self.sourceview_editor.set_buffer(self.sourceview_buffer) 44 | self.sourceview_editor.modify_font(Pango.FontDescription(config['text_font'])) 45 | language_manager = GtkSource.LanguageManager() 46 | self.sourceview_buffer.set_language(language_manager.get_language('html')) 47 | self.sourceview_buffer.set_highlight_syntax(True) 48 | self.sourceview_editor.set_property('highlight-current-line', config.get('text_source.highlight_line', True)) 49 | self.sourceview_editor.set_property('indent-width', config.get('text_source.tab_width', 4)) 50 | self.sourceview_editor.set_property('insert-spaces-instead-of-tabs', not config.get('text_source.hardtabs', False)) 51 | self.sourceview_editor.set_property('tab-width', config.get('text_source.tab_width', 4)) 52 | 53 | scheme_manager = GtkSource.StyleSchemeManager() 54 | style_scheme_name = config.get('text_source.theme', 'cobalt') 55 | style_scheme = scheme_manager.get_scheme(style_scheme_name) 56 | if style_scheme: 57 | self.sourceview_buffer.set_style_scheme(style_scheme) 58 | else: 59 | logger.error("invalid GTK source theme: '{0}'".format(style_scheme_name)) 60 | 61 | self.view_completion = self.sourceview_editor.get_completion() 62 | self.view_completion.set_property('accelerators', 0) 63 | self.view_completion.set_property('auto-complete-delay', 250) 64 | self.view_completion.set_property('show-icons', False) 65 | 66 | if not self.view_completion.get_providers(): 67 | self.view_completion.add_provider(completion_providers.HTMLCompletionProvider()) 68 | self.view_completion.add_provider(completion_providers.JinjaPageCompletionProvider()) 69 | logger.info('successfully loaded HTML and Jinja completion providers') 70 | 71 | def signal_buff_changed(self, _): 72 | if self.save_button.is_sensitive(): 73 | return 74 | self.save_button.set_sensitive(True) 75 | 76 | def load_file(self, file_contents): 77 | if not isinstance(file_contents, str): 78 | logger.info("received file_contents type of {} should be utf-8 string".format(type(file_contents))) 79 | file_contents = file_contents.decode('utf-8') 80 | self.sourceview_buffer.begin_not_undoable_action() 81 | self.sourceview_buffer.set_text(file_contents) 82 | self.file_contents = self.sourceview_buffer.get_text( 83 | self.sourceview_buffer.get_start_iter(), 84 | self.sourceview_buffer.get_end_iter(), 85 | False 86 | ) 87 | self.sourceview_buffer.end_not_undoable_action() 88 | self.save_button.set_sensitive(False) 89 | self.statusbar.push(self.statusbar.get_context_id(self.file_location + ' file: ' + self.file_path), self.file_location + ' file: ' + self.file_path) 90 | logger.info("sftp editor set to {} file {}".format(self.file_location, self.file_path)) 91 | 92 | def signal_template_help(self, _): 93 | utilities.open_uri('https://github.com/securestate/king-phisher/wiki/Templates#web-page-templates') 94 | -------------------------------------------------------------------------------- /client/pdf_generator/__init__.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | 4 | import king_phisher.client.gui_utilities as gui_utilities 5 | import king_phisher.client.mailer as mailer 6 | import king_phisher.client.plugins as plugins 7 | import king_phisher.client.widget.extras as extras 8 | 9 | import jinja2.exceptions 10 | 11 | try: 12 | from weasyprint import HTML 13 | except (ImportError, FileNotFoundError): 14 | has_weasyprint = False 15 | else: 16 | has_weasyprint = True 17 | 18 | class Plugin(getattr(plugins, 'ClientPluginMailerAttachment', plugins.ClientPlugin)): 19 | authors = ['Jeremy Schoeneman', 'Erik Daguerre'] 20 | classifiers = ['Plugin :: Client :: Email :: Attachment'] 21 | title = 'Generate PDF' 22 | description = """ 23 | Generates a PDF file from an html attachment that process client King Phisher Jinja variables 24 | allowing to embed links to your landing page so users that click the link in the PDF can be tracked 25 | when they visit. 26 | """ 27 | homepage = 'https://github.com/securestate/king-phisher-plugins' 28 | options = [ 29 | plugins.ClientOptionPath( 30 | 'css_stylesheet', 31 | 'CSS stylesheet to use for HTML to PDF', 32 | display_name='CSS stylesheet', 33 | path_type='file-open' 34 | ) 35 | ] 36 | req_min_version = '1.8.0' 37 | req_packages = { 38 | 'weasyprint==47': has_weasyprint 39 | } 40 | req_platforms = ('Linux',) 41 | version = '2.0' 42 | def initialize(self): 43 | self.add_menu_item('Tools > Create PDF Preview', self.make_preview) 44 | return True 45 | 46 | def make_preview(self, _): 47 | mailer_tab = self.application.main_tabs['mailer'] 48 | config_tab = mailer_tab.tabs['config'] 49 | config_tab.objects_save_to_config() 50 | input_path = self.application.config['mailer.attachment_file'] 51 | if not (os.path.isfile(input_path) and os.access(input_path, os.R_OK)): 52 | gui_utilities.show_dialog_error( 53 | 'PDF Build Error', 54 | self.application.get_active_window(), 55 | 'Attachment path is invalid or is not readable.' 56 | ) 57 | return 58 | 59 | dialog = extras.FileChooserDialog('Save Generated PDF File', self.application.get_active_window()) 60 | response = dialog.run_quick_save('PDF Preview.pdf') 61 | dialog.destroy() 62 | if response is None: 63 | return 64 | 65 | output_path = response['target_path'] 66 | if not self.process_attachment_file(input_path, output_path): 67 | return 68 | gui_utilities.show_dialog_info( 69 | 'PDF Created', 70 | self.application.get_active_window(), 71 | 'Successfully created the PDF file.' 72 | ) 73 | 74 | def process_attachment_file(self, input_path, output_path, target=None): 75 | output_path, _ = os.path.splitext(output_path) 76 | output_path += '.pdf' 77 | try: 78 | with codecs.open(input_path, 'r', encoding='utf-8') as file_: 79 | msg_template = file_.read() 80 | except UnicodeDecodeError as error: 81 | gui_utilities.show_dialog_error( 82 | 'PDF Build Error', 83 | self.application.get_active_window(), 84 | "HTML template not in UTF-8 format.\n\n{error}".format(error=error) 85 | ) 86 | return 87 | 88 | try: 89 | formatted_message = mailer.render_message_template(msg_template, self.application.config, target) 90 | except jinja2.exceptions.TemplateSyntaxError as error: 91 | gui_utilities.show_dialog_error( 92 | 'PDF Build Error', 93 | self.application.get_active_window(), 94 | "Template syntax error: {error.message} on line {error.lineno}.".format(error=error) 95 | ) 96 | return 97 | except jinja2.exceptions.UndefinedError as error: 98 | gui_utilities.show_dialog_error( 99 | 'PDF Build Error', 100 | self.application.get_active_window(), 101 | "Template undefined error: {error.message}.".format(error=error) 102 | ) 103 | return 104 | except TypeError as error: 105 | gui_utilities.show_dialog_error( 106 | 'PDF Build Error', 107 | self.application.get_active_window(), 108 | "Template type error: {0}.".format(error.args[0]) 109 | ) 110 | return 111 | 112 | css_style = self.config.get('css_stylesheet') 113 | if css_style: 114 | css_style = css_style.strip() 115 | if not (os.path.isfile(css_style) and os.access(css_style, os.R_OK)): 116 | self.logger.warning('invalid css file path: ' + css_style) 117 | css_style = None 118 | 119 | weasyprint_html = HTML(string=formatted_message, base_url=os.path.dirname(input_path)) 120 | weasyprint_html.write_pdf( 121 | output_path, 122 | stylesheets=[css_style] if css_style else None, 123 | presentational_hints=True 124 | ) 125 | return output_path 126 | -------------------------------------------------------------------------------- /server/alerts_email_via_smtp2go/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | import king_phisher.plugins as plugin_opts 5 | import king_phisher.templates as templates 6 | import king_phisher.server.plugins as plugins 7 | import king_phisher.server.signals as signals 8 | 9 | try: 10 | from smtp2go import core as smtp2go 11 | except ImportError: 12 | has_smtp2go = False 13 | else: 14 | has_smtp2go = True 15 | 16 | EXAMPLE_CONFIG = """\ 17 | api_key: 18 | server_email: 19 | email_jinja_template: 20 | """ 21 | 22 | class Plugin(plugins.ServerPlugin): 23 | authors = ['Spencer McIntyre', 'Mike Stringer'] 24 | classifiers = ['Plugin :: Server :: Notifications :: Alerts'] 25 | title = 'Campaign Alerts: via SMTP2Go' 26 | description = """ 27 | Send campaign alerts via the SMTP2go lib. This requires that users specify 28 | their email through the King Phisher client to subscribe to notifications. 29 | """ 30 | homepage = 'https://github.com/securestate/king-phisher-plugins' 31 | version = '1.1' 32 | options = [ 33 | plugin_opts.OptionString( 34 | name='api_key', 35 | description='SMTP2GO API Key' 36 | ), 37 | plugin_opts.OptionString( 38 | name='server_email', 39 | description='Server email address to send notifications from' 40 | ), 41 | plugin_opts.OptionString( 42 | name='email_jinja_template', 43 | description='Custom email jinja template to use for alerts', 44 | default='' 45 | ), 46 | ] 47 | req_min_version = '1.12.0b2' 48 | req_packages = { 49 | 'smtp2go': has_smtp2go 50 | } 51 | def initialize(self): 52 | signals.campaign_alert.connect(self.on_campaign_alert) 53 | signals.campaign_alert_expired.connect(self.on_campaign_alert_expired) 54 | template_path = self.config['email_jinja_template'] 55 | if not template_path: 56 | template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'template.html') 57 | if not os.path.isfile(template_path): 58 | self.logger.warning('invalid email template: ' + template_path) 59 | return False 60 | with open(template_path, 'r') as file_: 61 | template_data = file_.read() 62 | self.render_template = templates.TemplateEnvironmentBase().from_string(template_data) 63 | return True 64 | 65 | def on_campaign_alert(self, table, alert_subscription, count): 66 | return self.send_alert(alert_subscription) 67 | 68 | def on_campaign_alert_expired(self, camapign, alert_subscription): 69 | return self.send_alert(alert_subscription) 70 | 71 | def get_template_vars(self, alert_subscription): 72 | campaign = alert_subscription.campaign 73 | template_vars = { 74 | 'campaign': { 75 | 'id': str(campaign.id), 76 | 'name': campaign.name, 77 | 'created': campaign.created, 78 | 'expiration': campaign.expiration, 79 | 'has_expired': campaign.has_expired, 80 | 'message_count': len(campaign.messages), 81 | 'visit_count': len(campaign.visits), 82 | 'credential_count': len(campaign.credentials) 83 | }, 84 | 'time': { 85 | 'local': datetime.datetime.now(), 86 | 'utc': datetime.datetime.utcnow() 87 | } 88 | } 89 | return template_vars 90 | 91 | def send_alert(self, alert_subscription): 92 | user = alert_subscription.user 93 | if not user.email_address: 94 | self.logger.debug("user {0} has no email address specified, skipping SMTP alert".format(user.name)) 95 | return False 96 | 97 | # Workaround for python-smtp2go API, which forces the use of environment variables 98 | # https://github.com/smtp2go-oss/smtp2go-python/pull/1 has been submitted to fix this and should eventually be replaced with... 99 | # api = smtp2go.Smtp2goClient(self.config['api_key'] 100 | os.environ['SMTP2GO_API_KEY'] = self.config['api_key'] 101 | api = smtp2go.Smtp2goClient() 102 | server_email = self.config['server_email'] 103 | 104 | try: 105 | rendered_email = self.render_template.render(self.get_template_vars(alert_subscription)) 106 | except: 107 | self.logger.warning('failed to render the email template', exc_info=True) 108 | return False 109 | payload = { 110 | 'sender': server_email, 111 | 'recipients': [user.email_address], 112 | 'subject': 'Campaign Event: ' + alert_subscription.campaign.name, 113 | 'text': 'This message requires an HTML aware email agent to be properly viewed.\r\n\r\n', 114 | 'html': rendered_email, 115 | 'custom_headers': {} 116 | } 117 | response = api.send(**payload) 118 | 119 | if not response.success: 120 | if response.errors: 121 | self.logger.error(repr([err for err in response.errors])) 122 | return False 123 | self.logger.debug("successfully sent an email campaign alert to user: {0}".format(user.name)) 124 | return True 125 | -------------------------------------------------------------------------------- /server/postfix_message_info.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import re 3 | import os 4 | import time 5 | 6 | import king_phisher.plugins as plugin_opts 7 | import king_phisher.server.database.manager as db_manager 8 | import king_phisher.server.database.models as db_models 9 | import king_phisher.server.fs_utilities as fs_utilities 10 | import king_phisher.server.plugins as plugins 11 | import king_phisher.server.signals as signals 12 | import king_phisher.utilities as utilities 13 | 14 | EXAMPLE_CONFIG = """\ 15 | log_file: /var/log/mail.log 16 | """ 17 | 18 | #Reduce debuging log lines L96:L98 19 | #LOG_LINE_BLACKLIST = ['connect from localhost', 'disconnect from localhost', 'daemon started --'] 20 | 21 | def get_modified_time(path): 22 | return os.stat(path).st_mtime 23 | 24 | class LogInformation(object): 25 | __slots__ = ('message_id', 'statuses', 'message_details') 26 | def __init__(self, message_id): 27 | self.message_id = message_id 28 | self.statuses = collections.deque() 29 | self.message_details = None 30 | 31 | @property 32 | def message_status(self): 33 | if not self.statuses: 34 | return None 35 | return self.statuses[-1] 36 | 37 | class Plugin(plugins.ServerPlugin): 38 | authors = ['Skyler Knecht'] 39 | classifiers = ['Plugin :: Server'] 40 | title = 'Postfix Message Information' 41 | description = """ 42 | A plugin that analyzes message information from the postfix logs to provide 43 | King Phisher clients message status and detail information. 44 | """ 45 | homepage = 'https://github.com/securestate/king-phisher-plugins' 46 | version = '1.0.1' 47 | req_min_version = '1.14.0b1' 48 | options = [ 49 | plugin_opts.OptionString( 50 | name='log_file', 51 | description='Location of the log file to parse through for information.', 52 | default='/var/log/mail.log' 53 | ) 54 | ] 55 | 56 | def initialize(self): 57 | log_file = self.config['log_file'] 58 | setuid_username = self.root_config.get('server.setuid_username') 59 | if setuid_username and not fs_utilities.access(log_file, mode=os.R_OK, user=setuid_username): 60 | self.logger.error('permissions error, invalid access to {}'.format(log_file)) 61 | return False 62 | 63 | signals.server_initialized.connect(self.on_server_initialized) 64 | self.logger.info('{} has been initialized.'.format(self.title)) 65 | return True 66 | 67 | def on_server_initialized(self, server): 68 | self._worker_thread = utilities.Thread(target=self.check_file_change, args=(self.config['log_file'],)) 69 | self._worker_thread.start() 70 | 71 | def finalize(self): 72 | self._worker_thread.stop() 73 | self._worker_thread.join() 74 | 75 | def check_file_change(self, file): 76 | old_modified_time = get_modified_time(file) 77 | old_file_contents = self.get_file_contents(file) 78 | while self._worker_thread.stop_flag.is_clear(): 79 | new_modified_time = get_modified_time(file) 80 | if old_modified_time < new_modified_time: 81 | new_file_contents = self.get_file_contents(file) 82 | self.post_to_database(self.parse_logs(new_file_contents)) 83 | old_modified_time = new_modified_time 84 | time.sleep(5) 85 | 86 | @staticmethod 87 | def get_file_contents(path): 88 | with open(path, 'r') as file_h: 89 | return file_h.readlines() 90 | 91 | def parse_logs(self, log_lines): 92 | results = {} 93 | for line_number, line in enumerate(log_lines, 1): 94 | log_id = re.search(r'postfix/[a-z]+\[\d+\]:\s+(?P[0-9A-Z]{7,12}):\s+', line) 95 | if not log_id: 96 | # check blacklist strings to not spam log files 97 | # if not any(string in line for string in LOG_LINE_BLACKLIST): 98 | # self.logger.warning('failed to parse postfix log line: ' + str(line_number)) 99 | continue 100 | log_id = log_id.group('log_id') 101 | message_id = re.search(r'message-id=<(?P[0-9A-Za-z]{12,20})@', line) 102 | status = re.search(r'status=(?P[a-z]+)\s', line) 103 | details = re.search(r'status=[a-z]+\s\((?P
.+)\)', line) 104 | if log_id not in results and message_id: 105 | results[log_id] = LogInformation(message_id=message_id.group('mid')) 106 | if log_id in results and status: 107 | results[log_id].statuses.append(status.group('status')) 108 | if log_id in results and details: 109 | results[log_id].message_details = details.group('details') 110 | return results 111 | 112 | @staticmethod 113 | def post_to_database(results): 114 | session = db_manager.Session 115 | for log_info in results.values(): 116 | if not log_info.message_status: 117 | continue 118 | message = session.query(db_models.Message).filter_by(id=log_info.message_id).first() 119 | if message: 120 | message.delivery_status = log_info.message_status 121 | message.delivery_details = log_info.message_details 122 | session.add(message) 123 | session.commit() 124 | -------------------------------------------------------------------------------- /client/kpm_export_on_send.py: -------------------------------------------------------------------------------- 1 | import os 2 | import posixpath 3 | import shutil 4 | import tempfile 5 | 6 | import king_phisher.client.mailer as mailer 7 | import king_phisher.client.plugins as plugins 8 | 9 | import jinja2.exceptions 10 | 11 | def _expand_path(path, *joins, pathmod=os.path): 12 | path = pathmod.expandvars(path) 13 | path = pathmod.expanduser(path) 14 | pathmod.join(path, *joins) 15 | return path 16 | 17 | class Plugin(plugins.ClientPlugin): 18 | authors = ['Jeremy Schoeneman'] 19 | classifiers = ['Plugin :: Client :: Tool :: Data Management'] 20 | title = 'Upload KPM' 21 | description = """ 22 | Saves a KPM file to the King Phisher server when sending messages. The user 23 | must have write permissions to the specified directories. Both the "Local 24 | Directory" and "Remote Directory" options can use the variables that are 25 | available for use in message templates. 26 | """ 27 | homepage = 'https://github.com/securestate/king-phisher-plugins' 28 | options = [ 29 | plugins.ClientOptionString( 30 | 'local_directory', 31 | 'Local directory to save the KPM file to', 32 | default='$HOME/{{ campaign.name }}_{{ time.local | strftime(\'%Y-%m-%d_%H:%M\') }}.kpm', 33 | display_name='Local Directory' 34 | ), 35 | plugins.ClientOptionString( 36 | 'remote_directory', 37 | 'Directory on the server to upload the KPM file to', 38 | default='/usr/share/{{ campaign.name }}_{{ time.local | strftime(\'%Y-%m-%d_%H:%M\') }}.kpm', 39 | display_name='Remote Directory' 40 | ) 41 | ] 42 | req_min_version = '1.6.0' 43 | version = '1.2' 44 | def initialize(self): 45 | mailer_tab = self.application.main_tabs['mailer'] 46 | self.text_insert = mailer_tab.tabs['send_messages'].text_insert 47 | self.signal_connect('send-precheck', self.signal_save_kpm, gobject=mailer_tab) 48 | return True 49 | 50 | def signal_save_kpm(self, mailer_tab): 51 | if not any((self.config['local_directory'], self.config['remote_directory'])): 52 | self.logger.debug('skipping exporting kpm archive due to no directories being specified') 53 | return True 54 | fd, temp_kpm_path = tempfile.mkstemp(suffix='.kpm') 55 | os.close(fd) 56 | 57 | self.logger.debug('writing temporary kpm archive file to: ' + temp_kpm_path) 58 | if not mailer_tab.export_message_data(path=temp_kpm_path): 59 | self.logger.error('failed to export the temporary kpm file') 60 | self.text_insert('Failed to export the KPM file\n') 61 | return False 62 | 63 | result = True 64 | if self.config['local_directory'] and not self._save_local_kpm(temp_kpm_path): 65 | result = False 66 | if self.config['remote_directory'] and not self._save_remote_kpm(temp_kpm_path): 67 | result = False 68 | os.remove(temp_kpm_path) 69 | return result 70 | 71 | def _expand_path(self, path, *args, **kwargs): 72 | expanded_path = _expand_path(path, *args, **kwargs) 73 | try: 74 | expanded_path = mailer.render_message_template(expanded_path, self.application.config) 75 | except jinja2.exceptions.TemplateSyntaxError as error: 76 | self.logger.error("jinja2 syntax error ({0}) in directory: {1}".format(error.message, path)) 77 | self.text_insert("Jinja2 syntax error ({0}) in directory: {1}\n".format(error.message, path)) 78 | return None 79 | except ValueError as error: 80 | self.logger.error("value error ({0}) in directory: {1}".format(error, path)) 81 | self.text_insert("Value error ({0}) in directory: {1}\n".format(error, path)) 82 | return None 83 | return expanded_path 84 | 85 | def _save_local_kpm(self, local_kpm): 86 | target_kpm = self._expand_path(self.config['local_directory']) 87 | if target_kpm is None: 88 | return False 89 | 90 | try: 91 | shutil.copyfile(local_kpm, target_kpm) 92 | except Exception: 93 | self.logger.error('failed to save the kpm archive file to: ' + target_kpm) 94 | self.text_insert('Failed to save the KPM archive file to: ' + target_kpm + '\n') 95 | return False 96 | self.logger.info('kpm archive successfully saved to: ' + target_kpm) 97 | return True 98 | 99 | def _save_remote_kpm(self, local_kpm): 100 | target_kpm = self._expand_path(self.config['remote_directory'], pathmod=posixpath) 101 | if target_kpm is None: 102 | return False 103 | 104 | connection = self.application._ssh_forwarder 105 | if connection is None: 106 | self.logger.info('skipping uploading kpm archive due to the absence of an ssh connection') 107 | return True 108 | 109 | sftp = self.application._ssh_forwarder.client.open_sftp() 110 | try: 111 | sftp.put(local_kpm, target_kpm) 112 | except Exception: 113 | self.logger.error('failed to upload the kpm archive file to: ' + target_kpm) 114 | self.text_insert('Failed to upload the KPM archive file to: ' + target_kpm + '\n') 115 | return False 116 | self.logger.info('kpm archive successfully uploaded to: ' + target_kpm) 117 | return True 118 | -------------------------------------------------------------------------------- /client/totp_enrollment/totp_enrollment.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | TOTP Enrollment 8 | center-on-parent 9 | True 10 | 11 | 12 | True 13 | False 14 | 10 15 | 10 16 | 10 17 | 10 18 | 5 19 | 5 20 | 21 | 22 | Check 23 | True 24 | True 25 | True 26 | 27 | 28 | 2 29 | 2 30 | 31 | 32 | 33 | 34 | True 35 | True 36 | 37 | 38 | 1 39 | 2 40 | 41 | 42 | 43 | 44 | True 45 | False 46 | Enter Your TOTP 47 | 48 | 49 | 0 50 | 2 51 | 52 | 53 | 54 | 55 | True 56 | False 57 | Scan the QR code below into your TOTP application. 58 | 59 | 60 | 0 61 | 0 62 | 3 63 | 64 | 65 | 66 | 67 | True 68 | False 69 | True 70 | True 71 | 0 72 | 73 | 74 | True 75 | False 76 | 3 77 | 3 78 | 3 79 | 3 80 | 12 81 | 82 | 83 | True 84 | False 85 | True 86 | True 87 | gtk-missing-image 88 | 89 | 90 | 91 | 92 | 93 | 94 | True 95 | False 96 | New TOTP QR Code 97 | 98 | 99 | 100 | 101 | 0 102 | 1 103 | 3 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /client/phishery_docx/__init__.py: -------------------------------------------------------------------------------- 1 | import distutils.version 2 | import os 3 | import random 4 | import urllib.parse 5 | import zipfile 6 | 7 | import king_phisher.archive as archive 8 | import king_phisher.client.plugins as plugins 9 | import king_phisher.version as version 10 | 11 | min_version = '1.9.0' 12 | StrictVersion = distutils.version.StrictVersion 13 | api_compatible = StrictVersion(version.distutils_version) >= StrictVersion(min_version) 14 | 15 | def path_is_doc_file(path): 16 | if os.path.splitext(path)[1] not in ('.docx', '.docm'): 17 | return False 18 | if not zipfile.is_zipfile(path): 19 | return False 20 | return True 21 | 22 | def phishery_inject(input_file, document_urls, output_file=None): 23 | """ 24 | Inject a word document URL into a DOCX file using the Phisher technique. 25 | 26 | :param str input_file: The path to the input file to process. 27 | :param tuple document_urls: The URLs to inject into the document. 28 | :param str output_file: The output file to write the new document to. 29 | """ 30 | target_string = '' 31 | input_file = os.path.abspath(input_file) 32 | rids = [] 33 | while len(rids) < len(document_urls): 34 | rid = 'rId' + str(random.randint(10000, 99999)) 35 | if rid not in rids: 36 | rids.append(rid) 37 | 38 | settings = '\r\n' 39 | settings += '' 40 | for rid, url in zip(rids, document_urls): 41 | settings += target_string.format(rid=rid, target_url=url) 42 | settings += '' 43 | 44 | patches = {} 45 | patches['word/_rels/settings.xml.rels'] = settings 46 | with zipfile.ZipFile(input_file, 'r') as zin: 47 | settings = zin.read('word/settings.xml') 48 | settings = settings.decode('utf-8') 49 | for rid in rids: 50 | settings = settings.replace('/> 22 | password: 23 | room: notifications@public. 24 | server: : 25 | verify_cert: false 26 | """ 27 | 28 | class NotificationBot(_sleekxmpp_ClientXMPP): 29 | def __init__(self, jid, password, room, verify_cert): 30 | super(NotificationBot, self).__init__(jid, password) 31 | self.add_event_handler('disconnect', self.on_xmpp_disconnect) 32 | self.add_event_handler('session_start', self.on_xmpp_session_start) 33 | self.add_event_handler('ssl_invalid_cert', self.on_xmpp_ssl_invalid_cert) 34 | self.register_plugin('xep_0030') # service discovery 35 | self.register_plugin('xep_0045') # multi-user chat 36 | self.register_plugin('xep_0071') # xhtml im 37 | self.register_plugin('xep_0199') # xmpp ping 38 | self.room = room 39 | self.verify_cert = verify_cert 40 | self.logger = logging.getLogger('KingPhisher.Plugins.XMPPNotificationBot') 41 | 42 | def send_notification(self, message): 43 | ET = sleekxmpp.xmlstream.ET 44 | xhtml = ET.Element('span') 45 | xhtml.set('style', 'font-family: Monospace') 46 | message_lines = message.split('\n') 47 | for line in message_lines[:-1]: 48 | p = ET.SubElement(xhtml, 'p') 49 | p.text = line 50 | ET.SubElement(xhtml, 'br') 51 | p = ET.SubElement(xhtml, 'p') 52 | p.text = message_lines[-1] 53 | self.send_message(mto=self.room, mbody=message, mtype='groupchat', mhtml=xhtml) 54 | 55 | def on_kp_db_new_campaign(self, sender, targets, session): 56 | for campaign in targets: 57 | self.send_notification("new campaign '{0}' created by {1}".format(campaign.name, campaign.user_id)) 58 | 59 | def on_kp_db_new_credentials(self, sender, targets, session): 60 | for credential in targets: 61 | message = db_manager.get_row_by_id(session, db_models.Message, credential.message_id) 62 | self.send_notification("new credentials received from {0} for campaign '{1}'".format(message.target_email, message.campaign.name)) 63 | 64 | def on_kp_db_new_visit(self, sender, targets, session): 65 | for visit in targets: 66 | message = db_manager.get_row_by_id(session, db_models.Message, visit.message_id) 67 | self.send_notification("new visit received from {0} for campaign '{1}'".format(message.target_email, message.campaign.name)) 68 | 69 | def on_xmpp_disconnect(self, _): 70 | signals.db_session_inserted.disconnect(self.on_kp_db_new_campaign, sender='campaigns') 71 | signals.db_session_inserted.disconnect(self.on_kp_db_new_credentials, sender='credentials') 72 | signals.db_session_inserted.disconnect(self.on_kp_db_new_visit, sender='visits') 73 | 74 | def on_xmpp_session_start(self, _): 75 | self.send_presence() 76 | self.get_roster() 77 | 78 | self.plugin['xep_0045'].joinMUC(self.room, self.boundjid.user, wait=True) 79 | 80 | signals.db_session_inserted.connect(self.on_kp_db_new_campaign, sender='campaigns') 81 | signals.db_session_inserted.connect(self.on_kp_db_new_credentials, sender='credentials') 82 | signals.db_session_inserted.connect(self.on_kp_db_new_visit, sender='visits') 83 | self.send_notification('king phisher server notifications are now online') 84 | 85 | def on_xmpp_ssl_invalid_cert(self, pem_cert): 86 | if self.verify_cert: 87 | self.logger.warning('received an invalid ssl certificate, disconnecting from the server') 88 | self.disconnect(send_close=False) 89 | else: 90 | self.logger.warning('received an invalid ssl certificate, ignoring it per the configuration') 91 | return 92 | 93 | class Plugin(plugins.ServerPlugin): 94 | authors = ['Spencer McIntyre'] 95 | classifiers = ['Plugin :: Server :: Notifications'] 96 | title = 'XMPP Notifications' 97 | description = """ 98 | A plugin which pushes notifications regarding the King Phisher server to a 99 | specified XMPP server. 100 | """ 101 | homepage = 'https://github.com/securestate/king-phisher-plugins' 102 | options = [ 103 | plugin_opts.OptionString('jid', 'the username to login with'), 104 | plugin_opts.OptionString('password', 'the password to login with'), 105 | plugin_opts.OptionString('room', 'the room to send notifications to'), 106 | plugin_opts.OptionString('server', 'the server to connect to'), 107 | # verify_cert only functions when sleekxmpp supports it 108 | plugin_opts.OptionBoolean('verify_cert', 'verify the ssl certificate', default=True) 109 | ] 110 | req_min_version = '1.4.0' 111 | req_packages = { 112 | 'sleekxmpp': has_sleekxmpp 113 | } 114 | version = '1.0.1' 115 | def initialize(self): 116 | logger = logging.getLogger('sleekxmpp') 117 | logger.setLevel(logging.INFO) 118 | self.bot = None 119 | signals.server_initialized.connect(self.on_server_initialized) 120 | return True 121 | 122 | def on_server_initialized(self, server): 123 | self.bot = NotificationBot( 124 | self.config['jid'], 125 | self.config['password'], 126 | self.config['room'], 127 | self.config['verify_cert'] 128 | ) 129 | self.bot.connect(utilities.parse_server(self.config['server'], 5222)) 130 | self.bot.process(block=False) 131 | 132 | def finalize(self): 133 | if self.bot is None: 134 | return 135 | self.bot.disconnect() 136 | -------------------------------------------------------------------------------- /server/alerts_email_via_smtp/template.html: -------------------------------------------------------------------------------- 1 | {% macro campaign_row(key, value) %} 2 | 3 | 4 |

5 | 6 | {{ key }} 7 | 8 | 9 | 10 |

11 | 12 | 13 |

14 | {{ value }} 15 |

16 | 17 | 18 | {% endmacro %} 19 | 20 | 21 | 22 |   23 |

24 | 25 | 26 | 95 | 96 |
27 |
28 | 29 | 30 | 91 | 92 |
31 | 32 | 33 | 88 | 89 |
34 | 35 | 36 | 56 | 57 | 58 | 85 | 86 |
37 |
38 | King Phisher Campaign Alert 39 |
40 | 41 | 42 | 53 | 54 |
43 |

44 | 45 | {% if campaign.has_expired %} 46 | Expiration notification for: {{ campaign.name }} 47 | {% else %} 48 | Status update for: {{ campaign.name }} 49 | {% endif %} 50 | 51 |

52 |
55 |
59 | 60 | 61 | 64 | 65 | 66 | 77 | 78 | 79 | 82 | 83 |
62 | Basic Details: 63 |
67 | 68 | {{ campaign_row('Campaign Name', campaign.name) }} 69 | {{ campaign_row('Time Alert Triggered', time.utc | strftime("%Y-%m-%dT%H:%M:%S+00:00")) }} 70 | {{ campaign_row('Number of Visits', campaign.visit_count) }} 71 | {% if campaign.credential_count %} 72 | {{ campaign_row('Number of Credentials', campaign.credential_count) }} 73 | {% endif %} 74 | {{ campaign_row('Campaign Expiration', campaign.expiration) }} 75 |
76 |
80 | Powered by: King Phisher 81 |
84 |
87 |
90 |
93 |
94 |
97 | 98 | 99 | -------------------------------------------------------------------------------- /server/alerts_email_via_smtp2go/template.html: -------------------------------------------------------------------------------- 1 | {% macro campaign_row(key, value) %} 2 | 3 | 4 |

5 | 6 | {{ key }} 7 | 8 | 9 | 10 |

11 | 12 | 13 |

14 | {{ value }} 15 |

16 | 17 | 18 | {% endmacro %} 19 | 20 | 21 | 22 |   23 |

24 | 25 | 26 | 95 | 96 |
27 |
28 | 29 | 30 | 91 | 92 |
31 | 32 | 33 | 88 | 89 |
34 | 35 | 36 | 56 | 57 | 58 | 85 | 86 |
37 |
38 | King Phisher Campaign Alert 39 |
40 | 41 | 42 | 53 | 54 |
43 |

44 | 45 | {% if campaign.has_expired %} 46 | Expiration notification for: {{ campaign.name }} 47 | {% else %} 48 | Status update for: {{ campaign.name }} 49 | {% endif %} 50 | 51 |

52 |
55 |
59 | 60 | 61 | 64 | 65 | 66 | 77 | 78 | 79 | 82 | 83 |
62 | Basic Details: 63 |
67 | 68 | {{ campaign_row('Campaign Name', campaign.name) }} 69 | {{ campaign_row('Time Alert Triggered', time.utc | strftime("%Y-%m-%dT%H:%M:%S+00:00")) }} 70 | {{ campaign_row('Number of Visits', campaign.visit_count) }} 71 | {% if campaign.credential_count %} 72 | {{ campaign_row('Number of Credentials', campaign.credential_count) }} 73 | {% endif %} 74 | {{ campaign_row('Campaign Expiration', campaign.expiration) }} 75 |
76 |
80 | Powered by: King Phisher 81 |
84 |
87 |
90 |
93 |
94 |
97 | 98 | 99 | -------------------------------------------------------------------------------- /client/totp_enrollment/__init__.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import io 3 | import os 4 | 5 | import king_phisher.client.plugins as plugins 6 | import king_phisher.client.gui_utilities as gui_utilities 7 | 8 | from gi.repository import Gtk 9 | from gi.repository import GdkPixbuf 10 | import pyotp 11 | 12 | try: 13 | import qrcode 14 | except ImportError: 15 | has_qrcode = False 16 | else: 17 | has_qrcode = True 18 | 19 | try: 20 | import PIL 21 | except ImportError: 22 | has_pillow = False 23 | else: 24 | has_pillow = True 25 | 26 | relpath = functools.partial(os.path.join, os.path.dirname(os.path.realpath(__file__))) 27 | gtk_builder_file = relpath('totp_enrollment.ui') 28 | user_gql_query = relpath('user_query.graphql') 29 | 30 | class Plugin(plugins.ClientPlugin): 31 | authors = ['Spencer McIntyre'] 32 | classifiers = ['Plugin :: Client :: Tool'] 33 | title = 'TOTP Self Enrollment' 34 | description = """ 35 | This plugin allows users to manage the two factor authentication settings 36 | on their account. This includes setting a new and removing an existing TOTP 37 | secret. The two factor authentication used by King Phisher is compatible 38 | with free mobile applications such as Google Authenticator. 39 | """ 40 | homepage = 'https://github.com/securestate/king-phisher-plugins' 41 | req_min_version = '1.10.0' 42 | req_packages = { 43 | 'qrcode': has_qrcode, 44 | 'pillow': has_pillow 45 | } 46 | version = '1.1.2' 47 | def initialize(self): 48 | if not os.access(gtk_builder_file, os.R_OK): 49 | gui_utilities.show_dialog_error( 50 | 'Plugin Error', 51 | self.application.get_active_window(), 52 | "The GTK Builder data file ({0}) is not available.".format(os.path.basename(gtk_builder_file)) 53 | ) 54 | return False 55 | self.menu_items = {} 56 | self.add_submenu('Tools > TOTP Self Enrollment') 57 | self.menu_items['setup'] = self.add_menu_item('Tools > TOTP Self Enrollment > Setup', self.enrollment_setup) 58 | self.menu_items['remove'] = self.add_menu_item('Tools > TOTP Self Enrollment > Remove', self.enrollment_remove) 59 | return True 60 | 61 | def check_totp(self, _, window, entry, new_otp, this_user): 62 | if not new_otp.verify(entry.get_text().strip()): 63 | gui_utilities.show_dialog_warning( 64 | 'Incorrect TOTP', 65 | self.application.get_active_window(), 66 | 'The specified TOTP code is invalid. Make sure your time\n'\ 67 | + 'is correct, rescan the QR code and try again.' 68 | ) 69 | return 70 | self.application.rpc.remote_table_row_set('users', this_user['id'], {'otp_secret': new_otp.secret}) 71 | gui_utilities.show_dialog_info( 72 | 'TOTP Enrollment', 73 | self.application.get_active_window(), 74 | 'Successfully set the TOTP secret. Your account is now enrolled\n'\ 75 | + 'in two factor authentication. You will be prompted to enter the\n' 76 | + 'value the next time you login.' 77 | ) 78 | window.destroy() 79 | 80 | def enrollment_remove(self, _): 81 | rpc = self.application.rpc 82 | this_user = rpc.graphql_file(user_gql_query, {'name': rpc.username})['db']['user'] 83 | if this_user['otpSecret'] is None: 84 | gui_utilities.show_dialog_info( 85 | 'Not Enrolled', 86 | self.application.get_active_window(), 87 | 'This account is not currently enrolled in two factor\n'\ 88 | + 'authentication. There are no changes to make.' 89 | ) 90 | return 91 | remove = gui_utilities.show_dialog_yes_no( 92 | 'Already Enrolled', 93 | self.application.get_active_window(), 94 | 'Are you sure you want to unenroll in TOTP? This will remove\n'\ 95 | + 'two factor authentication on your account.' 96 | ) 97 | if not remove: 98 | return 99 | rpc.remote_table_row_set('users', this_user['id'], {'otp_secret': None}) 100 | gui_utilities.show_dialog_info( 101 | 'TOTP Unenrollment', 102 | self.application.get_active_window(), 103 | 'Successfully removed the TOTP secret. Your account is now unenrolled\n'\ 104 | + 'in two factor authentication. You will no longer be prompted to enter\n'\ 105 | + 'the value when you login.' 106 | ) 107 | 108 | def enrollment_setup(self, _): 109 | rpc = self.application.rpc 110 | this_user = rpc.graphql_file(user_gql_query, {'name': rpc.username})['db']['user'] 111 | if this_user['otpSecret'] is not None: 112 | reset = gui_utilities.show_dialog_yes_no( 113 | 'Already Enrolled', 114 | self.application.get_active_window(), 115 | 'This account is already enrolled in TOTP,\nreset the existing TOTP token?' 116 | ) 117 | if not reset: 118 | return 119 | new_otp = pyotp.TOTP(pyotp.random_base32()) 120 | provisioning_uri = rpc.username + '@' + self.application.config['server'].split(':', 1)[0] 121 | provisioning_uri = new_otp.provisioning_uri(provisioning_uri) + '&issuer=King%20Phisher' 122 | bytes_io = io.BytesIO() 123 | qrcode_ = qrcode.make(provisioning_uri).get_image() 124 | qrcode_.save(bytes_io, 'PNG') 125 | pixbuf_loader = GdkPixbuf.PixbufLoader.new() 126 | pixbuf_loader.write(bytes_io.getvalue()) 127 | pixbuf_loader.close() 128 | pixbuf = pixbuf_loader.get_pixbuf() 129 | 130 | self.logger.debug('loading gtk builder file from: ' + gtk_builder_file) 131 | builder = Gtk.Builder() 132 | builder.add_from_file(gtk_builder_file) 133 | window = builder.get_object('TOTPEnrollment.window') 134 | window.set_transient_for(self.application.get_active_window()) 135 | 136 | self.application.add_window(window) 137 | 138 | image = builder.get_object('TOTPEnrollment.image_qrcode') 139 | image.set_from_pixbuf(pixbuf) 140 | 141 | button_check = builder.get_object('TOTPEnrollment.button_check') 142 | entry_totp = builder.get_object('TOTPEnrollment.entry_totp') 143 | button_check.connect('clicked', self.check_totp, window, entry_totp, new_otp, this_user) 144 | entry_totp.connect('activate', self.check_totp, window, entry_totp, new_otp, this_user) 145 | 146 | window.show_all() 147 | -------------------------------------------------------------------------------- /server/alerts_email_via_smtp/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import smtplib 4 | import socket 5 | 6 | import king_phisher.plugins as plugin_opts 7 | import king_phisher.server.plugins as plugins 8 | import king_phisher.server.signals as signals 9 | import king_phisher.templates as templates 10 | 11 | from email.mime.multipart import MIMEMultipart 12 | from email.mime.text import MIMEText 13 | 14 | EXAMPLE_CONFIG = """ 15 | smtp_server: 16 | smtp_port: 17 | smtp_email: 18 | smtp_username: 19 | smtp_password: 20 | smtp_ssl: 21 | email_jinja_template: 22 | """ 23 | 24 | class Plugin(plugins.ServerPlugin): 25 | authors = ['Austin DeFrancesco', 'Spencer McIntyre', 'Mike Stringer', 'Erik Daguerre'] 26 | classifiers = ['Plugin :: Server :: Notifications :: Alerts'] 27 | title = 'Campaign Alerts: via Python 3 SMTPLib' 28 | description = """ 29 | Send campaign alerts via the SMTP Python 3 lib. This requires that users specify 30 | their email through the King Phisher client to subscribe to notifications. 31 | """ 32 | homepage = 'https://github.com/securestate/king-phisher-plugins' 33 | version = '1.1' 34 | 35 | # Email accounts with 2FA, such as Gmail, will not work unless "less secure apps" are allowed 36 | # Reference: https://support.google.com/accounts/answer/60610255 37 | # Gmail and other providers require SSL on port 465, TLS will start with the activation of SSL 38 | options = [ 39 | plugin_opts.OptionString( 40 | name='smtp_server', 41 | description='Location of SMTP server', 42 | default='localhost' 43 | ), 44 | plugin_opts.OptionInteger( 45 | name='smtp_port', 46 | description='Port used for SMTP server', 47 | default=25 48 | ), 49 | plugin_opts.OptionString( 50 | name='smtp_email', 51 | description='SMTP email address to send notifications from', 52 | default='' 53 | ), 54 | plugin_opts.OptionString( 55 | name='smtp_username', 56 | description='Username to authenticate to the SMTP server with' 57 | ), 58 | plugin_opts.OptionString( 59 | name='smtp_password', 60 | description='Password to authenticate to the SMTP server with', 61 | default='' 62 | ), 63 | plugin_opts.OptionBoolean( 64 | name='smtp_ssl', 65 | description='Connect to the SMTP server with SSL', 66 | default=False 67 | ), 68 | plugin_opts.OptionString( 69 | name='email_jinja_template', 70 | description='Custom email jinja template to use for alerts', 71 | default='' 72 | ), 73 | ] 74 | req_min_version = '1.12.0b2' 75 | def initialize(self): 76 | signals.campaign_alert.connect(self.on_campaign_alert) 77 | signals.campaign_alert_expired.connect(self.on_campaign_alert_expired) 78 | template_path = self.config['email_jinja_template'] 79 | if not template_path: 80 | template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'template.html') 81 | if not os.path.isfile(template_path): 82 | self.logger.warning('invalid email template: ' + template_path) 83 | return False 84 | with open(template_path, 'r') as file_: 85 | template_data = file_.read() 86 | self.render_template = templates.TemplateEnvironmentBase().from_string(template_data) 87 | return True 88 | 89 | def on_campaign_alert(self, table, alert_subscription, count): 90 | return self.send_alert(alert_subscription) 91 | 92 | def on_campaign_alert_expired(self, camapign, alert_subscription): 93 | return self.send_alert(alert_subscription) 94 | 95 | def get_template_vars(self, alert_subscription): 96 | campaign = alert_subscription.campaign 97 | template_vars = { 98 | 'campaign': { 99 | 'id': str(campaign.id), 100 | 'name': campaign.name, 101 | 'created': campaign.created, 102 | 'expiration': campaign.expiration, 103 | 'has_expired': campaign.has_expired, 104 | 'message_count': len(campaign.messages), 105 | 'visit_count': len(campaign.visits), 106 | 'credential_count': len(campaign.credentials) 107 | }, 108 | 'time': { 109 | 'local': datetime.datetime.now(), 110 | 'utc': datetime.datetime.utcnow() 111 | } 112 | } 113 | return template_vars 114 | 115 | def create_message(self, alert_subscription): 116 | message = MIMEMultipart() 117 | message['Subject'] = "Campaign Event: {0}".format(alert_subscription.campaign.name) 118 | message['From'] = "<{0}>".format(self.config['smtp_email']) 119 | message['To'] = "<{0}>".format(alert_subscription.user.email_address) 120 | 121 | textual_message = MIMEMultipart('alternative') 122 | plaintext_part = MIMEText('This message requires an HTML aware email agent to be properly viewed.\r\n\r\n', 'plain') 123 | textual_message.attach(plaintext_part) 124 | 125 | try: 126 | rendered_email = self.render_template.render(self.get_template_vars(alert_subscription)) 127 | except: 128 | self.logger.warning('failed to render the email template', exc_info=True) 129 | return False 130 | html_part = MIMEText(rendered_email, 'html') 131 | textual_message.attach(html_part) 132 | 133 | message.attach(textual_message) 134 | encoded_email = message.as_string() 135 | return encoded_email 136 | 137 | def send_alert(self, alert_subscription): 138 | user = alert_subscription.user 139 | if not user.email_address: 140 | self.logger.debug("user {0} has no email address specified, skipping SMTP alert".format(user.name)) 141 | return False 142 | 143 | msg = self.create_message(alert_subscription) 144 | if not msg: 145 | return False 146 | 147 | if self.config['smtp_ssl']: 148 | SmtpClass = smtplib.SMTP_SSL 149 | else: 150 | SmtpClass = smtplib.SMTP 151 | try: 152 | server = SmtpClass(self.config['smtp_server'], self.config['smtp_port'], timeout=15) 153 | server.ehlo() 154 | except smtplib.SMTPException: 155 | self.logger.warning('received an SMTPException while connecting to the SMTP server', exc_info=True) 156 | return False 157 | except socket.error: 158 | self.logger.warning('received a socket.error while connecting to the SMTP server') 159 | return False 160 | 161 | if not self.config['smtp_ssl'] and 'starttls' in server.esmtp_features: 162 | self.logger.debug('target SMTP server supports the STARTTLS extension') 163 | try: 164 | server.starttls() 165 | server.ehlo() 166 | except smtplib.SMTPException: 167 | self.logger.warning('received an SMTPException wile negotiating STARTTLS with SMTP server', exc_info=True) 168 | return False 169 | 170 | if self.config['smtp_username']: 171 | try: 172 | server.login(self.config['smtp_username'], self.config['smtp_password']) 173 | except smtplib.SMTPNotSupportedError: 174 | self.logger.debug('SMTP server does not support authentication') 175 | except smtplib.SMTPException as error: 176 | self.logger.warning("received an {0} while authenticating to the SMTP server".format(error.__class__.__name__)) 177 | server.quit() 178 | return False 179 | 180 | mail_options = ['SMTPUTF8'] if server.has_extn('SMTPUTF8') else [] 181 | try: 182 | server.sendmail(self.config['smtp_email'], alert_subscription.user.email_address, msg, mail_options) 183 | except smtplib.SMTPException as error: 184 | self.logger.warning("received error {0} while sending mail".format(error.__class__.__name__)) 185 | return False 186 | finally: 187 | server.quit() 188 | self.logger.debug("successfully sent an email campaign alert to user: {0}".format(user.name)) 189 | return True 190 | -------------------------------------------------------------------------------- /client/dmarc.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import collections 3 | 4 | import king_phisher.color as color 5 | import king_phisher.constants as constants 6 | import king_phisher.spf as spf 7 | import king_phisher.client.mailer as mailer 8 | import king_phisher.client.plugins as plugins 9 | import king_phisher.client.gui_utilities as gui_utilities 10 | 11 | import dns.rdtypes.ANY.TXT 12 | import dns.resolver 13 | 14 | TAGS = { 15 | 'adkim': 'r', 16 | 'aspf': 'r', 17 | 'fo': '0', 18 | 'p': None, 19 | 'pct': '100', 20 | 'rf': 'afrf', 21 | 'ri': '86400', 22 | 'rua': None, 23 | 'ruf': None, 24 | 'sp': None, 25 | 'v': None 26 | } 27 | """ 28 | A dictionary of all tags defined in RFC-7489 with their default value as a 29 | string, or None if no default value is specified. 30 | """ 31 | 32 | class DMARCError(Exception): 33 | def __init__(self, message): 34 | self.message = message 35 | 36 | def __repr__(self): 37 | return "<{0} message='{1}' >".format(self.__class__.__name__, self.message) 38 | 39 | class DMARCNoRecordError(DMARCError): 40 | pass 41 | 42 | class DMARCParseError(DMARCError): 43 | def __init__(self, message, tag=None): 44 | self.message = message 45 | self.tag = tag 46 | 47 | class DMARCPolicy(object): 48 | def __init__(self, record): 49 | record = record.strip() 50 | self.record = record 51 | self.tags = collections.OrderedDict() 52 | record = record.split(';') 53 | # the tag specification is defined in the DKIM spec (RFC 6376) https://tools.ietf.org/html/rfc6376#section-3.2 54 | for token in record: 55 | token = token.strip() 56 | if not token: 57 | continue 58 | if not '=' in token: 59 | raise DMARCParseError('can not separate record token: ' + token) 60 | tag, value = token.split('=', 1) 61 | if tag not in TAGS: 62 | # ignore unknown tags per https://tools.ietf.org/html/rfc7489#section-6.3 63 | continue 64 | self.tags[tag.strip()] = value.strip() 65 | if 'p' in self.tags and self.tags['p'] not in ('none', 'quarantine', 'reject'): 66 | raise DMARCParseError("invalid dmarc record (invalid policy: {0})".format(self.tags['p']), tag='p') 67 | if self.version is None: 68 | raise DMARCParseError('invalid dmarc record (missing version tag)', tag='v') 69 | if self.version != 'DMARC1': 70 | raise DMARCParseError("invalid dmarc record (invalid version value: {0})".format(self.version), tag='v') 71 | 72 | def __repr__(self): 73 | return "<{0} v={1} >".format(self.__class__.__name__, self.version) 74 | 75 | def __str__(self): 76 | return self.record 77 | 78 | @classmethod 79 | def from_domain(cls, domain): 80 | if not domain.startswith('_dmarc.'): 81 | domain = '_dmarc.' + domain 82 | try: 83 | answers = dns.resolver.query(domain, 'TXT') 84 | except dns.exception.DNSException: 85 | raise DMARCNoRecordError("DNS resolution error for: {0} TXT".format(domain)) from None 86 | answers = list(answer for answer in answers if isinstance(answer, dns.rdtypes.ANY.TXT.TXT)) 87 | 88 | answers = [answer for answer in answers if answer.strings[0].decode('utf-8').startswith('v=DMARC')] 89 | if len(answers) == 0: 90 | raise DMARCParseError('failed to parse dmarc record for domain: ' + domain) 91 | record = ''.join([part.decode('utf-8') for part in answers[0].strings]) 92 | return cls(record) 93 | 94 | def get(self, tag): 95 | if not tag in TAGS: 96 | raise KeyError(tag) 97 | return self.tags.get(tag, TAGS[tag]) 98 | 99 | @property 100 | def policy(self): 101 | return self.tags.get('p') 102 | 103 | @property 104 | def version(self): 105 | return self.tags.get('v') 106 | 107 | class Plugin(plugins.ClientPlugin): 108 | authors = ['Spencer McIntyre'] 109 | classifiers = [ 110 | 'Plugin :: Client :: Email :: Spam Evasion', 111 | 'Script :: CLI' 112 | ] 113 | title = 'DMARC Check' 114 | description = """ 115 | This plugin adds another safety check to the message precheck routines to 116 | verify that if DMARC exists the message will not be quarentined or rejected. 117 | If no DMARC policy is present, the policy is set to none or the percentage 118 | is set to 0, the message sending operation will proceed. 119 | """ 120 | homepage = 'https://github.com/securestate/king-phisher-plugins' 121 | reference_urls = ['https://dmarc.org/overview/'] 122 | req_min_version = '1.5.0' 123 | version = '1.2' 124 | def initialize(self): 125 | self.signal_connect('send-precheck', self.signal_send_precheck, gobject=self.application.main_tabs['mailer']) 126 | return True 127 | 128 | def signal_send_precheck(self, mailer_tab): 129 | test_ip = mailer.guess_smtp_server_address( 130 | self.application.config['smtp_server'], 131 | (self.application.config['ssh_server'] if self.application.config['smtp_ssh_enable'] else None) 132 | ) 133 | if not test_ip: 134 | self.logger.info('skipping dmarc policy check because the smtp server address could not be resolved') 135 | return True 136 | test_sender, test_domain = self.application.config['mailer.source_email_smtp'].split('@') 137 | self.logger.debug('checking the dmarc policy for domain: ' + test_domain) 138 | text_insert = mailer_tab.tabs['send_messages'].text_insert 139 | 140 | text_insert("Checking the DMARC policy of target domain '{0}'... ".format(test_domain)) 141 | try: 142 | spf_result = spf.check_host(test_ip, test_domain, sender=test_sender) 143 | except spf.SPFError as error: 144 | text_insert("done, encountered exception: {0}.\n".format(error.__class__.__name__)) 145 | return True 146 | 147 | try: 148 | dmarc_policy = DMARCPolicy.from_domain(test_domain) 149 | except DMARCNoRecordError: 150 | self.logger.debug('no dmarc policy found for domain: ' + test_domain) 151 | text_insert('done, no policy found.\n') 152 | return True 153 | except DMARCError as error: 154 | self.logger.warning('dmarc error: ' + error.message) 155 | text_insert("done, encountered exception: {0}.\n".format(error.__class__.__name__)) 156 | return False 157 | text_insert('done.\n') 158 | self.logger.debug("dmarc policy set to {0!r} for domain: {1}".format(dmarc_policy.policy, test_domain)) 159 | text_insert('Found DMARC policy:\n') 160 | text_insert(' Policy: ' + dmarc_policy.policy + '\n') 161 | text_insert(' Percent: ' + dmarc_policy.get('pct') + '\n') 162 | if dmarc_policy.get('rua'): 163 | text_insert(' RUA URI: ' + dmarc_policy.get('rua') + '\n') 164 | if dmarc_policy.get('ruf'): 165 | text_insert(' RUF URI: ' + dmarc_policy.get('ruf') + '\n') 166 | 167 | if spf_result == constants.SPFResult.PASS: 168 | return True 169 | if dmarc_policy.policy == 'none' or dmarc_policy.get('pct') == '0': 170 | return True 171 | 172 | if dmarc_policy.policy == 'quarantine': 173 | message = 'The DMARC policy results in these messages being quarantined.' 174 | elif dmarc_policy.policy == 'reject': 175 | message = 'The DMARC policy results in these messages being rejected.' 176 | text_insert('WARNING: ' + message + '\n') 177 | ignore = gui_utilities.show_dialog_yes_no( 178 | 'DMARC Policy Failure', 179 | self.application.get_active_window(), 180 | message + '\nContinue sending messages anyways?' 181 | ) 182 | return ignore 183 | 184 | def main(): 185 | parser = argparse.ArgumentParser(description='DMARC Check Utility', conflict_handler='resolve') 186 | parser.add_argument('domain', help='the name of the domain to check') 187 | arguments = parser.parse_args() 188 | 189 | try: 190 | policy = DMARCPolicy.from_domain(arguments.domain) 191 | except DMARCNoRecordError: 192 | color.print_error('dmarc policy not found') 193 | return 194 | 195 | color.print_status('dmarc policy found') 196 | color.print_status('record: ' + policy.record) 197 | color.print_status('version: ' + policy.version) 198 | color.print_status('policy: ' + policy.policy) 199 | 200 | if __name__ == '__main__': 201 | main() 202 | -------------------------------------------------------------------------------- /client/sftp_client/tasks.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import logging 4 | 5 | logger = logging.getLogger('KingPhisher.Plugins.SFTPClient.tasks') 6 | 7 | class TaskQueue(object): 8 | """ 9 | Task queue used for transfer tasks that handles thread and task management 10 | in a way to prevent errors. 11 | """ 12 | def __init__(self): 13 | self.mutex = threading.RLock() 14 | self.not_empty = threading.Condition(self.mutex) 15 | self.not_full = threading.Condition(self.mutex) 16 | self.queue = [] 17 | self.unfinished_tasks = 0 18 | 19 | @property 20 | def queue_ready(self): 21 | for task in self.queue: 22 | if task.is_ready: 23 | yield task 24 | 25 | def _qsize(self, len=len): # pylint: disable=redefined-builtin 26 | return len(list(self.queue)) 27 | 28 | def _qsize_ready(self, len=len): # pylint: disable=redefined-builtin 29 | return len(list(self.queue_ready)) 30 | 31 | def get(self, block=True, timeout=None): 32 | self.not_empty.acquire() 33 | try: 34 | if not block: 35 | if not self._qsize_ready(): 36 | return None 37 | elif timeout is None: 38 | while not self._qsize_ready(): 39 | self.not_empty.wait() 40 | elif timeout < 0: 41 | raise ValueError('\'timeout\' must be a non-negative number') 42 | else: 43 | endtime = time() + timeout # pylint: disable = not-callable 44 | while not self._qsize_ready(): 45 | remaining = endtime - time() # pylint: disable = not-callable 46 | if remaining <= 0.0: 47 | return None 48 | self.not_empty.wait(remaining) 49 | task = next(self.queue_ready) 50 | task.state = 'Active' 51 | self.not_full.notify() 52 | return task 53 | finally: 54 | self.not_empty.release() 55 | 56 | def put(self, task): 57 | """ 58 | Put a task in the queue. 59 | 60 | :param task: A task to be put in the queue. 61 | """ 62 | if not isinstance(task, Task): 63 | raise TypeError('argument 1 task must be Task instance') 64 | with self.not_full: 65 | task.register(self.not_empty) 66 | self.queue.append(task) 67 | self.unfinished_tasks += 1 68 | self.not_empty.notify() 69 | logger.debug('queued task: ' + str(task)) 70 | 71 | def remove(self, task): 72 | """ 73 | Remove a task from the queue. 74 | 75 | :param task: A task to be removed from the queue. 76 | """ 77 | with self.mutex: 78 | self.queue.remove(task) 79 | self.unfinished_tasks += 1 80 | self.not_full.notify() 81 | 82 | class Task(object): 83 | """ 84 | Generic task class that contains information about task state and readiness. 85 | """ 86 | _states = ('Active', 'Cancelled', 'Completed', 'Error', 'Paused', 'Pending') 87 | _ready_states = ('Pending',) 88 | __slots__ = ('_ready', '_state') 89 | def __init__(self, state=None): 90 | self._ready = None 91 | self._state = None 92 | self.state = (state or 'Pending') 93 | 94 | @property 95 | def is_done(self): 96 | return self._state in ('Cancelled', 'Completed', 'Error') 97 | 98 | @property 99 | def is_ready(self): 100 | return self._state in self._ready_states 101 | 102 | @property 103 | def state(self): 104 | return self._state 105 | 106 | @state.setter 107 | def state(self, value): 108 | if value not in self._states: 109 | raise ValueError('invalid state') 110 | self._state = value 111 | if self._state in self._ready_states and self._ready is not None: 112 | self._ready.notify() 113 | 114 | def register(self, ready_event): 115 | if self._ready is not None: 116 | raise RuntimeError('this task has already been registered') 117 | self._ready = ready_event 118 | 119 | class ShutdownTask(Task): 120 | """ 121 | Dummy task used to signal the queue to shutdown. 122 | """ 123 | def __str__(self): 124 | return 'shutdown' 125 | 126 | class TransferTask(Task): 127 | """ 128 | Task used to model transfers. Each task is put in the queue where it will be 129 | pass into the _transfer method of the FileManager class for the transfer to 130 | occur. 131 | """ 132 | _states = ('Active', 'Cancelled', 'Completed', 'Error', 'Paused', 'Pending', 'Transferring') 133 | __slots__ = ('_state', 'local_path', 'remote_path', 'size', 'transferred', 'treerowref', 'parent') 134 | def __init__(self, local_path, remote_path, parent=None, size=None, state=None): 135 | super(TransferTask, self).__init__(state=state) 136 | self.local_path = local_path 137 | """A string representing the local filesystem path of the transfer.""" 138 | self.remote_path = remote_path 139 | """A string representing the remote filesystem path of the transfer.""" 140 | self.transferred = 0 141 | """ 142 | If the task is a file transfer, an integer of the number of bytes transferred, 143 | if the task is a directory transfer, the number of children files transferred. 144 | """ 145 | self.size = size 146 | """ 147 | If the task is a file transfer, an integer of the total number of bytes, 148 | if the task is a directory transfer, the total number of children files. 149 | """ 150 | self.treerowref = None 151 | """A TreeRowReference object representing the Tasks position in the treeview.""" 152 | self.parent = parent 153 | 154 | def __repr__(self): 155 | return "<{0} local_path={1!r} remote_path={2!r} state={3!r}>".format(self.__class__.__name__, self.local_path, self.remote_path, self.state) 156 | 157 | @property 158 | def parents(self): 159 | parents = [] 160 | node = self 161 | while node.parent is not None: 162 | parents.append(node.parent) 163 | node = node.parent 164 | return parents 165 | 166 | @property 167 | def progress(self): 168 | if self.size is None: 169 | percent = 0 170 | elif self.size == 0: 171 | percent = 1 172 | else: 173 | percent = (float(self.transferred) / float(self.size)) 174 | return min(int(percent * 100), 100) 175 | 176 | @property 177 | def state(self): 178 | return Task.state.fget(self) 179 | 180 | @state.setter 181 | def state(self, value): 182 | if value == Task.state.fget(self): 183 | return 184 | Task.state.fset(self, value) 185 | if value in ('Cancelled', 'Completed'): 186 | for parent_task in self.parents: 187 | if value == 'Cancelled': 188 | parent_task.size -= 1 189 | else: 190 | parent_task.transferred += 1 191 | if parent_task.size == parent_task.transferred: 192 | parent_task.state = ('Completed' if parent_task.size else 'Cancelled') 193 | 194 | class DownloadTask(TransferTask): 195 | """ 196 | Subclass of TransferTask that indicates 197 | the task is downloading files. 198 | """ 199 | transfer_direction = 'download' 200 | def __str__(self): 201 | return "download file {0} -> {1}".format(self.remote_path, self.local_path) 202 | 203 | class UploadTask(TransferTask): 204 | """ 205 | Subclass of TransferTask that indicates 206 | the task is uploading files. 207 | """ 208 | transfer_direction = 'upload' 209 | def __str__(self): 210 | return "upload file {0} -> {1}".format(self.local_path, self.remote_path) 211 | 212 | class TransferDirectoryTask(TransferTask): 213 | """ 214 | Task to model a folder transfer. Acts as a parent task 215 | to other TransferTasks and is passed into _transfer_dir. 216 | """ 217 | pass 218 | 219 | class DownloadDirectoryTask(DownloadTask, TransferDirectoryTask): 220 | """ 221 | Subclass of DownloadTask and TransferDirectoryTask that indicates the task 222 | is downloading folders. 223 | """ 224 | def __str__(self): 225 | return "download directory {0} -> {1}".format(self.remote_path, self.local_path) 226 | DownloadTask.dir_cls = DownloadDirectoryTask 227 | 228 | class UploadDirectoryTask(UploadTask, TransferDirectoryTask): 229 | """ 230 | Subclass of UploadTask and TransferDirectoryTask that indicates the task is 231 | uploading folders. 232 | """ 233 | def __str__(self): 234 | return "upload directory {0} -> {1}".format(self.remote_path, self.local_path) 235 | UploadTask.dir_cls = UploadDirectoryTask 236 | -------------------------------------------------------------------------------- /client/message_padding.py: -------------------------------------------------------------------------------- 1 | import king_phisher.client.plugins as plugins 2 | 3 | import os 4 | 5 | PLUGIN_PATH = os.path.realpath(os.path.dirname(__file__)) 6 | STATIC_PADDING = """\ 7 |

It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. \ 8 | The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it \ 9 | look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem \ 10 | ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose. \ 11 | Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 \ 12 | years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a \ 13 | Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections \ 14 | 1.10.32 and 1.10.33 of 'de Finibus Bonorum et Malorum' (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory \ 15 | of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, 'Lorem ipsum dolor sit amet..', comes from a line in section 1.10.32. The \ 16 | standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from 'de Finibus Bonorum et \ 17 | Malorum' by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. The \ 18 | point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it \ 19 | look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem \ 20 | ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose. \ 21 | Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 \ 22 | years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a \ 23 | Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections \ 24 | 1.10.32 and 1.10.33 of 'de Finibus Bonorum et Malorum' (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory \ 25 | of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, 'Lorem ipsum dolor sit amet..', comes from a line in section 1.10.32. The \ 26 | standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from 'de Finibus Bonorum et \ 27 | Malorum' by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. \ 28 | The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it \ 29 | look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem \ 30 | ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose. \ 31 | Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 \ 32 | years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a \ 33 | Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections \ 34 | 1.10.32 and 1.10.33 of 'de Finibus Bonorum et Malorum' (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory \ 35 | of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, 'Lorem ipsum dolor sit amet..', comes from a line in section 1.10.32. The \ 36 | standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from 'de Finibus Bonorum et \ 37 | Malorum' by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.

\ 38 | """ 39 | 40 | try: 41 | import markovify 42 | except ImportError: 43 | has_markovify = False 44 | else: 45 | has_markovify = True 46 | 47 | class Plugin(plugins.ClientPlugin): 48 | authors = ['Spencer McIntyre', 'Mike Stringer'] 49 | classifiers = ['Plugin :: Client :: Email :: Spam Evasion'] 50 | title = 'Message Padding' 51 | description = """ 52 | Add and modify custom HTML messages from a file to reduce Spam Assassin 53 | scores. This plugin interacts with the message content to append a long 54 | series of randomly generated sentences to meet the ideal image-text ratio. 55 | """ 56 | homepage = 'https://github.com/securestate/king-phisher-plugins' 57 | options = [ 58 | plugins.ClientOptionString( 59 | 'corpus', 60 | description='Text file containing text to generate dynamic padding', 61 | default=os.path.join(PLUGIN_PATH, 'corpus.txt'), 62 | display_name='Corpus File' 63 | ), 64 | plugins.ClientOptionBoolean( 65 | 'dynamic_padding', 66 | description='Sets whether dynamically generated or static padding is appended to the messaged', 67 | default=True 68 | ) 69 | ] 70 | req_min_version = '1.10.0' 71 | version = '1.0' 72 | req_packages = { 73 | 'markovify': has_markovify 74 | } 75 | def initialize(self): 76 | mailer_tab = self.application.main_tabs['mailer'] 77 | self.signal_connect('message-create', self.signal_message_create, gobject=mailer_tab) 78 | if os.path.isfile(os.path.realpath(self.config['corpus'])): 79 | self.corpus = os.path.realpath(self.config['corpus']) 80 | else: 81 | self.corpus = None 82 | self.logger.debug('corpus file: ' + repr(self.corpus)) 83 | if self.corpus: 84 | self.dynamic = self.config['dynamic_padding'] 85 | else: 86 | if self.config['dynamic_padding']: 87 | self.logger.warning('the corpus file is unavailable, ignoring the dynamic padding setting') 88 | self.dynamic = False 89 | return True 90 | 91 | def signal_message_create(self, mailer_tab, target, message): 92 | for part in message.walk(): 93 | if not part.get_content_type().startswith('text/html'): 94 | continue 95 | payload_string = part.payload_string 96 | tag = '' 97 | if tag not in payload_string: 98 | self.logger.warning('can not find ' + tag + ' tag to anchor the message padding') 99 | continue 100 | part.payload_string = payload_string.replace(tag, self.make_padding() + tag) 101 | 102 | def make_padding(self): 103 | if self.dynamic: 104 | f = open(self.corpus, 'r') 105 | text = markovify.Text(f) 106 | self.logger.info('generating dynamic padding from corpus') 107 | pad = '

' 108 | for i in range(1, 50): 109 | temp = text.make_sentence() 110 | if temp is not None: 111 | pad += ' ' + temp 112 | if i % 5 == 0: 113 | pad +='
' 114 | else: 115 | pad += '
' 116 | pad += '

' 117 | self.logger.info('dynamic padding generated successfully') 118 | f.close() 119 | else: 120 | self.logger.warning('message created using static padding') 121 | pad = STATIC_PADDING 122 | return pad 123 | -------------------------------------------------------------------------------- /client/campaign_message_configuration.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | import king_phisher.find as find 5 | import king_phisher.serializers as serializers 6 | import king_phisher.client.plugins as plugins 7 | import king_phisher.client.gui_utilities as gui_utilities 8 | 9 | def is_managed_key(key): 10 | """ 11 | Return True for configuration keys which should be managed by this 12 | plugin. This is to let keys for other configuration settings remain the 13 | same. 14 | 15 | :param str key: The name of the configuration key. 16 | :return: Whether or not the key should be managed by this plugin. 17 | :rtype: bool 18 | """ 19 | if key == 'mailer.company_name': 20 | return False 21 | if key.startswith('mailer.'): 22 | return True 23 | if key in ('remove_attachment_metadata', 'spf_check_level'): 24 | return True 25 | return False 26 | 27 | class Plugin(plugins.ClientPlugin): 28 | authors = ['Spencer McIntyre'] 29 | classifiers = ['Plugin :: Client :: Tool :: Data Management'] 30 | title = 'Campaign Message Configuration Manager' 31 | description = """ 32 | Store campaign message configurations for their respective campaigns. This 33 | allows users to switch between campaigns while keeping each of the message 34 | configurations and restoring them when the user returns to the original 35 | campaign. New campaigns can either be created with customizable default 36 | settings or from the existing configuration (see the "Transfer Settings" 37 | option). 38 | """ 39 | homepage = 'https://github.com/securestate/king-phisher-plugins' 40 | options = [ 41 | plugins.ClientOptionBoolean( 42 | 'transfer_options', 43 | 'Whether or not to keep the current settings for new campaigns.', 44 | default=True, 45 | display_name='Transfer Settings' 46 | ) 47 | ] 48 | req_min_version = '1.10.0' 49 | version = '1.0.1' 50 | def initialize(self): 51 | self.signal_connect('campaign-set', self.signal_kpc_campaign_set) 52 | self.storage = self.load_storage() 53 | 54 | # create the submenu for setting and clearing the default config 55 | submenu = 'Tools > Message Configuration' 56 | self.add_submenu(submenu) 57 | self.menu_items = { 58 | 'set_defaults': self.add_menu_item(submenu + ' > Set Defaults', self.menu_item_set_defaults), 59 | 'clear_defaults': self.add_menu_item(submenu + ' > Clear Defaults', self.menu_item_clear_defaults) 60 | } 61 | return True 62 | 63 | def finalize(self): 64 | self.set_campaign_config(self.get_current_config(), self.application.config['campaign_id']) 65 | self.save_storage() 66 | 67 | def menu_item_clear_defaults(self, _): 68 | proceed = gui_utilities.show_dialog_yes_no( 69 | 'Clear Default Campaign Configuration?', 70 | self.application.get_active_window(), 71 | 'Are you sure you want to clear the default\n'\ 72 | + 'message configuration for new campaigns?' 73 | ) 74 | if not proceed: 75 | return 76 | self.storage['default'] = {} 77 | 78 | def menu_item_set_defaults(self, _): 79 | proceed = gui_utilities.show_dialog_yes_no( 80 | 'Set The Default Campaign Configuration?', 81 | self.application.get_active_window(), 82 | 'Are you sure you want to set the default\n'\ 83 | + 'message configuration for new campaigns?' 84 | ) 85 | if not proceed: 86 | return 87 | self.storage['default'] = self.get_current_config() 88 | 89 | @property 90 | def storage_file_path(self): 91 | """The path on disk of where to store the plugin's data.""" 92 | return os.path.join(self.application.user_data_path, 'campaign_message_config.json') 93 | 94 | def load_default_config(self): 95 | """ 96 | Load the default configuration to use when settings are missing. This 97 | will load the user's configured defaults and fail back to the core ones 98 | distributed with the application. 99 | 100 | :return: The default configuration. 101 | :rtype: dict 102 | """ 103 | default_client_config = find.data_file('client_config.json') 104 | with open(default_client_config, 'r') as tmp_file: 105 | default_client_config = serializers.JSON.load(tmp_file) 106 | 107 | users_defaults = self.storage['default'] 108 | for key, value in users_defaults.items(): 109 | if not is_managed_key(key): 110 | continue 111 | if key not in default_client_config: 112 | continue 113 | default_client_config[key] = value 114 | return default_client_config 115 | 116 | def load_storage(self): 117 | """ 118 | Load this plugin's stored data from disk. 119 | 120 | :return: The plugin's stored data. 121 | :rtype: dict 122 | """ 123 | storage = {'campaigns': {}, 'default': {}} 124 | file_path = self.storage_file_path 125 | if os.path.isfile(file_path) and os.access(file_path, os.R_OK): 126 | self.logger.debug('loading campaign messages configuration file: ' + file_path) 127 | with open(file_path, 'r') as file_h: 128 | storage = serializers.JSON.load(file_h) 129 | else: 130 | self.logger.debug('campaigns configuration file not found') 131 | return storage 132 | 133 | def save_storage(self): 134 | """Save this plugin's stored data to disk.""" 135 | file_path = self.storage_file_path 136 | self.logger.debug('writing campaign messages configuration file: ' + file_path) 137 | with open(file_path, 'w') as file_h: 138 | serializers.JSON.dump(self.storage, file_h, pretty=True) 139 | 140 | def get_campaign_config(self, campaign_id=None): 141 | """ 142 | Get the message configuration for a specific campaign. If *campaign_id* 143 | is not specified, then the current campaign is used. If not settings 144 | are available for the specified campaign, an empty dictionary is 145 | returned. 146 | 147 | :param str campaign_id: The ID of the campaign. 148 | :return: The campaign's message configuration or an empty dictionary. 149 | :rtype: dict 150 | """ 151 | if campaign_id is None: 152 | campaign_id = self.application.config['campaign_id'] 153 | campaign_id = str(campaign_id) 154 | if not self.storage.get('campaigns'): 155 | return {} 156 | return self.storage['campaigns'].get(campaign_id, {}).get('configuration') 157 | 158 | def set_campaign_config(self, config, campaign_id=None): 159 | """ 160 | Add the message configuration into the plugin's storage data and 161 | associate it with the specified campaign. If *campaign_id* is not 162 | specified, then the current campaign is used. 163 | 164 | :param dict config: 165 | :param str campaign_id: The ID of the campaign. 166 | """ 167 | if campaign_id is None: 168 | campaign_id = self.application.config['campaign_id'] 169 | campaign_id = str(campaign_id) 170 | config = { 171 | 'created': datetime.datetime.utcnow(), 172 | 'configuration': config 173 | } 174 | self.storage['campaigns'][campaign_id] = config 175 | 176 | def get_message_config_tab(self): 177 | main_window = self.application.main_window 178 | mailer_tab = main_window.tabs['mailer'] 179 | return mailer_tab.tabs['config'] 180 | mailer_config_tab.objects_save_to_config() 181 | 182 | def get_current_config(self): 183 | """ 184 | Get the current configuration options that are managed. This saves the 185 | settings from the message configuration tab to the standard 186 | configuration then returns a new dictionary with all of the managed 187 | settings. 188 | 189 | :return: The current configuration options. 190 | :rtype: dict 191 | """ 192 | app_config = self.application.config 193 | mailer_config_tab = self.get_message_config_tab() 194 | mailer_config_tab.objects_save_to_config() 195 | current_config = dict((key, value) for key, value in app_config.items() if is_managed_key(key)) 196 | return current_config 197 | 198 | def signal_kpc_campaign_set(self, app, old_campaign_id, new_campaign_id): 199 | dft_config = self.load_default_config() 200 | app_config = self.application.config 201 | mailer_config_tab = self.get_message_config_tab() 202 | 203 | if old_campaign_id is not None: 204 | # switching campaigns 205 | self.set_campaign_config(self.get_current_config(), old_campaign_id) 206 | self.save_storage() 207 | 208 | new_campaign_config = self.get_campaign_config(new_campaign_id) 209 | if new_campaign_config: 210 | for key in app_config.keys(): 211 | if not is_managed_key(key): 212 | continue 213 | if key in new_campaign_config: 214 | app_config[key] = new_campaign_config[key] 215 | elif key in dft_config: 216 | app_config[key] = dft_config[key] 217 | elif not self.config['transfer_options']: 218 | for key in app_config.keys(): 219 | if not is_managed_key(key): 220 | continue 221 | app_config[key] = dft_config.get(key) 222 | 223 | mailer_config_tab.objects_load_from_config() 224 | -------------------------------------------------------------------------------- /server/request_redirect.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import ipaddress 3 | 4 | import king_phisher.errors as errors 5 | import king_phisher.server.plugins as plugins 6 | import king_phisher.server.server_rpc as server_rpc 7 | import king_phisher.server.signals as signals 8 | 9 | try: 10 | import rule_engine 11 | except ImportError: 12 | has_rule_engine = False 13 | else: 14 | has_rule_engine = True 15 | 16 | EXAMPLE_CONFIG = """\ 17 | # the optional access level to require for users to change entries 18 | access_level_write: 1000 19 | entries: 20 | # first entry is an exception because no target is specified 21 | - source: 192.168.0.0/16 22 | 23 | # second rule redirects all ipv4 addresses to google 24 | - source: 0.0.0.0/0 25 | target: https://www.google.com 26 | permanent: false 27 | """ 28 | 29 | def _context_resolver(handler, name): 30 | if name == 'accept': 31 | return handler.headers.get('Accept', '') 32 | elif name == 'dst_addr': 33 | return handler.server.socket.getsockname()[0] 34 | elif name == 'dst_port': 35 | return handler.server.socket.getsockname()[1] 36 | elif name == 'path': 37 | return handler.request_path 38 | elif name == 'src_addr': 39 | return handler.client_address[0] 40 | elif name == 'src_port': 41 | return handler.client_address[1] 42 | elif name == 'user_agent': 43 | return handler.headers.get('User-Agent', '') 44 | elif name == 'verb': 45 | return handler.command 46 | elif name == 'vhost': 47 | return handler.vhost 48 | raise rule_engine.SymbolResolutionError(name) 49 | 50 | def check_access_level(function): 51 | @functools.wraps(function) 52 | def wrapped(plugin, handler, *args, **kwargs): 53 | if not plugin.handler_has_write_access(handler): 54 | raise errors.KingPhisherPermissionError('the user does not possess the necessary access level to change this data') 55 | return function(plugin, handler, *args, **kwargs) 56 | return wrapped 57 | 58 | class Plugin(plugins.ServerPlugin): 59 | authors = ['Spencer McIntyre'] 60 | classifiers = ['Plugin :: Server :: Notifications'] 61 | title = 'Request Redirect' 62 | description = """ 63 | A plugin that allows requests to be redirected based on a matching source 64 | IP address or Range. This can be useful for redirecting known ranges of 65 | systems which maybe analyzing the server. 66 | Rules are processed in order and each one is a hash with at least a source 67 | key of an IP address or network. Additionally a target string will be used 68 | as the destination of the redirect or can be left as null for an exception. 69 | Finally, a boolean key of permanent can be used to specify whether a 301 or 70 | 302 redirect should be used. 71 | """ 72 | homepage = 'https://github.com/securestate/king-phisher-plugins' 73 | req_min_version = '1.14.0b0' 74 | req_packages = { 75 | 'rule-engine': has_rule_engine 76 | } 77 | version = '2.0' 78 | rule_types = { 79 | 'accept': rule_engine.DataType.STRING, 80 | 'dst_addr': rule_engine.DataType.STRING, 81 | 'dst_port': rule_engine.DataType.FLOAT, 82 | 'path': rule_engine.DataType.STRING, 83 | 'src_addr': rule_engine.DataType.STRING, 84 | 'src_port': rule_engine.DataType.FLOAT, 85 | 'user_agent': rule_engine.DataType.STRING, 86 | 'verb': rule_engine.DataType.STRING, 87 | 'vhost': rule_engine.DataType.STRING, 88 | } 89 | def initialize(self): 90 | self._context = rule_engine.Context( 91 | resolver=_context_resolver, 92 | type_resolver=rule_engine.type_resolver_from_dict(self.rule_types) 93 | ) 94 | self._pending = set() 95 | signals.server_initialized.connect(self.on_server_initialized) 96 | signals.rpc_user_logged_out.connect(self.on_rpc_user_logged_out) 97 | return True 98 | 99 | def _entry_from_raw(self, entry, index=None): 100 | entry = entry.copy() 101 | if 'rule' in entry: 102 | entry['rule'] = rule_engine.Rule(entry['rule'], context=self._context) 103 | elif 'source' in entry: 104 | entry['source'] = ipaddress.ip_network(entry['source']) 105 | else: 106 | raise RuntimeError("rule {}contains neither a rule or source key".format('' if index is None else '#' + str(index) + ' ')) 107 | entry['permanent'] = entry.get('permanent', True) 108 | return entry 109 | 110 | def _entry_to_raw(self, entry): 111 | entry = entry.copy() 112 | if 'rule' in entry: 113 | entry['rule'] = str(entry['rule']) 114 | if 'source' in entry: 115 | entry['source'] = str(entry['source']) 116 | return entry 117 | 118 | @check_access_level 119 | def _rpc_request_entries_insert(self, handler, index, rule): 120 | rule = self._entry_from_raw(rule, index) 121 | self.entries.insert(index, rule) 122 | self._pending.add(handler.rpc_session_id) 123 | 124 | def _rpc_request_entries_list(self, handler): 125 | rules = [] 126 | for rule in self.entries: 127 | rule = dict(rule) # shallow copy 128 | if 'rule' in rule: 129 | rule['rule'] = rule['rule'].text 130 | if 'source' in rule: 131 | rule['source'] = str(rule['source']) 132 | rules.append(rule) 133 | return rules 134 | 135 | def _rpc_request_permissions(self, handler): 136 | permissions = ['read'] 137 | if self.handler_has_write_access(handler): 138 | permissions.append('write') 139 | return permissions 140 | 141 | @check_access_level 142 | def _rpc_request_entries_remove(self, handler, index): 143 | del self.entries[index] 144 | self._pending.add(handler.rpc_session_id) 145 | 146 | @check_access_level 147 | def _rpc_request_entries_set(self, handler, index, rule): 148 | rule = self._entry_from_raw(rule, index) 149 | self.entries[index] = rule 150 | self._pending.add(handler.rpc_session_id) 151 | 152 | def _rpc_request_symbols(self, handler): 153 | return {key: value.name for key, value in self.rule_types.items()} 154 | 155 | def _store_entries(self): 156 | self.logger.info("storing {:,} request redirect entries to the database storage".format(len(self.entries))) 157 | self.storage['entries'] = [self._entry_to_raw(entry) for entry in self.entries] 158 | 159 | def finalize(self): 160 | self._store_entries() 161 | 162 | def handler_has_write_access(self, handler): 163 | access_level = self.config.get('access_level_write') 164 | if access_level is None: 165 | return True 166 | return handler.rpc_session.user_access_level <= access_level 167 | 168 | def on_request_handle(self, handler): 169 | if handler.command == 'RPC' or handler.path.startswith('/_/'): 170 | return 171 | client_ip = ipaddress.ip_address(handler.client_address[0]) 172 | for entry in self.entries: 173 | if 'rule' in entry and not entry['rule'].matches(handler): 174 | continue 175 | if 'source' in entry and client_ip not in entry['source']: 176 | continue 177 | target = entry.get('target') 178 | if not target: 179 | self.logger.debug("request redirect rule for {0} matched exception".format(str(client_ip))) 180 | break 181 | self.logger.debug("request redirect rule for {0} matched target: {1}".format(str(client_ip), target)) 182 | self.respond_redirect(handler, entry) 183 | raise errors.KingPhisherAbortRequestError(response_sent=True) 184 | 185 | def on_rpc_user_logged_out(self, handler, session, name): 186 | if session not in self._pending: 187 | return 188 | self._pending.remove(session) 189 | self._store_entries() 190 | 191 | def on_server_initialized(self, server): 192 | entries = self.config.get('entries', []) 193 | if entries: 194 | self.logger.debug("loaded request redirect entries from the configuration".format(len(entries))) 195 | else: 196 | entries = self.storage.get('entries', []) 197 | if entries: 198 | self.logger.debug("loaded request redirect entries from the database storage".format(len(entries))) 199 | self.entries = [] 200 | for idx, entry in enumerate(entries, 1): 201 | self.entries.append(self._entry_from_raw(entry, idx)) 202 | 203 | signals.request_handle.connect(self.on_request_handle) 204 | self.logger.info("initialized with {0:,} redirect entries".format(len(self.entries))) 205 | 206 | rpc_api_base = '/plugins/request_redirect/' 207 | server_rpc.register_rpc(rpc_api_base + 'entries/insert')(self._rpc_request_entries_insert) 208 | server_rpc.register_rpc(rpc_api_base + 'entries/list')(self._rpc_request_entries_list) 209 | server_rpc.register_rpc(rpc_api_base + 'entries/remove')(self._rpc_request_entries_remove) 210 | server_rpc.register_rpc(rpc_api_base + 'entries/set')(self._rpc_request_entries_set) 211 | server_rpc.register_rpc(rpc_api_base + 'permissions')(self._rpc_request_permissions) 212 | server_rpc.register_rpc(rpc_api_base + 'rule_symbols')(self._rpc_request_symbols) 213 | 214 | def respond_redirect(self, handler, rule): 215 | handler.send_response(301 if rule['permanent'] else 302) 216 | handler.send_header('Content-Length', 0) 217 | handler.send_header('Location', rule['target']) 218 | handler.end_headers() 219 | -------------------------------------------------------------------------------- /client/request_redirect/request_redirect.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | Request Redirect 8 | center-on-parent 9 | 800 10 | 600 11 | True 12 | 13 | 14 | 15 | 16 | 17 | True 18 | False 19 | vertical 20 | 5 21 | 22 | 23 | True 24 | False 25 | 26 | 27 | True 28 | False 29 | _File 30 | True 31 | 32 | 33 | True 34 | False 35 | 36 | 37 | True 38 | False 39 | Import 40 | True 41 | 42 | 43 | 44 | 45 | True 46 | False 47 | Export 48 | True 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | False 58 | True 59 | 0 60 | 61 | 62 | 63 | 64 | True 65 | False 66 | start 67 | Showing 0 Redirect Configurations 68 | 69 | 70 | 71 | 72 | 73 | False 74 | True 75 | 1 76 | 77 | 78 | 79 | 80 | RequestRedirect.infobar_read_only_warning 81 | True 82 | False 83 | True 84 | 85 | 86 | False 87 | 88 | 89 | OK 90 | RequestRedirect.button_read_only_acknowledgment 91 | True 92 | True 93 | True 94 | 95 | 96 | True 97 | True 98 | 0 99 | 100 | 101 | 102 | 103 | False 104 | True 105 | 0 106 | 107 | 108 | 109 | 110 | False 111 | True 112 | 113 | 114 | True 115 | False 116 | gtk-dialog-warning 117 | 6 118 | 119 | 120 | False 121 | True 122 | 0 123 | 124 | 125 | 126 | 127 | True 128 | False 129 | start 130 | True 131 | The server configuration is read-only. 132 | 133 | 134 | False 135 | True 136 | 1 137 | 138 | 139 | 140 | 141 | False 142 | True 143 | 0 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | False 152 | True 153 | 2 154 | 155 | 156 | 157 | 158 | True 159 | True 160 | True 161 | True 162 | always 163 | in 164 | 165 | 166 | True 167 | True 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | False 176 | True 177 | 3 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt text](https://github.com/securestate/king-phisher/raw/master/data/king-phisher-logo.png "King Phisher") 2 | 3 | # King Phisher Plugins 4 | Plugins to extend the [King Phisher][king-phisher-repo] Phishing Campaign 5 | Toolkit. For more information regarding King Phisher, see the project's 6 | [wiki page][king-phisher-wiki]. 7 | 8 | ## Client Plugins 9 | | Name | Description | 10 | |:------------------------------------------|:------------------| 11 | | [Blink(1) Notifications](/client/blink1.py) | A plugin which will flash a Blink(1) peripheral based on campaign events such as when a new visit is received or new credentials have been submitted. | 12 | | [Campaign Message Configuration Manager](/client/campaign_message_configuration.py) | Store campaign message configurations for their respective campaigns. This allows users to switch between campaigns while keeping each of the message configurations and restoring them when the user returns to the original campaign. New campaigns can either be created with customizable default settings or from the existing configuration (see the "Transfer Settings" option). | 13 | | [Clockwork SMS](/client/clockwork_sms.py) | Send SMS messages using the Clockwork SMS API's email gateway. While enabled, this plugin will automatically update phone numbers into email addresses for sending using the service. | 14 | | [DMARC Check](/client/dmarc.py) | This plugin adds another safety check to the message precheck routines to verify that if DMARC exists the message will not be quarentined or rejected. If no DMARC policy is present, the policy is set to none or the percentage is set to 0, the message sending operation will proceed. | 15 | | [Domain Validator](/client/domain_check.py) | Checks to see if a domain can be resolved and then looks up the WHOIS information for it. Good for email spoofing and bypassing some spam filters. | 16 | | [File Logging](/client/file_logging.py) | Write the client's logs to a file in the users data directory. Additionally if an unhandled exception occurs, the details will be written to a dedicated directory. | 17 | | [GTUBE Header](/client/gtube_header.py) | Add the Generic Test for Unsolicited Bulk Email (GTUBE) string as a X-GTUBE header and append it to the end of all text/* parts of the MIME messages that are sent.

This will cause messages to be identified as SPAM. | 18 | | [Hello World!](/client/hello_world.py) | A 'hello world' plugin to serve as a basic template and demonstration. This plugin will display a message box when King Phisher exits. | 19 | | [Save KPM On Exit](/client/kpm_export_on_exit.py) | Prompt to save the message data as a KPM file when King Phisher exits. | 20 | | [Upload KPM](/client/kpm_export_on_send.py) | Saves a KPM file to the King Phisher server when sending messages. The user must have write permissions to the specified directories. Both the "Local Directory" and "Remote Directory" options can use the variables that are available for use in message templates. | 21 | | [Message Padding](/client/message_padding.py) | Add and modify custom HTML messages from a file to reduce Spam Assassin scores. This plugin interacts with the message content to append a long series of randomly generated sentences to meet the ideal image-text ratio. | 22 | | [Message Plaintext](/client/message_plaintext.py) | Parse and include a plaintext version of an email based on the HTML version. | 23 | | [Custom Message MIME Headers](/client/mime_headers.py) | Add custom MIME headers to messages that are sent. This can, for example be used to add a Sender and / or a Return-Path header to outgoing messages. Headers are rendered as template strings and can use variables that are valid in messages. | 24 | | [Office 2007+ Document Metadata Remover](/client/office_metadata_remover.py) | Remove metadata from Microsoft Office 2007+ file types. These files types generally use the extension docx, pptx, xlsx etc. If the attachment file is not an Office 2007+ file, this plugin does not modify it or block the sending operation. | 25 | | [Generate PDF](/client/pdf_generator.py) | Generates a PDF file from an html attachment that process client King Phisher Jinja variables allowing to embed links to your landing page so users that click the link in the PDF can be tracked when they visit. | 26 | | [Phishery DOCX URL Injector](/client/phishery_docx.py) | Inject Word Document Template URLs into DOCX files. The Phishery technique is used to place multiple document template URLs into the word document (one per-line from the plugin settings). | 27 | | [Request Redirect](/client/request_redirect.py) | Edit entries for the server "Request Redirect" plugin. | 28 | | [Sample Set Generator](/client/sample_set_generator.py) | Brings in a master list and generates a sample set from said list. | 29 | | [SFTP Client](/client/sftp_client.py) | Secure File Transfer Protocol Client that can be used to upload, download, create, and delete local and remote files on the King Phisher Server.

The editor allows you edit files on remote or local system. It is primarily designed for the use of editing remote web pages on the King Phisher Server. | 30 | | [Spell Check](/client/spell_check.py) | Add spell check capabilities to the message editor. This requires GtkSpell to be available with the correct Python GObject Introspection bindings. On Ubuntu and Debian based systems, this is provided by the 'gir1.2-gtkspell3-3.0' package.

After being loaded, the language can be changed from the default of en_US via the context menu (available when right clicking in the text view). | 31 | | [TOTP Self Enrollment](/client/totp_enrollment.py) | This plugin allows users to manage the two factor authentication settings on their account. This includes setting a new and removing an existing TOTP secret. The two factor authentication used by King Phisher is compatible with free mobile applications such as Google Authenticator. | 32 | | [URI Spoof Generator](/client/uri_spoof_generator.py) | Exports a redirect page which allows URI spoofing in the address bar of the target's browser. | 33 | 34 | ## Server Plugins 35 | | Name | Description | 36 | |:------------------------------------------|:------------------| 37 | | [Campaign Alerts: via Python 3 SMTPLib](/server/alerts_email_via_smtp.py) | Send campaign alerts via the SMTP Python 3 lib. This requires that users specify their email through the King Phisher client to subscribe to notifications. | 38 | | [Campaign Alerts: via SMTP2Go](/server/alerts_email_via_smtp2go.py) | Send campaign alerts via the SMTP2go lib. This requires that users specify their email through the King Phisher client to subscribe to notifications. | 39 | | [Campaign Alerts: via Clockwork SMS](/server/alerts_sms_via_clockwork.py) | Send campaign alerts via the Clockwork SMS API. This requires that users specify their cell phone number through the King Phisher client. | 40 | | [Campaign Alerts: via Carrier SMS Email Gateways](/server/alerts_sms_via_email.py) | Send campaign alerts as SMS messages through cell carrier's email gateways. This requires that users supply both their cell phone number and specify a supported carrier through the King Phisher client. | 41 | | [Hello World!](/server/hello_world.py) | A 'hello world' plugin to serve as a basic template and demonstration. This plugin will log simple messages to show that it is functioning. | 42 | | [IFTTT Campaign Success Notification](/server/ifttt_on_campaign_success.py) | A plugin that will publish an event to a specified IFTTT Maker channel when a campaign has been deemed 'successful'. | 43 | | [Postfix Message Information](/server/postfix_message_info.py) | A plugin that analyzes message information from the postfix logs to provide King Phisher clients message status and detail information. | 44 | | [Pushbullet Notifications](/server/pushbullet_notifications.py) | A plugin that uses Pushbullet's API to send push notifications on new website visits and submitted credentials. | 45 | | [Request Redirect](/server/request_redirect.py) | A plugin that allows requests to be redirected based on a matching source IP address or Range. This can be useful for redirecting known ranges of systems which maybe analyzing the server. Rules are processed in order and each one is a hash with at least a source key of an IP address or network. Additionally a target string will be used as the destination of the redirect or can be left as null for an exception. Finally, a boolean key of permanent can be used to specify whether a 301 or 302 redirect should be used. | 46 | | [Slack Notifications](/server/slack_notifications.py) | A plugin that uses Slack Webhooks to send notifications on new website visits and submitted credentials to a slack channel. Notifications about credentials are sent with @here. | 47 | | [XMPP Notifications](/server/xmpp_notifications.py) | A plugin which pushes notifications regarding the King Phisher server to a specified XMPP server. | 48 | 49 | ## Plugin Installation 50 | ### Client Plugin Installation 51 | Client plugins can be placed in the `$HOME/.config/king-phisher/plugins` 52 | directory, then loaded and enabled with the plugin manager. 53 | 54 | ### Server Plugin Installation 55 | Server plugins can be placed in the `data/server/king_phisher/plugins` 56 | directory of the King Phisher installation. Additional search paths can be 57 | defined using the `plugin_directories` option in the server's configuration 58 | file. After being copied into the necessary directory, the server's 59 | configuration file needs to be updated to enable the plugin. 60 | 61 | ### Dependency Installation 62 | Some plugins require additional Python packages to be installed in order to 63 | function. These packages must be installed in the King Phisher environment by 64 | running `pipenv install $package` from within the King Phisher installation 65 | directory. 66 | 67 | ## License 68 | King Phisher Plugins are released under the BSD 3-clause license, for more 69 | details see the [LICENSE][license-file] file. 70 | 71 | [king-phisher-repo]: https://github.com/securestate/king-phisher 72 | [king-phisher-wiki]: https://github.com/securestate/king-phisher/wiki 73 | [license-file]: /LICENSE -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pre-commit 5 | # 6 | # Copyright 2016 Spencer McIntyre 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are 10 | # met: 11 | # 12 | # * Redistributions of source code must retain the above copyright 13 | # notice, this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above 15 | # copyright notice, this list of conditions and the following disclaimer 16 | # in the documentation and/or other materials provided with the 17 | # distribution. 18 | # * Neither the name of the nor the names of its 19 | # contributors may be used to endorse or promote products derived from 20 | # this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | # 34 | 35 | # This file is used by the development team in order to ensure that the catalog 36 | # is properly updated with a valid signature. This file should be symlinked to 37 | # .git/hooks/pre-commit and assumes that King Phisher exists in a directory 38 | # at the same level as the plugins, such that: 39 | # 40 | # some/path/king-phisher 41 | # some/path/king-phisher-plugins 42 | # 43 | # The King Phisher development dependencies must have been installed with 44 | # `pipenv install --dev`, and the operator should have a valid King Phisher 45 | # signing key. 46 | # 47 | # Commands to merge a Pull Request: 48 | # 49 | # 1. git checkout master 50 | # 2. git merge --edit --no-commit --no-ff pr/## 51 | # 3. git commit 52 | # 53 | # The third command will run the pre-commit script and properly sign the 54 | # necessary files. In Git v2.24 a new pre-merge-commit hook was made available 55 | # which may streamline this process, but this has not been tested. 56 | 57 | import argparse 58 | import collections 59 | import datetime 60 | import getpass 61 | import glob 62 | import importlib 63 | import os 64 | import sys 65 | 66 | import jinja2 67 | 68 | __version__ = '1.0' 69 | 70 | if sys.version_info < (3, 4): 71 | # python 3 is required to import without an __init__.py file 72 | print('this requires at least python version 3.4 to run') 73 | sys.exit(os.EX_SOFTWARE) 74 | 75 | plugins_dir = os.path.dirname(os.path.abspath(__file__)) 76 | if plugins_dir.endswith(os.path.join('.git', 'hooks')): 77 | plugins_dir = os.path.normpath(os.path.join(plugins_dir, '..', '..')) 78 | king_phisher_dir = os.path.normpath(os.path.join(plugins_dir, '..', 'king-phisher')) 79 | 80 | sys.path.insert(0, king_phisher_dir) 81 | sys.path.insert(0, plugins_dir) 82 | 83 | try: 84 | import git 85 | except ImportError: 86 | print('failed to import GitPython') 87 | sys.exit(os.EX_UNAVAILABLE) 88 | 89 | try: 90 | import king_phisher.catalog as catalog 91 | import king_phisher.find as find 92 | import king_phisher.security_keys as security_keys 93 | import king_phisher.serializers as serializers 94 | except ImportError: 95 | print('failed to import king_phisher') 96 | print('path assumed to be at: ' + king_phisher_dir) 97 | sys.exit(os.EX_UNAVAILABLE) 98 | 99 | find.init_data_path() 100 | 101 | PLUGIN_TYPES = ('client', 'server') 102 | Plugin = collections.namedtuple('Plugin', ('class_', 'name', 'removed', 'type')) 103 | 104 | def load_plugins(plugin_type, plugins_dir, verbose=False): 105 | plugins = [] 106 | plugins_dir = os.path.join(plugins_dir, plugin_type) 107 | for plugin in glob.glob(os.path.join(plugins_dir, '*.py')): 108 | plugin = os.path.basename(plugin) 109 | plugin = os.path.splitext(plugin)[0] 110 | if verbose: 111 | print('loading ' + plugin_type + ' plugin: ' + plugin) 112 | plugin_module = importlib.import_module(plugin_type + '.' + plugin) 113 | plugins.append(plugin_module.Plugin) 114 | 115 | for plugin in os.listdir(plugins_dir): 116 | if not os.path.isdir(os.path.join(plugins_dir, plugin)): 117 | continue 118 | if not os.path.isfile(os.path.join(plugins_dir, plugin, '__init__.py')): 119 | continue 120 | if verbose: 121 | print('loading ' + plugin_type + ' plugin: ' + plugin) 122 | plugin_module = importlib.import_module(plugin_type + '.' + plugin) 123 | plugins.append(plugin_module.Plugin) 124 | plugins = sorted(plugins, key=lambda plugin: plugin.name) 125 | return plugins 126 | 127 | def _make_plugin_collection(plugins_repo, plugin, signing_key, verbose=False): 128 | plugin_path = os.path.join(plugins_repo.working_tree_dir, plugin.type, plugin.name) 129 | if os.path.isfile(plugin_path + '.py'): 130 | plugin_path += '.py' 131 | elif not (os.path.isdir(plugin_path) and os.path.isfile(os.path.join(plugin_path, '__init__.py'))): 132 | raise RuntimeError("unknown path for {0} plugin: {1}".format(plugin.type, plugin.name)) 133 | plugin_metadata = plugin.class_.metadata 134 | plugin_metadata.pop('is_compatible', None) 135 | 136 | if verbose: 137 | print('signing files for ' + plugin.type + ' plugin: ' + plugin.name) 138 | item_files = catalog.sign_item_files(plugin_path, signing_key, repo_path=plugins_dir) 139 | plugin_metadata['files'] = list(sorted(item_files, key=lambda item_file: item_file.path_source)) 140 | for idx, item_file in enumerate(plugin_metadata['files']): 141 | plugin_metadata['files'][idx] = { 142 | 'path-destination': item_file.path_destination, 143 | 'path-source': item_file.path_source, 144 | 'signature': item_file.signature, 145 | 'signed-by': item_file.signed_by 146 | } 147 | return plugin_metadata 148 | 149 | def _plugin_exists(plugins_repo, plugin_type, plugin_name): 150 | plugin_path = os.path.join(plugins_repo.working_tree_dir, plugin_type, plugin_name) 151 | if os.path.isfile(plugin_path + '.py'): 152 | return True 153 | elif os.path.isdir(plugin_path) and os.path.isfile(os.path.join(plugin_path, '__init__.py')): 154 | return True 155 | return False 156 | 157 | def _get_all_plugins(plugins): 158 | new_plugins = collections.deque() 159 | for plugin_type in PLUGIN_TYPES: 160 | for plugin_class in plugins.get(plugin_type, []): 161 | new_plugins.append(Plugin( 162 | class_=plugin_class, 163 | name=plugin_class.name, 164 | removed=False, 165 | type=plugin_type 166 | )) 167 | return new_plugins 168 | 169 | def _get_modified_plugins(plugins, plugins_repo): 170 | path_to_name = lambda path: path.split(os.sep)[1] 171 | staged_plugin_files = collections.defaultdict(set) 172 | for diff in plugins_repo.index.diff('HEAD'): 173 | for staged_file in (diff.a_path, diff.b_path): 174 | plugin_type = staged_file.split(os.sep, 1)[0] 175 | if plugin_type not in PLUGIN_TYPES: 176 | continue 177 | plugin_name = path_to_name(staged_file) 178 | if plugin_name.endswith('.py'): 179 | plugin_name = plugin_name[:-3] 180 | staged_plugin_files[(plugin_type, plugin_name)].add(staged_file) 181 | 182 | new_plugins = collections.deque() 183 | for (plugin_type, plugin_name) in staged_plugin_files.keys(): 184 | plugin_class = next((plugin_class for plugin_class in plugins[plugin_type] if plugin_class.name == plugin_name), None) 185 | new_plugins.append(Plugin( 186 | class_=plugin_class, 187 | name=plugin_name, 188 | removed=not _plugin_exists(plugins_repo, plugin_type, plugin_name), 189 | type=plugin_type 190 | )) 191 | return new_plugins 192 | 193 | def update_plugin_collections(collection, plugins_repo, plugins, signing_key, sign_all=True, verbose=False): 194 | # update the collection metadata 195 | collection['created'] = datetime.datetime.utcnow().isoformat() + '+00:00' 196 | collection['created-by'] = signing_key.id 197 | collection['signed-by'] = signing_key.id 198 | 199 | # get the plugins to process, either all or just the modified ones 200 | if sign_all: 201 | plugins = _get_all_plugins(plugins) 202 | # remove all existing collections 203 | collection['collections'] = {} 204 | else: 205 | plugins = _get_modified_plugins(plugins, plugins_repo) 206 | # preserve existing collections 207 | collection['collections'] = collection.get('collections', {}) 208 | 209 | for plugin in plugins: 210 | plugin_collection = collection['collections'].get('plugins/' + plugin.type, []) 211 | # check if the plugin already exists... 212 | existing = next((plugin_metadata for plugin_metadata in plugin_collection if plugin_metadata['name'] == plugin.name), None) 213 | if existing: 214 | # and if so, remove the entry 215 | plugin_collection.remove(existing) 216 | if plugin.removed: 217 | continue 218 | plugin_collection.append(_make_plugin_collection(plugins_repo, plugin, signing_key, verbose=verbose)) 219 | collection['collections']['plugins/' + plugin.type] = plugin_collection 220 | 221 | for plugin_type in set((plugin.type for plugin in plugins)): 222 | collection_type = 'plugins/' + plugin_type 223 | plugin_collection = collection['collections'].get(collection_type) 224 | if plugin_collection is None: 225 | continue 226 | plugin_collection = sorted(plugin_collection, key=lambda plugin: plugin['name']) 227 | collection['collections'][collection_type] = plugin_collection 228 | 229 | def main(): 230 | parser = argparse.ArgumentParser(description='pre-commit hook', conflict_handler='resolve') 231 | parser.add_argument('-k', '--key', default=os.getenv('KING_PHISHER_DEV_KEY'), help='path to a signing key') 232 | parser.add_argument('-V', '--verbose', action='store_true', default=False, help='print verbose output') 233 | parser.add_argument('-v', '--version', action='version', version='%(prog)s Version: ' + __version__) 234 | parser.add_argument('--git-no-add', action='store_false', default=True, dest='git_add', help='don\'t stage changed files in git') 235 | parser.add_argument('--sign-all', action='store_true', default=False, help='sign all files') 236 | arguments = parser.parse_args() 237 | 238 | if arguments.key is None: 239 | print('must specify a dev key path') 240 | return os.EX_USAGE 241 | if not os.path.isfile(arguments.key): 242 | print('invalid path to dev key: ' + arguments.key) 243 | return os.EX_NOINPUT 244 | if not os.access(arguments.key, os.R_OK): 245 | print('missing read privileges on dev key: ' + arguments.key) 246 | return os.EX_NOPERM 247 | 248 | try: 249 | signing_key = security_keys.SigningKey.from_file( 250 | arguments.key, 251 | password=(getpass.getpass('password: ') if arguments.key.endswith('.enc') else None) 252 | ) 253 | except KeyboardInterrupt: 254 | return os.EX_TEMPFAIL 255 | except ValueError: 256 | print('failed to load the signing key') 257 | return os.EX_TEMPFAIL 258 | 259 | if arguments.verbose: 260 | print('plugins directory: ' + plugins_dir) 261 | plugins_repo = git.Repo(plugins_dir) 262 | 263 | jinja_env = jinja2.Environment(trim_blocks=True) 264 | jinja_env.filters['strftime'] = lambda dt, fmt: dt.strftime(fmt) 265 | with open(os.path.join(plugins_dir, 'README.jnj'), 'r') as file_h: 266 | readme_template = jinja_env.from_string(file_h.read()) 267 | 268 | plugins = { 269 | 'client': load_plugins('client', plugins_dir, verbose=arguments.verbose), 270 | 'server': load_plugins('server', plugins_dir, verbose=arguments.verbose) 271 | } 272 | readme = readme_template.render(plugins=plugins, timestamp=datetime.datetime.utcnow()) 273 | with open(os.path.join(plugins_dir, 'README.md'), 'w') as file_h: 274 | file_h.write(readme) 275 | if arguments.git_add: 276 | plugins_repo.index.add(['README.md']) 277 | 278 | with open(os.path.join(plugins_dir, 'catalog-collections.json'), 'r') as file_h: 279 | collection = serializers.JSON.load(file_h) 280 | 281 | update_plugin_collections( 282 | collection, 283 | plugins_repo, 284 | plugins, 285 | signing_key, 286 | sign_all=arguments.sign_all, 287 | verbose=arguments.verbose 288 | ) 289 | 290 | collection = signing_key.sign_dict(collection) 291 | with open(os.path.join(plugins_dir, 'catalog-collections.json'), 'w') as file_h: 292 | serializers.JSON.dump(collection, file_h) 293 | if arguments.git_add: 294 | plugins_repo.index.add(['catalog-collections.json']) 295 | return os.EX_OK 296 | 297 | if __name__ == '__main__': 298 | sys.exit(main()) 299 | -------------------------------------------------------------------------------- /client/request_redirect/__init__.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import datetime 3 | import functools 4 | import ipaddress 5 | import os 6 | 7 | from king_phisher import serializers 8 | from king_phisher.client import gui_utilities 9 | from king_phisher.client import plugins 10 | from king_phisher.client.widget import extras 11 | from king_phisher.client.widget import managers 12 | 13 | from gi.repository import GObject 14 | from gi.repository import Gtk 15 | import jsonschema 16 | import rule_engine 17 | import rule_engine.errors 18 | 19 | relpath = functools.partial(os.path.join, os.path.dirname(os.path.realpath(__file__))) 20 | gtk_builder_file = relpath('request_redirect.ui') 21 | json_schema_file = relpath('schema.json') 22 | 23 | _ModelNamedRow = collections.namedtuple('ModelNamedRow', ( 24 | 'index', 25 | 'target', 26 | 'permanent', 27 | 'type', 28 | 'text' 29 | )) 30 | 31 | def named_row_to_entry(named_row): 32 | entry = { 33 | 'permanent': named_row.permanent, 34 | 'target': named_row.target, 35 | named_row.type.lower(): named_row.text 36 | } 37 | return entry 38 | 39 | def _update_model_indexes(model, starting, modifier): 40 | for row in model: 41 | named_row = _ModelNamedRow(*row) 42 | if named_row.index < starting: 43 | continue 44 | model[row.iter][_ModelNamedRow._fields.index('index')] += modifier 45 | 46 | class _CellRendererIndex(getattr(extras, 'CellRendererPythonText', object)): 47 | python_value = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE) 48 | @staticmethod 49 | def render_python_value(value): 50 | if not isinstance(value, int): 51 | return 52 | return str(value + 1) 53 | 54 | class Plugin(plugins.ClientPlugin): 55 | authors = ['Spencer McIntyre'] 56 | title = 'Request Redirect' 57 | description = """ 58 | Edit entries for the server "Request Redirect" plugin. 59 | """ 60 | homepage = 'https://github.com/securestate/king-phisher' 61 | req_min_version = '1.14.0b1' 62 | version = '1.0.0' 63 | def initialize(self): 64 | self.window = None 65 | if not os.access(gtk_builder_file, os.R_OK): 66 | gui_utilities.show_dialog_error( 67 | 'Plugin Error', 68 | self.application.get_active_window(), 69 | "The GTK Builder data file ({0}) is not available.".format(os.path.basename(gtk_builder_file)) 70 | ) 71 | return False 72 | self._label_summary = None 73 | self._rule_context = None 74 | self._tv = None 75 | self._tv_model = Gtk.ListStore(int, str, bool, str, str) 76 | self._tv_model.connect('row-inserted', self.signal_model_multi) 77 | self._tv_model.connect('row-deleted', self.signal_model_multi) 78 | self.menu_items = {} 79 | self.menu_items['edit_rules'] = self.add_menu_item('Tools > Request Redirect Rules', self.show_editor_window) 80 | return True 81 | 82 | def _editor_refresh(self): 83 | self.application.rpc.async_call( 84 | 'plugins/request_redirect/entries/list', 85 | on_success=self.asyncrpc_list, 86 | when_idle=False 87 | ) 88 | 89 | def _editor_delete(self, treeview, selection): 90 | selection = treeview.get_selection() 91 | (model, tree_paths) = selection.get_selected_rows() 92 | if not tree_paths: 93 | return 94 | rows = [] 95 | for tree_path in tree_paths: 96 | rows.append((_ModelNamedRow(*model[tree_path]).index, Gtk.TreeRowReference.new(model, tree_path))) 97 | if len(rows) == 1: 98 | message = 'Delete This Row?' 99 | else: 100 | message = "Delete These {0:,} Rows?".format(len(rows)) 101 | if not gui_utilities.show_dialog_yes_no(message, self.window, 'This information will be lost.'): 102 | return 103 | 104 | rows = reversed(sorted(rows, key=lambda item: item[0])) 105 | for row_index, row_ref in rows: 106 | self.application.rpc.async_call( 107 | 'plugins/request_redirect/entries/remove', 108 | (row_index,), 109 | on_success=self.asyncrpc_remove, 110 | when_idle=True, 111 | cb_args=(model, row_ref) 112 | ) 113 | 114 | def _update_remote_entry(self, path): 115 | named_row = _ModelNamedRow(*self._tv_model[path]) 116 | entry = named_row_to_entry(named_row) 117 | self.application.rpc.async_call( 118 | 'plugins/request_redirect/entries/set', 119 | (named_row.index, entry) 120 | ) 121 | 122 | def finalize(self): 123 | if self.window is not None: 124 | self.window.destroy() 125 | 126 | def show_editor_window(self, _): 127 | self.application.rpc.async_graphql( 128 | 'query getPlugin($name: String!) { plugin(name: $name) { version } }', 129 | query_vars={'name': self.name}, 130 | on_success=self.asyncrpc_graphql, 131 | when_idle=True 132 | ) 133 | 134 | def asyncrpc_graphql(self, plugin_info): 135 | if plugin_info['plugin'] is None: 136 | gui_utilities.show_dialog_error( 137 | 'Missing Server Plugin', 138 | self.application.get_active_window(), 139 | 'The server side plugin is missing. It must be installed and enabled by the server administrator.' 140 | ) 141 | return 142 | self.application.rpc.async_call( 143 | 'plugins/request_redirect/permissions', 144 | on_success=self.asyncrpc_permissions, 145 | when_idle=True 146 | ) 147 | 148 | def asyncrpc_permissions(self, permissions): 149 | writable = 'write' in permissions 150 | if self.window is None: 151 | self.application.rpc.async_call( 152 | 'plugins/request_redirect/rule_symbols', 153 | on_success=self.asyncrpc_symbols, 154 | when_idle=False 155 | ) 156 | builder = Gtk.Builder() 157 | self.logger.debug('loading gtk builder file from: ' + gtk_builder_file) 158 | builder.add_from_file(gtk_builder_file) 159 | 160 | self.window = builder.get_object('RequestRedirect.editor_window') 161 | self.window.set_transient_for(self.application.get_active_window()) 162 | self.window.connect('destroy', self.signal_window_destroy) 163 | self._tv = builder.get_object('RequestRedirect.treeview_editor') 164 | self._tv.set_model(self._tv_model) 165 | tvm = managers.TreeViewManager( 166 | self._tv, 167 | cb_delete=(self._editor_delete if writable else None), 168 | cb_refresh=self._editor_refresh, 169 | selection_mode=Gtk.SelectionMode.MULTIPLE, 170 | ) 171 | 172 | # target renderer 173 | target_renderer = Gtk.CellRendererText() 174 | if writable: 175 | target_renderer.set_property('editable', True) 176 | target_renderer.connect('edited', functools.partial(self.signal_renderer_edited, 'target')) 177 | 178 | # permanent renderer 179 | permanent_renderer = Gtk.CellRendererToggle() 180 | if writable: 181 | permanent_renderer.connect('toggled', functools.partial(self.signal_renderer_toggled, 'permanent')) 182 | 183 | # type renderer 184 | store = Gtk.ListStore(str) 185 | store.append(['Rule']) 186 | store.append(['Source']) 187 | type_renderer = Gtk.CellRendererCombo() 188 | type_renderer.set_property('has-entry', False) 189 | type_renderer.set_property('model', store) 190 | type_renderer.set_property('text-column', 0) 191 | if writable: 192 | type_renderer.set_property('editable', True) 193 | type_renderer.connect('edited', self.signal_renderer_edited_type) 194 | 195 | # text renderer 196 | text_renderer = Gtk.CellRendererText() 197 | if writable: 198 | text_renderer.set_property('editable', True) 199 | text_renderer.connect('edited', functools.partial(self.signal_renderer_edited, 'text')) 200 | 201 | tvm.set_column_titles( 202 | ('#', 'Target', 'Permanent', 'Type', 'Text'), 203 | renderers=( 204 | _CellRendererIndex(), # index 205 | target_renderer, # Target 206 | permanent_renderer, # Permanent 207 | type_renderer, # Type 208 | text_renderer # Text 209 | ) 210 | ) 211 | # treeview right-click menu 212 | menu = tvm.get_popup_menu() 213 | if writable: 214 | menu_item = Gtk.MenuItem.new_with_label('Insert') 215 | menu_item.connect('activate', self.signal_menu_item_insert) 216 | menu_item.show() 217 | menu.append(menu_item) 218 | 219 | # top menu bar 220 | menu_item = builder.get_object('RequestRedirect.menuitem_import') 221 | menu_item.connect('activate', self.signal_menu_item_import) 222 | menu_item.set_sensitive(writable) 223 | menu_item = builder.get_object('RequestRedirect.menuitem_export') 224 | menu_item.connect('activate', self.signal_menu_item_export) 225 | 226 | infobar = builder.get_object('RequestRedirect.infobar_read_only_warning') 227 | infobar.set_revealed(not writable) 228 | button = builder.get_object('RequestRedirect.button_read_only_acknowledgment') 229 | button.connect('clicked', lambda _: infobar.set_revealed(False)) 230 | 231 | self._label_summary = builder.get_object('RequestRedirect.label_summary') 232 | self._editor_refresh() 233 | self.window.show() 234 | self.window.present() 235 | 236 | def asyncrpc_list(self, entries): 237 | things = [] 238 | for idx, rule in enumerate(entries): 239 | if 'rule' in rule: 240 | type_ = 'Rule' 241 | text = rule['rule'] 242 | elif 'source' in rule: 243 | type_ = 'Source' 244 | text = rule['source'] 245 | else: 246 | self.logger.warning("rule #{0} contains neither a rule or source key".format(idx)) 247 | continue 248 | things.append((idx, rule['target'], rule['permanent'], type_, text)) 249 | gui_utilities.glib_idle_add_store_extend(self._tv_model, things, clear=True) 250 | 251 | def asyncrpc_remove(self, model, row_ref, _): 252 | tree_path = row_ref.get_path() 253 | if tree_path is None: 254 | return 255 | old_index = _ModelNamedRow(*model[tree_path]).index 256 | del model[tree_path] 257 | _update_model_indexes(model, old_index, -1) 258 | 259 | def asyncrpc_symbols(self, symbols): 260 | symbols = {k: getattr(rule_engine.DataType, v) for k, v in symbols.items()} 261 | type_resolver = rule_engine.type_resolver_from_dict(symbols) 262 | self._rule_context = rule_engine.Context(type_resolver=type_resolver) 263 | 264 | def signal_menu_item_export(self, _): 265 | dialog = extras.FileChooserDialog('Export Entries', self.window) 266 | response = dialog.run_quick_save('request_redirect.json') 267 | dialog.destroy() 268 | if not response: 269 | return 270 | entries = [] 271 | for row in self._tv_model: 272 | named_row = _ModelNamedRow(*row) 273 | entries.append(named_row_to_entry(named_row)) 274 | export = { 275 | 'created': datetime.datetime.utcnow().isoformat() + '+00:00', 276 | 'entries': entries 277 | } 278 | with open(response['target_path'], 'w') as file_h: 279 | serializers.JSON.dump(export, file_h) 280 | 281 | def signal_menu_item_import(self, _): 282 | dialog = extras.FileChooserDialog('Import Entries', self.window) 283 | dialog.quick_add_filter('Data Files', '*.json') 284 | dialog.quick_add_filter('All Files', '*') 285 | response = dialog.run_quick_open() 286 | dialog.destroy() 287 | if not response: 288 | return 289 | try: 290 | with open(response['target_path'], 'r') as file_h: 291 | data = serializers.JSON.load(file_h) 292 | except Exception: 293 | gui_utilities.show_dialog_error( 294 | 'Import Failed', 295 | self.window, 296 | 'Could not load the specified file.' 297 | ) 298 | return 299 | with open(json_schema_file, 'r') as file_h: 300 | schema = serializers.JSON.load(file_h) 301 | try: 302 | jsonschema.validate(data, schema) 303 | except jsonschema.exceptions.ValidationError: 304 | gui_utilities.show_dialog_error( 305 | 'Import Failed', 306 | self.window, 307 | 'Could not load the specified file, the data is malformed.' 308 | ) 309 | return 310 | cursor = len(self._tv_model) 311 | for entry in data['entries']: 312 | if 'rule' in entry: 313 | entry_type = 'Rule' 314 | text = entry['rule'] 315 | elif 'source' in entry: 316 | entry_type = 'Source' 317 | text = entry['source'] 318 | new_named_row = _ModelNamedRow(cursor, entry['target'], entry['permanent'], entry_type, text) 319 | self.application.rpc.async_call( 320 | 'plugins/request_redirect/entries/insert', 321 | (cursor, named_row_to_entry(new_named_row)) 322 | ) 323 | self._tv_model.append(new_named_row) 324 | cursor += 1 325 | 326 | def signal_menu_item_insert(self, _): 327 | selection = self._tv.get_selection() 328 | new_named_row = _ModelNamedRow(len(self._tv_model), '', True, 'Source', '0.0.0.0/32') 329 | if selection.count_selected_rows() == 0: 330 | self._tv_model.append(new_named_row) 331 | elif selection.count_selected_rows() == 1: 332 | (model, tree_paths) = selection.get_selected_rows() 333 | tree_iter = model.get_iter(tree_paths[0]) 334 | new_named_row = new_named_row._replace(index=_ModelNamedRow(*model[tree_iter]).index) 335 | _update_model_indexes(model, new_named_row.index, 1) 336 | self._tv_model.insert_before(tree_iter, new_named_row) 337 | else: 338 | gui_utilities.show_dialog_error( 339 | 'Can Not Insert Entry', 340 | self.window, 341 | 'Can not insert a new entry when multiple entries are selected.' 342 | ) 343 | return 344 | 345 | entry = named_row_to_entry(new_named_row) 346 | self.application.rpc.async_call( 347 | 'plugins/request_redirect/entries/set', 348 | (new_named_row.index, entry) 349 | ) 350 | 351 | def signal_model_multi(self, model, *_): 352 | if self._label_summary is None: 353 | return 354 | self._label_summary.set_text("Showing {:,} Redirect Configuration{}".format(len(model), '' if len(model) == 1 else 's')) 355 | 356 | def signal_renderer_edited(self, field, _, path, text): 357 | text = text.strip() 358 | if field == 'text': 359 | entry_type = self._tv_model[path][_ModelNamedRow._fields.index('type')].lower() 360 | if entry_type == 'source': 361 | try: 362 | ipaddress.ip_network(text) 363 | except ValueError: 364 | gui_utilities.show_dialog_error('Invalid Source', self.window, 'The specified text is not a valid IP network in CIDR notation.') 365 | return 366 | else: 367 | try: 368 | rule_engine.Rule(text, context=self._rule_context) 369 | except rule_engine.SymbolResolutionError as error: 370 | gui_utilities.show_dialog_error('Invalid Rule', self.window, "The specified rule text contains the unknown symbol {!r}.".format(error.symbol_name)) 371 | return 372 | except rule_engine.errors.SyntaxError: 373 | gui_utilities.show_dialog_error('Invalid Rule', self.window, 'The specified rule text contains a syntax error.') 374 | return 375 | except rule_engine.errors.EngineError: 376 | gui_utilities.show_dialog_error('Invalid Rule', self.window, 'The specified text is not a valid rule.') 377 | return 378 | self._tv_model[path][_ModelNamedRow._fields.index(field)] = text 379 | self._update_remote_entry(path) 380 | 381 | def signal_renderer_edited_type(self, _, path, text): 382 | field_index = _ModelNamedRow._fields.index('type') 383 | if self._tv_model[path][field_index] == text: 384 | return 385 | self._tv_model[path][field_index] = text 386 | if text.lower() == 'source': 387 | self._tv_model[path][_ModelNamedRow._fields.index('text')] = '0.0.0.0/32' 388 | elif text.lower() == 'rule': 389 | self._tv_model[path][_ModelNamedRow._fields.index('text')] = 'false' 390 | self._update_remote_entry(path) 391 | 392 | def signal_renderer_toggled(self, field, _, path): 393 | index = _ModelNamedRow._fields.index(field) 394 | self._tv_model[path][index] = not self._tv_model[path][index] 395 | self._update_remote_entry(path) 396 | 397 | def signal_window_destroy(self, window): 398 | self.window = None 399 | --------------------------------------------------------------------------------