Skip to content

Instantly share code, notes, and snippets.

@zeroSteiner
Last active January 27, 2022 00:36
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zeroSteiner/2b9b159c184ffe171339 to your computer and use it in GitHub Desktop.
Save zeroSteiner/2b9b159c184ffe171339 to your computer and use it in GitHub Desktop.
King Phisher CLI Mail Utility
#!/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