Last active
January 27, 2022 00:36
-
-
Save zeroSteiner/2b9b159c184ffe171339 to your computer and use it in GitHub Desktop.
King Phisher CLI Mail Utility
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# tools/cli_mailer.py | |
# | |
# Redistribution and use in source and binary forms, with or without | |
# modification, are permitted provided that the following conditions are | |
# met: | |
# | |
# * Redistributions of source code must retain the above copyright | |
# notice, this list of conditions and the following disclaimer. | |
# * Redistributions in binary form must reproduce the above | |
# copyright notice, this list of conditions and the following disclaimer | |
# in the documentation and/or other materials provided with the | |
# distribution. | |
# * Neither the name of the project nor the names of its | |
# contributors may be used to endorse or promote products derived from | |
# this software without specific prior written permission. | |
# | |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
# | |
import argparse | |
import getpass | |
import ipaddress | |
import logging | |
import os | |
import pwd | |
import random | |
import shutil | |
import socket | |
import sys | |
import tempfile | |
import time | |
import urlparse | |
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
# https://github.com/securestate/king-phisher | |
from king_phisher import constants | |
from king_phisher import errors | |
from king_phisher import sms | |
from king_phisher import utilities | |
from king_phisher import version | |
from king_phisher.client import client_rpc | |
from king_phisher.client import export | |
from king_phisher.client import mailer | |
from king_phisher.ssh_forward import SSHTCPForwarder | |
from AdvancedHTTPServer import AdvancedHTTPServerRPCError | |
import paramiko | |
KPM_REQUIRED_SETTINGS = { | |
'mailer.webserver_url': 'Web Server URL', | |
'mailer.company_name': 'Company Name', | |
'mailer.source_email': 'Source Email', | |
'mailer.subject': 'Friendly Alias', | |
'mailer.html_file': 'Message HTML File', | |
'mailer.target_file': 'Target CSV File' | |
} | |
class CLIMailSenderThread(mailer.MailSenderThread): | |
def __init__(self, config, target_file, rpc, sms_carrier=None, sms_number=None): | |
super(CLIMailSenderThread, self).__init__(config, target_file, rpc) | |
self.sms_carrier = sms_carrier | |
self.sms_number = sms_number | |
self.sms_last_sent = float('-inf') | |
self.sms_frequency = 500 | |
def process_pause(self, set_pause=False): | |
if not set_pause: | |
return True | |
self.pause() | |
self.send_sms("Sending messages for the {0} campaign has been paused".format(self.config['campaign_name'])) | |
try: | |
raw_input('sending messages has been paused, press enter to continue... ') | |
except KeyboardInterrupt: | |
return False | |
self.unpause() | |
return True | |
def tab_notify_sent(self, emails_done, emails_total): | |
if (emails_done % self.sms_frequency) == 0: | |
percent = (float(emails_done) / float(emails_total)) * 100 | |
self.send_sms("{0:,} of {1:,} ({2:.2f}%) messages sent for campaign: {3}".format(emails_done, emails_total, percent, self.config['campaign_name'])) | |
def send_sms(self, message, force=False): | |
if not self.sms_carrier: | |
return | |
if not self.sms_number: | |
return | |
if not force and (time.time() - self.sms_last_sent) < 180: | |
return | |
try: | |
sms.send_sms(message, self.sms_number, self.sms_carrier) | |
except: | |
pass | |
else: | |
self.sms_last_sent = time.time() | |
def cli_runner(arguments, tempdir, rpc): | |
logger = logging.getLogger('KingPhisher.Mailer.CLI') | |
try: | |
message_config = export.message_data_from_kpm(arguments.kpm_file, tempdir) | |
except errors.KingPhisherInputValidationError as error: | |
logger.critical('failed to extract the kpm archive, input validation error: ' + error.message) | |
return | |
logger.debug('successfully extracted kpm archive') | |
if arguments.target_file: | |
target_file = arguments.target_file.name | |
else: | |
if not 'target_file' in message_config: | |
logger.critical('the kpm archive does not contain a target list, it needs to be set and re-exported from the client') | |
return | |
target_file = os.path.join(tempdir, message_config['target_file']) | |
config = dict(map(lambda x: ('mailer.' + x[0], x[1]), message_config.items())) | |
config['smtp_server'] = arguments.smtp_server | |
config['smtp_max_send_rate'] = arguments.smtp_max_send_rate | |
config['smtp_ssl_enable'] = arguments.smtp_use_ssl | |
config['mailer.target_file'] = target_file | |
for setting, setting_name in KPM_REQUIRED_SETTINGS.items(): | |
if not config.get(setting): | |
logger.critical("missing required option: '{0}'".format(setting_name)) | |
return | |
logger.debug('loading necessary server configuration settings') | |
config['server_config'] = rpc('config/get', ['server.require_id', 'server.secret_id', 'server.tracking_image']) | |
logger.debug('looking up id for campaign name: ' + arguments.campaign_name) | |
for row in rpc.remote_table('campaigns'): | |
if row.name == arguments.campaign_name: | |
config['campaign_id'] = row.id | |
config['campaign_name'] = arguments.campaign_name | |
break | |
if not 'campaign_id' in config: | |
logger.critical('failed to look up the id for campaign: ' + arguments.campaign_name) | |
mail_thread = CLIMailSenderThread(config, target_file, rpc, arguments.sms_carrier, arguments.sms_number) | |
mail_thread.sms_frequency = arguments.sms_frequency | |
missing_files = mail_thread.missing_files() | |
if missing_files: | |
logger.critical('the following files are missing from the message: ' + ', '.join(missing_files)) | |
return | |
if mail_thread.server_smtp_connect() != constants.ConnectionErrorReason.SUCCESS: | |
logger.critical('failed to connect to the smtp server') | |
return | |
mail_thread.start() | |
try: | |
while mail_thread.is_alive(): | |
mail_thread.join(3) | |
except KeyboardInterrupt: | |
if not mail_thread.paused.is_set(): | |
logger.warning('received ctrl+c, stopping the mail sending thread...') | |
mail_thread.stop() | |
logger.warning('the mail sending thread has stopped') | |
else: | |
mail_thread.send_sms("Finished sending all {0:,} messages for campaign: {1}".format(mail_thread.count_messages(), arguments.campaign_name), force=True) | |
def main(): | |
parser = argparse.ArgumentParser(description='King Phisher CLI Mailer', conflict_handler='resolve') | |
parser.add_argument('-v', '--version', action='version', version=parser.prog + ' Version: ' + version.version) | |
parser.add_argument('-L', '--log', dest='loglvl', action='store', choices=['DEBUG', 'INFO', 'WARNING'], default='INFO', help='set the logging level') | |
parser.add_argument('--logger', default='KingPhisher', help='specify the root logger') | |
parser.add_argument('-t', '--target', dest='target_file', type=argparse.FileType('r'), help='use the specified target file instead of the one in the KPM file') | |
parser.add_argument('-c', '--campaign', dest='campaign_name', help='the name of the campaign that the emails are being sent for') | |
parser.add_argument('--server-remote-port', dest='server_remote_port', type=int, default=80, help='the port that King Phisher is listening on') | |
parser.add_argument('--server-use-ssl', dest='server_use_ssl', action='store_true', default=False, help='connect to King Phisher with SSL') | |
parser.add_argument('--smtp-max-send-rate', dest='smtp_max_send_rate', type=float, default=45.0, help='the max messages to send per minute') | |
parser.add_argument('--smtp-use-ssl', dest='smtp_use_ssl', action='store_true', default=False, help='connect to SMTP with SSL') | |
parser.add_argument('--sms-carrier', dest='sms_carrier', help='the sms carrier to send alerts to') | |
parser.add_argument('--sms-frequency', dest='sms_frequency', type=int, default=500, help='the frequency of which to send sms alerts for sent messages') | |
parser.add_argument('--sms-number', dest='sms_number', help='the sms number to send alerts to') | |
parser.add_argument('server', help='the King Phisher server to use while sending') | |
parser.add_argument('smtp_server', help='the SMTP server to use for sending messages') | |
parser.add_argument('kpm_file', help='the kpm archive file to load the message from') | |
arguments = parser.parse_args() | |
logging.getLogger(arguments.logger).setLevel(logging.DEBUG) | |
console_log_handler = logging.StreamHandler() | |
console_log_handler.setLevel(getattr(logging, arguments.loglvl)) | |
console_log_handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s")) | |
logging.getLogger('').addHandler(console_log_handler) | |
logger = logging.getLogger('KingPhisher.Mailer.CLI') | |
if not arguments.campaign_name: | |
try: | |
arguments.campaign_name = raw_input('Campaign name: ') | |
except KeyboardInterrupt: | |
return 0 | |
server = arguments.server | |
if not server.startswith('ssh://'): | |
server = 'ssh://' + server | |
server = urlparse.urlparse(server) | |
username = server.username or pwd.getpwuid(os.getuid()).pw_name | |
local_port = random.randint(2000, 6000) | |
if (bool(arguments.sms_carrier) ^ bool(arguments.sms_number)): | |
logger.critical('both an sms carrier and an sms number must be specified, not one or the other') | |
return 0 | |
if arguments.sms_carrier and not sms.lookup_carrier_gateway(arguments.sms_carrier): | |
logger.critical('the sms carrier specified is invalid') | |
return 0 | |
ssh_forwarder = None | |
password = None | |
if server.hostname == 'localhost' or (utilities.is_valid_ip_address(server.hostname) and ipaddress.ip_address(server.hostname).is_loopback): | |
logger.info('skipping ssh for local server') | |
else: | |
connected = False | |
for _ in range(0, 3): | |
try: | |
password = getpass.getpass("{0}@{1}'s password: ".format(username, server.hostname)) | |
ssh_forwarder = SSHTCPForwarder((server.hostname, (server.port or 22)), username, password, ('127.0.0.1', arguments.server_remote_port), local_port) | |
except KeyboardInterrupt: | |
break | |
except paramiko.AuthenticationException as error: | |
logger.error('authenticating to the king phisher server failed: ' + error.message) | |
except socket.error: | |
logger.error('connecting to the king phisher server failed') | |
else: | |
connected = True | |
break | |
if not connected: | |
logger.error('not connected to the server, exiting') | |
return 0 | |
ssh_forwarder.start() | |
logger.info('connected to the server, started port forwarding') | |
rpc = client_rpc.KingPhisherRPCClient(('localhost', local_port), use_ssl=arguments.server_use_ssl) | |
try: | |
server_version_info = rpc('version') | |
except AdvancedHTTPServerRPCError as err: | |
if err.status == 401: | |
logger.warning('failed to authenticate to the remote king phisher service') | |
else: | |
logger.warning('failed to connect to the remote rpc server with http status: ' + str(err.status)) | |
if ssh_forwarder is not None: | |
ssh_forwarder.stop() | |
return 0 | |
except: | |
logger.warning('failed to connect to the remote rpc service') | |
if ssh_forwarder is not None: | |
ssh_forwarder.stop() | |
return 0 | |
server_rpc_api_version = server_version_info.get('rpc_api_version', -1) | |
logger.info("successfully connected to the king phisher server (version: {0} rpc api version: {1})".format(server_version_info['version'], server_rpc_api_version)) | |
if tuple(version.rpc_api_version) != tuple(server_rpc_api_version): | |
logger.error('the rpc versions are incompatible') | |
if ssh_forwarder is not None: | |
ssh_forwarder.stop() | |
return 0 | |
if password is None: | |
password = getpass.getpass("{0}@{1}'s password: ".format(username, server.hostname)) | |
login_result, login_reason = rpc.login(username, password) | |
if not login_result: | |
if login_reason == constants.ConnectionErrorReason.ERROR_INVALID_OTP: | |
otp_token = raw_input("{0}@{1}'s otp token: ".format(username, server.hostname)) | |
login_result, login_reason = rpc.login(username, password, otp=otp_token) | |
if not login_result: | |
logger.error('failed to authenticate to the remote king phisher service, reason: ' + login_reason) | |
if ssh_forwarder is not None: | |
ssh_forwarder.stop() | |
return 0 | |
tempdir = tempfile.mkdtemp() | |
logger.info('using temporary directory: ' + tempdir) | |
try: | |
cli_runner(arguments, tempdir, rpc) | |
except Exception as error: | |
logger.critical("an unhandled exception occurred: {0}.{1} ({2})".format(error.__class__.__module__, error.__class__.__name__, error.message), exc_info=True) | |
logger.debug('deleting temporary directory: ' + tempdir) | |
shutil.rmtree(tempdir) | |
if ssh_forwarder is not None: | |
ssh_forwarder.stop() | |
return 0 | |
if __name__ == '__main__': | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment