def format_row_data(self, visit): msg_details = self.rpc.remote_table_row('messages', visit.message_id, cache=True) if not msg_details: return None visitor_ip = ipaddress.ip_address(visit.visitor_ip) geo_location = UNKNOWN_LOCATION_STRING if visitor_ip.is_loopback: geo_location = 'N/A (Loopback)' elif visitor_ip.is_private: geo_location = 'N/A (Private)' elif isinstance(visitor_ip, ipaddress.IPv6Address): geo_location = 'N/A (IPv6 Address)' else: if not visitor_ip in self._ips_for_georesolution: self._ips_for_georesolution[visitor_ip] = visit.first_visit elif self._ips_for_georesolution[visitor_ip] > visit.first_visit: self._ips_for_georesolution[visitor_ip] = visit.first_visit row = ( msg_details.target_email, str(visitor_ip), visit.visit_count, visit.visitor_details, geo_location, visit.first_visit, visit.last_visit ) return row
def format_node_data(self, node): geo_location = UNKNOWN_LOCATION_STRING visitor_ip = node['visitorIp'] if visitor_ip is None: visitor_ip = '' else: visitor_ip = ipaddress.ip_address(visitor_ip) if visitor_ip.is_loopback: geo_location = 'N/A (Loopback)' elif visitor_ip.is_private: geo_location = 'N/A (Private)' elif isinstance(visitor_ip, ipaddress.IPv6Address): geo_location = 'N/A (IPv6 Address)' elif node['visitorGeoloc']: geo_location = node['visitorGeoloc']['city'] row = ( node['message']['targetEmail'], str(visitor_ip), node['visitCount'], node['visitorDetails'], geo_location, node['firstVisit'], node['lastVisit'] ) return row
def rpc_login(self, session, username, password, otp=None): logger = logging.getLogger('KingPhisher.Server.Authentication') if not ipaddress.ip_address(self.client_address[0]).is_loopback: logger.warning("failed login request from {0} for user {1}, (invalid source address)".format(self.client_address[0], username)) raise ValueError('invalid source address for login') fail_default = (False, ConnectionErrorReason.ERROR_INVALID_CREDENTIALS, None) fail_otp = (False, ConnectionErrorReason.ERROR_INVALID_OTP, None) if not (username and password): logger.warning("failed login request from {0} for user {1}, (missing username or password)".format(self.client_address[0], username)) return fail_default if not self.server.forked_authenticator.authenticate(username, password): logger.warning("failed login request from {0} for user {1}, (authentication failed)".format(self.client_address[0], username)) return fail_default user = db_manager.get_row_by_id(session, db_models.User, username) if not user: logger.info('creating new user object with id: ' + username) user = db_models.User(id=username) session.add(user) session.commit() elif user.otp_secret: if otp is None: logger.debug("failed login request from {0} for user {1}, (missing otp)".format(self.client_address[0], username)) return fail_otp if not (isinstance(otp, str) and len(otp) == 6 and otp.isdigit()): logger.warning("failed login request from {0} for user {1}, (invalid otp)".format(self.client_address[0], username)) return fail_otp totp = pyotp.TOTP(user.otp_secret) now = datetime.datetime.now() if not otp in (totp.at(now + datetime.timedelta(seconds=offset)) for offset in (0, -30, 30)): logger.warning("failed login request from {0} for user {1}, (invalid otp)".format(self.client_address[0], username)) return fail_otp logger.info("successful login request from {0} for user {1}".format(self.client_address[0], username)) return True, ConnectionErrorReason.SUCCESS, self.server.session_manager.put(username)
def test_geoip_raw_geolocation(self): loc = geoip.GeoLocation(GEO_TEST_IP) loc_raw = geoip.GeoLocation(GEO_TEST_IP, result=geoip.lookup(GEO_TEST_IP)) for field in geoip.DB_RESULT_FIELDS: self.assertEqual(getattr(loc, field), getattr(loc_raw, field)) self.assertIsInstance(loc.ip_address, ipaddress.IPv4Address) self.assertEqual(loc.ip_address, ipaddress.ip_address(GEO_TEST_IP))
def lookup(ip, lang='en'): """ Lookup the geo location information for the specified IP from the configured GeoLite2 City database. :param str ip: The IP address to look up the information for. :param str lang: The language to prefer for regional names. :return: The geo location information as a dict. The keys are the values of :py:data:`.DB_RESULT_FIELDS`. :rtype: dict """ if not _geoip_db: raise RuntimeError('the geoip database has not been initialized yet') lang = (lang or 'en') if isinstance(ip, str): ip = ipaddress.ip_address(ip) if isinstance(ip, ipaddress.IPv6Address): raise TypeError('ipv6 addresses are not supported at this time') if ip.is_loopback or ip.is_private: raise RuntimeError('the specified IP address is not a public IP address') with _geoip_db_lock: city = _geoip_db.city(str(ip)) result = {} result['city'] = city.city.names.get(lang) result['continent'] = city.continent.names.get(lang) result['coordinates'] = (city.location.latitude, city.location.longitude) result['country'] = city.country.names.get(lang) result['postal_code'] = city.postal.code result['time_zone'] = city.location.time_zone return result
def campaign_visits_to_geojson(rpc, campaign_id, geojson_file): """ Export the geo location information for all the visits of a campaign into the `GeoJSON <http://geojson.org/>`_ format. :param rpc: The connected RPC instance to load the information with. :type rpc: :py:class:`.KingPhisherRPCClient` :param campaign_id: The ID of the campaign to load the information for. :param str geojson_file: The destination file for the GeoJSON data. """ ips_for_georesolution = {} ip_counter = collections.Counter() for visit in rpc.remote_table('visits', query_filter={'campaign_id': campaign_id}): ip_counter.update((visit.visitor_ip,)) visitor_ip = ipaddress.ip_address(visit.visitor_ip) if not isinstance(visitor_ip, ipaddress.IPv4Address): continue if visitor_ip.is_loopback or visitor_ip.is_private: continue if not visitor_ip in ips_for_georesolution: ips_for_georesolution[visitor_ip] = visit.first_visit elif ips_for_georesolution[visitor_ip] > visit.first_visit: ips_for_georesolution[visitor_ip] = visit.first_visit ips_for_georesolution = [ip for (ip, _) in sorted(ips_for_georesolution.items(), key=lambda x: x[1])] locations = {} for ip_addresses in iterutils.chunked(ips_for_georesolution, 50): locations.update(rpc.geoip_lookup_multi(ip_addresses)) points = [] for ip, location in locations.items(): if not (location.coordinates and location.coordinates[0] and location.coordinates[1]): continue points.append(geojson.Feature(geometry=location, properties={'count': ip_counter[ip], 'ip-address': ip})) feature_collection = geojson.FeatureCollection(points) with open(geojson_file, 'w') as file_h: json_ex.dump(feature_collection, file_h)
def from_ip_address(cls, ip_address): ip_address = ipaddress.ip_address(ip_address) if ip_address.is_private: return result = geoip.lookup(ip_address) if result is None: return return cls(**result)
def do_OPTIONS(self): available_methods = list(x[3:] for x in dir(self) if x.startswith('do_')) if 'RPC' in available_methods: if not len(self.rpc_handler_map) or not ipaddress.ip_address(self.client_address[0]).is_loopback: self.logger.debug('removed RPC method from Allow header in OPTIONS reply') available_methods.remove('RPC') self.send_response(200) self.send_header('Content-Length', 0) self.send_header('Allow', ', '.join(available_methods)) self.end_headers()
def from_ip_address(cls, ip_address): ip_address = ipaddress.ip_address(ip_address) if ip_address.is_private: return try: result = geoip.lookup(ip_address) except geoip2.errors.AddressNotFoundError: result = None if result is None: return return cls(**result)
def _resolve_geolocations(self, all_ips): geo_locations = {} public_ips = [] for visitor_ip in all_ips: ip = ipaddress.ip_address(visitor_ip) if ip.is_private or ip.is_loopback: continue public_ips.append(visitor_ip) public_ips.sort() for ip_chunk in iterutils.chunked(public_ips, 100): geo_locations.update(self.rpc.geoip_lookup_multi(ip_chunk)) return geo_locations
def expand_macros(self, value, ip, domain, sender): """ Expand a string based on the macros it contains as specified by section 7 of RFC 7208. :param str value: The string containing macros to expand. :param ip: The IP address to use when expanding macros. :type ip: str, :py:class:`ipaddress.IPv4Address`, :py:class:`ipaddress.IPv6Address` :param str domain: The domain name to use when expanding macros. :param str sender: The email address of the sender to use when expanding macros. :return: The string with the interpreted macros replaced within it. :rtype: str """ if isinstance(ip, str): ip = ipaddress.ip_address(ip) macro_table = { 's': sender, 'l': sender.split('@', 1)[0], 'o': sender.split('@', 1)[1], 'd': domain, 'i': (str(ip) if isinstance(ip, ipaddress.IPv4Address) else '.'.join(ip.exploded.replace(':', ''))), #'p' 'v': ('in-addr' if isinstance(ip, ipaddress.IPv4Address) else 'ip6'), 'h': self.helo_domain } for escape in (('%%', '%'), ('%-', '%20'), ('%_', ' ')): value = value.replace(*escape) end = 0 result = '' for match in MACRO_REGEX.finditer(value): result += value[end:match.start()] macro_type = match.group(1) macro_digit = int(match.group(2) or 128) macro_reverse = (match.group(3) == 'r') macro_delimiter = (match.group(4) or '.') if not macro_type in macro_table: raise SPFPermError("unsupported macro type: '{0}'".format(macro_type)) macro_value = macro_table[macro_type] macro_value = macro_value.split(macro_delimiter) if macro_reverse: macro_value.reverse() macro_value = macro_value[-macro_digit:] macro_value = '.'.join(macro_value) result += macro_value end = match.end() result += value[end:] return result
def do_OPTIONS(self): available_methods = list(x[3:] for x in dir(self) if x.startswith('do_')) if 'RPC' in available_methods: if not len(self.rpc_handler_map) or not ipaddress.ip_address( self.client_address[0]).is_loopback: self.logger.debug( 'removed RPC method from Allow header in OPTIONS reply') available_methods.remove('RPC') self.send_response(200) self.send_header('Content-Length', 0) self.send_header('Allow', ', '.join(available_methods)) self.end_headers()
def check_authorization(self): # don't require authentication for non-RPC requests if self.command != 'RPC': return True if not ipaddress.ip_address(self.client_address[0]).is_loopback: return False if self.path in ('/version', '/login'): return True self.rpc_session = self.server.session_manager.get(self.rpc_session_id) if not isinstance(self.rpc_session, aaa.AuthenticatedSession): return False return True
def guess_smtp_server_address(host, forward_host=None): """ Guess the IP address of the SMTP server that will be connected to given the SMTP host information and an optional SSH forwarding host. If a hostname is in use it will be resolved to an IP address, either IPv4 or IPv6 and in that order. If a hostname resolves to multiple IP addresses, None will be returned. This function is intended to guess the SMTP servers IP address given the client configuration so it can be used for SPF record checks. :param str host: The SMTP server that is being connected to. :param str forward_host: An optional host that is being used to tunnel the connection. :return: The IP address of the SMTP server. :rtype: None, :py:class:`ipaddress.IPv4Address`, :py:class:`ipaddress.IPv6Address` """ host = host.rsplit(':', 1)[0] if ipaddress.is_valid(host): ip = ipaddress.ip_address(host) if not ip.is_loopback: return ip else: info = None for family in (socket.AF_INET, socket.AF_INET6): try: info = socket.getaddrinfo(host, 1, family) except socket.gaierror: continue info = set(list([r[4][0] for r in info])) if len(info) != 1: return break if info: ip = ipaddress.ip_address(info.pop()) if not ip.is_loopback: return ip if forward_host: return guess_smtp_server_address(forward_host) return
def wrapped(handler, params): client_ip = ipaddress.ip_address(handler.client_address[0]) config = handler.config if not config.get('server.rest_api.enabled'): logger.warning( "denying REST API request from {0} (REST API is disabled)". format(client_ip)) handler.respond_unauthorized() return networks = config.get_if_exists('server.rest_api.networks') if networks is not None: if isinstance(networks, str): networks = (networks, ) found = False for network in networks: if client_ip in ipaddress.ip_network(network, strict=False): found = True break if not found: logger.warning( "denying REST API request from {0} (origin is from an unauthorized network)" .format(client_ip)) handler.respond_unauthorized() return if not handler.config.get('server.rest_api.token'): logger.warning( "denying REST API request from {0} (configured token is invalid)" .format(client_ip)) handler.respond_unauthorized() return if config.get('server.rest_api.token') != handler.get_query('token'): logger.warning( "denying REST API request from {0} (invalid authentication token)" .format(client_ip)) handler.respond_unauthorized() return response = dict(result=handle_function(handler, params)) response = json.dumps(response, sort_keys=True, indent=2, separators=(',', ': ')) response = response.encode('utf-8') handler.send_response(200) handler.send_header('Content-Type', 'application/json') handler.send_header('Content-Length', str(len(response))) handler.end_headers() handler.wfile.write(response) return
def campaign_visits_to_geojson(rpc, campaign_id, geojson_file): """ Export the geo location information for all the visits of a campaign into the `GeoJSON <http://geojson.org/>`_ format. :param rpc: The connected RPC instance to load the information with. :type rpc: :py:class:`.KingPhisherRPCClient` :param campaign_id: The ID of the campaign to load the information for. :param str geojson_file: The destination file for the GeoJSON data. """ ips_for_georesolution = {} ip_counter = collections.Counter() for visit in rpc.remote_table('visits', query_filter={'campaign_id': campaign_id}): ip_counter.update((visit.visitor_ip, )) visitor_ip = ipaddress.ip_address(visit.visitor_ip) if not isinstance(visitor_ip, ipaddress.IPv4Address): continue if visitor_ip.is_loopback or visitor_ip.is_private: continue if not visitor_ip in ips_for_georesolution: ips_for_georesolution[visitor_ip] = visit.first_visit elif ips_for_georesolution[visitor_ip] > visit.first_visit: ips_for_georesolution[visitor_ip] = visit.first_visit ips_for_georesolution = [ ip for (ip, _) in sorted(ips_for_georesolution.items(), key=lambda x: x[1]) ] locations = {} for ip_addresses in iterutils.chunked(ips_for_georesolution, 50): locations.update(rpc.geoip_lookup_multi(ip_addresses)) points = [] for ip, location in locations.items(): if not (location.coordinates and location.coordinates[0] and location.coordinates[1]): continue points.append( geojson.Feature(geometry=location, properties={ 'count': ip_counter[ip], 'ip-address': ip })) feature_collection = geojson.FeatureCollection(points) with open(geojson_file, 'w') as file_h: serializers.JSON.dump(feature_collection, file_h, pretty=True)
def rpc_version(self): """ Get the version information of the server. This returns a dictionary with keys of version, version_info and rpc_api_version. These values are provided for the client to determine compatibility. :return: A dictionary with version information. :rtype: dict """ assert ipaddress.ip_address(self.client_address[0]).is_loopback vinfo = { 'rpc_api_version': version.rpc_api_version, 'version': version.version, 'version_info': version.version_info._asdict() } return vinfo
def __init__(self, ip, domain, sender=None, timeout=DEFAULT_DNS_TIMEOUT): """ :param ip: The IP address of the host sending the message. :type ip: str, :py:class:`ipaddress.IPv4Address`, :py:class:`ipaddress.IPv6Address` :param str domain: The domain to check the SPF policy of. :param str sender: The "MAIL FROM" identity of the message being sent. :param int timeout: The timeout for DNS queries. """ if isinstance(ip, str): ip = ipaddress.ip_address(ip) self.ip_address = ip self.domain = domain self.helo_domain = 'unknown' sender = (sender or 'postmaster') if not '@' in sender: sender = sender + '@' + self.domain self.sender = sender self.records = collections.OrderedDict() """ A :py:class:`collections.OrderedDict` of all the SPF records that were resolved. This would be any records resolved due to an "include" directive in addition to the top level domain. """ self.matches = [] """ A list of :py:class:`.SPFMatch` instances showing the path traversed to identify a matching directive. Multiple entries in this list are present when include directives are used and a match is found within the body of one. The list is ordered from the top level domain to the matching record. """ # dns lookup limit per https://tools.ietf.org/html/rfc7208#section-4.6.4 self.query_limit = MAX_QUERIES self.query_limit_void = MAX_QUERIES_VOID self.policy = None self.timeout = timeout """ The human readable policy result, one of the :py:class:`.SPFResult` constants`. """ self._policy_checked = False self.logger = logging.getLogger( 'KingPhisher.SPF.SenderPolicyFramework')
def format_node_data(self, node): geo_location = UNKNOWN_LOCATION_STRING visitor_ip = node['visitorIp'] if visitor_ip is None: visitor_ip = '' else: visitor_ip = ipaddress.ip_address(visitor_ip) if visitor_ip.is_loopback: geo_location = 'N/A (Loopback)' elif visitor_ip.is_private: geo_location = 'N/A (Private)' elif isinstance(visitor_ip, ipaddress.IPv6Address): geo_location = 'N/A (IPv6 Address)' elif node['visitorGeoloc']: geo_location = node['visitorGeoloc']['city'] row = (node['message']['targetEmail'], str(visitor_ip), node['visitCount'], node['visitorDetails'], geo_location, node['firstVisit'], node['lastVisit']) return row
def __init__(self, ip, lang='en', result=None): """ :param str ip: The IP address to look up geographic location data for. :param str lang: The language to prefer for regional names. :param dict result: A raw query result from a previous call to :py:func:`.lookup`. """ if isinstance(ip, str): ip = ipaddress.ip_address(ip) if not result: result = lookup(ip, lang=lang) self.ip_address = ip """The :py:class:`~ipaddress.IPv4Address` which this geographic location data describes.""" for field in DB_RESULT_FIELDS: if field not in result: raise RuntimeError('the retrieved information is missing required data field: ' + field) if field in ('coordinates',): continue setattr(self, field, result[field]) self.coordinates = Coordinates(latitude=result['coordinates'][0], longitude=result['coordinates'][1])
def check_authorization(self): # don't require authentication for non-RPC requests cmd = self.command if cmd == 'GET': # check if the GET request is to open a web socket if 'upgrade' not in self.headers: return True elif cmd != 'RPC': return True if not ipaddress.ip_address(self.client_address[0]).is_loopback: return False # the only two RPC methods that do not require authentication if self.path in ('/login', '/version'): return True self.rpc_session = self.server.session_manager.get(self.rpc_session_id) if not isinstance(self.rpc_session, aaa.AuthenticatedSession): return False return True
def rpc_login(handler, session, username, password, otp=None): logger = logging.getLogger('KingPhisher.Server.Authentication') if not ipaddress.ip_address(handler.client_address[0]).is_loopback: logger.warning("failed login request from {0} for user {1}, (invalid source address)".format(handler.client_address[0], username)) raise ValueError('invalid source address for login') fail_default = (False, ConnectionErrorReason.ERROR_INVALID_CREDENTIALS, None) fail_otp = (False, ConnectionErrorReason.ERROR_INVALID_OTP, None) if not (username and password): logger.warning("failed login request from {0} for user {1}, (missing username or password)".format(handler.client_address[0], username)) return fail_default if not handler.server.forked_authenticator.authenticate(username, password): logger.warning("failed login request from {0} for user {1}, (authentication failed)".format(handler.client_address[0], username)) return fail_default user = session.query(db_models.User).filter_by(name=username).first() if not user: logger.info('creating new user object with name: ' + username) user = db_models.User(name=username) elif user.has_expired: logger.warning("failed login request from {0} for user {1}, (user has expired)".format(handler.client_address[0], username)) return fail_default elif user.otp_secret: if otp is None: logger.debug("failed login request from {0} for user {1}, (missing otp)".format(handler.client_address[0], username)) return fail_otp if not (isinstance(otp, str) and len(otp) == 6 and otp.isdigit()): logger.warning("failed login request from {0} for user {1}, (invalid otp)".format(handler.client_address[0], username)) return fail_otp totp = pyotp.TOTP(user.otp_secret) now = datetime.datetime.now() if otp not in (totp.at(now + datetime.timedelta(seconds=offset)) for offset in (0, -30, 30)): logger.warning("failed login request from {0} for user {1}, (invalid otp)".format(handler.client_address[0], username)) return fail_otp user.last_login = db_models.current_timestamp() session.add(user) session.commit() session_id = handler.server.session_manager.put(user.id) logger.info("successful login request from {0} for user {1}".format(handler.client_address[0], username)) signals.send_safe('rpc-user-logged-in', logger, handler, session=session_id, name=username) return True, ConnectionErrorReason.SUCCESS, session_id
def rpc_login(handler, session, username, password, otp=None): logger = logging.getLogger('KingPhisher.Server.Authentication') if not ipaddress.ip_address(handler.client_address[0]).is_loopback: logger.warning("failed login request from {0} for user {1}, (invalid source address)".format(handler.client_address[0], username)) raise ValueError('invalid source address for login') fail_default = (False, ConnectionErrorReason.ERROR_INVALID_CREDENTIALS, None) fail_otp = (False, ConnectionErrorReason.ERROR_INVALID_OTP, None) if not (username and password): logger.warning("failed login request from {0} for user {1}, (missing username or password)".format(handler.client_address[0], username)) return fail_default if not handler.server.forked_authenticator.authenticate(username, password): logger.warning("failed login request from {0} for user {1}, (authentication failed)".format(handler.client_address[0], username)) return fail_default user = session.query(db_models.User).filter_by(name=username).first() if not user: logger.info('creating new user object with name: ' + username) user = db_models.User(name=username) elif user.has_expired: logger.warning("failed login request from {0} for user {1}, (user has expired)".format(handler.client_address[0], username)) return fail_default elif user.otp_secret: if otp is None: logger.debug("failed login request from {0} for user {1}, (missing otp)".format(handler.client_address[0], username)) return fail_otp if not (isinstance(otp, str) and len(otp) == 6 and otp.isdigit()): logger.warning("failed login request from {0} for user {1}, (invalid otp)".format(handler.client_address[0], username)) return fail_otp totp = pyotp.TOTP(user.otp_secret) now = datetime.datetime.now() if otp not in (totp.at(now + datetime.timedelta(seconds=offset)) for offset in (0, -30, 30)): logger.warning("failed login request from {0} for user {1}, (invalid otp)".format(handler.client_address[0], username)) return fail_otp user.last_login = db_models.current_timestamp() session.add(user) session.commit() session_id = handler.server.session_manager.put(user) logger.info("successful login request from {0} for user {1}".format(handler.client_address[0], username)) signals.send_safe('rpc-user-logged-in', logger, handler, session=session_id, name=username) return True, ConnectionErrorReason.SUCCESS, session_id
def __init__(self, ip, lang='en', result=None): """ :param str ip: The IP address to look up geographic location data for. :param str lang: The language to prefer for regional names. :param dict result: A raw query result from a previous call to :py:func:`.lookup`. """ if isinstance(ip, str): ip = ipaddress.ip_address(ip) if not result: result = lookup(ip, lang=lang) self.ip_address = ip """The :py:class:`~ipaddress.IPv4Address` which this geographic location data describes.""" for field in DB_RESULT_FIELDS: if not field in result: raise RuntimeError( 'the retrieved information is missing required data') if field in ('coordinates', ): continue setattr(self, field, result[field]) self.coordinates = Coordinates(latitude=result['coordinates'][0], longitude=result['coordinates'][1])
def rpc_version(handler): """ Get the version information of the server. This returns a dictionary with keys of version, version_info and rpc_api_version. These values are provided for the client to determine compatibility. :return: A dictionary with version information. :rtype: dict """ if not ipaddress.ip_address(handler.client_address[0]).is_loopback: message = "an rpc request to /version was received from non-loopback IP address: {0}".format(handler.client_address[0]) rpc_logger.error(message) raise errors.KingPhisherAPIError(message) vinfo = { 'rpc_api_version': version.rpc_api_version, 'version': version.version, 'version_info': version.version_info._asdict() } return vinfo
def dispatch(self, handler): """ A method that is suitable for use as a :py:attr:`~advancedhttpserver.RequestHandler.web_socket_handler`. :param handler: The current request handler instance. :type handler: :py:class:`~king_phisher.server.server.KingPhisherRequestHandler` """ if not ipaddress.ip_address(handler.client_address[0]).is_loopback: return prefix = '/' if self.config.get('server.vhost_directories'): prefix += handler.vhost + '/' request_path = handler.path if request_path.startswith(prefix): request_path = request_path[len(prefix):] if request_path == '_/ws/events/json': EventSocket(handler, self) return handler.respond_not_found() return
def __init__(self, ip, domain, sender=None): """ :param ip: The IP address of the host sending the message. :type ip: str, :py:class:`ipaddress.IPv4Address`, :py:class:`ipaddress.IPv6Address` :param str domain: The domain to check the SPF policy of. :param str sender: The "MAIL FROM" identity of the message being sent. """ if isinstance(ip, str): ip = ipaddress.ip_address(ip) self.ip_address = ip self.domain = domain self.helo_domain = 'unknown' sender = (sender or 'postmaster') if not '@' in sender: sender = sender + '@' + self.domain self.sender = sender self.records = collections.OrderedDict() """ A :py:class:`collections.OrderedDict` of all the SPF records that were resolved. This would be any records resolved due to an "include" directive in addition to the top level domain. """ self.matches = [] """ A list of :py:class:`.SPFMatch` instances showing the path traversed to identify a matching directive. Multiple entries in this list are present when include directives are used and a match is found within the body of one. The list is ordered from the top level domain to the matching record. """ # dns lookup limit per https://tools.ietf.org/html/rfc7208#section-4.6.4 self.query_limit = MAX_QUERIES self.query_limit_void = MAX_QUERIES_VOID self.policy = None """ The human readable policy result, one of the :py:class:`.SPFResult` constants`. """ self._policy_checked = False self.logger = logging.getLogger('KingPhisher.SPF.SenderPolicyFramework')
def wrapped(handler, params): client_ip = ipaddress.ip_address(handler.client_address[0]) config = handler.config if not config.get("server.rest_api.enabled"): logger.warning("denying REST API request from {0} (REST API is disabled)".format(client_ip)) handler.respond_unauthorized() return networks = config.get_if_exists("server.rest_api.networks") if networks is not None: if isinstance(networks, str): networks = (networks,) found = False for network in networks: if client_ip in ipaddress.ip_network(network, strict=False): found = True break if not found: logger.warning( "denying REST API request from {0} (origin is from an unauthorized network)".format(client_ip) ) handler.respond_unauthorized() return if not handler.config.get("server.rest_api.token"): logger.warning("denying REST API request from {0} (configured token is invalid)".format(client_ip)) handler.respond_unauthorized() return if config.get("server.rest_api.token") != handler.get_query("token"): logger.warning("denying REST API request from {0} (invalid authentication token)".format(client_ip)) handler.respond_unauthorized() return response = dict(result=handle_function(handler, params)) response = json.dumps(response, sort_keys=True, indent=2, separators=(",", ": ")) response = response.encode("utf-8") handler.send_response(200) handler.send_header("Content-Type", "application/json") handler.send_header("Content-Length", str(len(response))) handler.end_headers() handler.wfile.write(response) return
def format_row_data(self, visit): msg_details = self.rpc.remote_table_row('messages', visit.message_id, cache=True) if not msg_details: return None visitor_ip = ipaddress.ip_address(visit.visitor_ip) geo_location = UNKNOWN_LOCATION_STRING if visitor_ip.is_loopback: geo_location = 'N/A (Loopback)' elif visitor_ip.is_private: geo_location = 'N/A (Private)' elif isinstance(visitor_ip, ipaddress.IPv6Address): geo_location = 'N/A (IPv6 Address)' else: if not visitor_ip in self._ips_for_georesolution: self._ips_for_georesolution[visitor_ip] = visit.first_visit elif self._ips_for_georesolution[visitor_ip] > visit.first_visit: self._ips_for_georesolution[visitor_ip] = visit.first_visit row = (msg_details.target_email, str(visitor_ip), visit.visit_count, visit.visitor_details, geo_location, visit.first_visit, visit.last_visit) return row
def __init__(self, ip, domain, sender=None, spf_records=None): """ :param ip: The IP address of the host sending the message. :type ip: str, :py:class:`ipaddress.IPv4Address`, :py:class:`ipaddress.IPv6Address` :param str domain: The domain to check the SPF policy of. :param str sender: The "MAIL FROM" identity of the message being sent. """ if isinstance(ip, str): ip = ipaddress.ip_address(ip) self.ip_address = ip self.domain = domain self.helo_domain = 'unknown' sender = (sender or 'postmaster') if not '@' in sender: sender = sender + '@' + self.domain self.sender = sender self.spf_records = (spf_records or []) self.spf_record_id = -1 # dns lookup limit per https://tools.ietf.org/html/rfc7208#section-4.6.4 self.query_limit = 10 self.policy = None self._policy_checked = False self.logger = logging.getLogger('KingPhisher.SPF.SenderPolicyFramework')