├── .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 | 
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 |
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 |
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 = '