def _verify_key_value(self) -> None: """ Verify target host public key, raise exception if invalid/unknown Args: N/A Returns: None Raises: ScrapliConnectionNotOpened: if session is unopened/None ScrapliAuthenticationFailed: if host is in known hosts but public key does not match or cannot glean remote server key from session. """ if not self.session: raise ScrapliConnectionNotOpened known_hosts = SSHKnownHosts(self.plugin_transport_args.ssh_known_hosts_file) known_host_public_key = known_hosts.lookup(self._base_transport_args.host) remote_server_key = self.session.get_server_host_key() if remote_server_key is None: raise ScrapliAuthenticationFailed( f"failed gleaning remote server ssh key for host {self._base_transport_args.host}" ) remote_public_key = remote_server_key.export_public_key().split()[1].decode() if known_host_public_key["public_key"] != remote_public_key: raise ScrapliAuthenticationFailed( f"{self._base_transport_args.host} in known_hosts but public key does not match!" )
def channel_authenticate_netconf(self, auth_password: str, auth_private_key_passphrase: str) -> None: """ Handle SSH Authentication for transports that only operate "in the channel" (i.e. system) Args: auth_password: password to authenticate with auth_private_key_passphrase: passphrase for ssh key if necessary Returns: None Raises: ScrapliAuthenticationFailed: if password prompt seen more than twice ScrapliAuthenticationFailed: if passphrase prompt seen more than twice """ self.logger.debug("attempting in channel netconf authentication") password_count = 0 passphrase_count = 0 authenticate_buf = b"" with self._channel_lock(): while True: buf = self.read() authenticate_buf += buf.lower() self._capabilities_buf += buf self._ssh_message_handler(output=authenticate_buf) if b"password" in authenticate_buf: # clear the authentication buffer so we don't re-read the password prompt authenticate_buf = b"" password_count += 1 if password_count > 2: msg = "password prompt seen more than once, assuming auth failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) self.write(channel_input=auth_password, redacted=True) self.send_return() if b"enter passphrase for key" in authenticate_buf: # clear the authentication buffer so we don't re-read the passphrase prompt authenticate_buf = b"" passphrase_count += 1 if passphrase_count > 2: msg = "passphrase prompt seen more than once, assuming auth failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) self.write(channel_input=auth_private_key_passphrase, redacted=True) self.send_return() if self._authenticate_check_hello(buf=authenticate_buf): self.logger.info( "found start of server capabilities, authentication successful" ) return
async def open(self) -> None: self._pre_open_closing_log(closing=False) if self.plugin_transport_args.auth_strict_key: self.logger.debug( f"Attempting to validate {self._base_transport_args.host} public key is in known " f"hosts" ) self._verify_key() # we already fetched host/port/user from the user input and/or the ssh config file, so we # want to use those explicitly. likewise we pass config file we already found. set known # hosts and agent to None so we can not have an agent and deal w/ known hosts ourselves common_args = { "host": self._base_transport_args.host, "port": self._base_transport_args.port, "username": self.plugin_transport_args.auth_username, "known_hosts": None, "agent_path": None, "config": self.plugin_transport_args.ssh_config_file, } try: self.session = await asyncio.wait_for( connect( client_keys=self.plugin_transport_args.auth_private_key, password=self.plugin_transport_args.auth_password, preferred_auth=( "publickey", "keyboard-interactive", "password", ), **common_args, ), timeout=self._base_transport_args.timeout_socket, ) except PermissionDenied as exc: msg = "all authentication methods failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) from exc except asyncio.TimeoutError as exc: msg = "timed out opening connection to device" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) from exc if not self.session: raise ScrapliConnectionNotOpened if self.plugin_transport_args.auth_strict_key: self.logger.debug( f"Attempting to validate {self._base_transport_args.host} public key is in known " f"hosts and is valid" ) self._verify_key_value() self.stdin, self.stdout, _ = await self.session.open_session( term_type="xterm", encoding=None ) self._post_open_closing_log(closing=False)
def _verify_key(self) -> None: """ Verify target host public key, raise exception if invalid/unknown Args: N/A Returns: None Raises: ScrapliConnectionNotOpened: if session is unopened/None ScrapliAuthenticationFailed: if host is not in known hosts ScrapliAuthenticationFailed: if host is in known hosts but public key does not match """ if not self.session: raise ScrapliConnectionNotOpened known_hosts = SSHKnownHosts( self.plugin_transport_args.ssh_known_hosts_file) if self._base_transport_args.host not in known_hosts.hosts.keys(): raise ScrapliAuthenticationFailed( f"{self._base_transport_args.host} not in known_hosts!") remote_server_key = self.session.get_remote_server_key() remote_public_key = remote_server_key.get_base64() if known_hosts.hosts[self._base_transport_args. host]["public_key"] != remote_public_key: raise ScrapliAuthenticationFailed( f"{self._base_transport_args.host} in known_hosts but public key does not match!" )
def open(self) -> None: """ Open telnet channel Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if cant successfully authenticate """ self.session_lock.acquire() # establish session with "socket" timeout, then reset timeout to "transport" timeout try: telnet_session = ScrapliTelnet(host=self.host, port=self.port, timeout=self.timeout_socket) except ConnectionError as exc: msg = f"Failed to open telnet session to host {self.host}" if "connection refused" in str(exc).lower(): msg = f"Failed to open telnet session to host {self.host}, connection refused" raise ScrapliAuthenticationFailed(msg) telnet_session.timeout = self.timeout_transport LOG.debug(f"Session to host {self.host} spawned") self.session_lock.release() self._authenticate(telnet_session=telnet_session) if not self._telnet_isauthenticated(telnet_session=telnet_session): raise ScrapliAuthenticationFailed( f"Could not authenticate over telnet to host: {self.host}") LOG.debug(f"Authenticated to host {self.host} with password") self.session = telnet_session
def _verify_key(self) -> None: """ Verify target host public key, raise exception if invalid/unknown Args: N/A Returns: None Raises: ScrapliConnectionNotOpened: if session is unopened/None ScrapliAuthenticationFailed: if public key verification fails """ if not self.session: raise ScrapliConnectionNotOpened known_hosts = SSHKnownHosts( self.plugin_transport_args.ssh_known_hosts_file) if self._base_transport_args.host not in known_hosts.hosts.keys(): raise ScrapliAuthenticationFailed( f"{self._base_transport_args.host} not in known_hosts!") remote_server_key_info = self.session.hostkey() encoded_remote_server_key = remote_server_key_info[0] raw_remote_public_key = base64.encodebytes(encoded_remote_server_key) remote_public_key = raw_remote_public_key.replace(b"\n", b"").decode() if known_hosts.hosts[self._base_transport_args. host]["public_key"] != remote_public_key: raise ScrapliAuthenticationFailed( f"{self._base_transport_args.host} in known_hosts but public key does not match!" )
def _authenticate(self) -> bytes: """ Private method to check initial authentication when using pty_session Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if we see a password prompt more than once, or we got an unhandled EOF message """ self.session_lock.acquire() output = b"" password_count = 0 while True: try: new_output = self.session.read() output += new_output self.logger.debug( f"Attempting to authenticate. Read: {repr(new_output)}") except EOFError as exc: self._ssh_message_handler(output=output) # if _ssh_message_handler didn't raise any exception, we can raise the standard -- # did you disable strict key message/exception msg = ( f"Failed to open connection to host {self.host}. Do you need to disable " "`auth_strict_key`?") self.logger.critical(msg) self.session_lock.release() raise ScrapliAuthenticationFailed(msg) from exc if b"password:"******"" self.logger.info("Found password prompt, sending password") self.session.write(self.auth_password.encode()) self.session.write(self._comms_return_char.encode()) if password_count > 1: msg = ( "`password` seen multiple times during session establishment, " "likely failed authentication") self.session_lock.release() raise ScrapliAuthenticationFailed(msg) if b"<hello" in output.lower(): self.logger.info( "Found start of server capabilities, authentication successful" ) self._isauthenticated = True self.session_lock.release() return output
def open(self) -> None: """ Parent method to open session, authenticate and acquire shell If possible it is preferable to use the `_open_pipes` method, but we can only do this IF we can authenticate with public key authorization (because we don't have to spawn a PTY; if no public key we have to spawn PTY to deal w/ authentication prompts). IF we get a private key provided, use pipes method, otherwise we will just deal with `_open_pty`. `_open_pty` is less preferable because we have to spawn a PTY and cannot as easily tell if SSH authentication is successful. With `_open_pipes` we can read stderr which contains the output from the verbose flag for SSH -- this contains a message that indicates success of SSH auth. In the case of `_open_pty` we have to read from the channel directly like in the case of telnet... so it works, but its just a bit less desirable. Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if all authentication means fail """ self.session_lock.acquire() # If authenticating with private key prefer to use open pipes # _open_pipes uses subprocess Popen which is preferable to opening a pty # if _open_pipes fails and no password available, raise failure, otherwise try password auth if self.auth_private_key: open_pipes_result = self._open_pipes() if open_pipes_result: return if not self.auth_password or not self.auth_username: msg = ( f"Public key authentication to host {self.host} failed. Missing username or" " password unable to attempt password authentication.") LOG.critical(msg) raise ScrapliAuthenticationFailed(msg) # If public key auth fails or is not configured, open a pty session if not self._open_pty(): msg = f"Authentication to host {self.host} failed" LOG.critical(msg) raise ScrapliAuthenticationFailed(msg) if self.keepalive: self._session_keepalive()
def authenticate(self) -> None: """ Parent method to try all means of authentication Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if authentication fails """ if self.auth_private_key: self._authenticate_public_key() if self.isauthenticated(): LOG.debug(f"Authenticated to host {self.host} with public key auth") return if not self.auth_password or not self.auth_username: msg = ( f"Public key authentication to host {self.host} failed. Missing username or" " password unable to attempt password authentication." ) LOG.critical(msg) raise ScrapliAuthenticationFailed(msg) self._authenticate_password() if self.isauthenticated(): LOG.debug(f"Authenticated to host {self.host} with password")
def _pty_authenticate(self, pty_session: PtyProcess) -> None: """ Private method to check initial authentication when using pty_session Args: pty_session: PtyProcess session object Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if we receive an EOFError -- this usually indicates that host key checking is enabled and failed. """ self.session_lock.acquire() output = b"" while True: try: new_output = pty_session.read() output += new_output LOG.debug(f"Attempting to authenticate. Read: {repr(new_output)}") except EOFError: msg = self._pty_authentication_eof_handler(output) LOG.critical(msg) raise ScrapliAuthenticationFailed(msg) if self._comms_ansi: output = strip_ansi(output) if b"password" in output.lower(): LOG.info("Found password prompt, sending password") pty_session.write(self.auth_password.encode()) pty_session.write(self._comms_return_char.encode()) self.session_lock.release() break
def _escalate(self, escalate_priv: PrivilegeLevel) -> None: """ Escalate to the next privilege level up Args: escalate_priv: privilege level to escalate to Returns: None Raises: ScrapliAuthenticationFailed: if auth escalation timeout """ self._pre_escalate(escalate_priv=escalate_priv) if escalate_priv.escalate_auth is False: self.channel.send_input(channel_input=escalate_priv.escalate) else: try: super().send_interactive( interact_events=[ (escalate_priv.escalate, escalate_priv.escalate_prompt, False), (self.auth_secondary, escalate_priv.pattern, True), ], interaction_complete_patterns=[escalate_priv.pattern], ) except ScrapliTimeout as exc: raise ScrapliAuthenticationFailed( f"failed escalating privilege from '{escalate_priv.previous_priv}' to " f"'{escalate_priv.name}'. do you need to set an 'auth_secondary' password?" ) from exc
def _authenticate(self) -> None: """ Parent method to try all means of authentication Args: N/A Returns: None Raises: ScrapliConnectionNotOpened: if session is unopened/None ScrapliAuthenticationFailed: if auth fails """ if not self.session: raise ScrapliConnectionNotOpened if self.plugin_transport_args.auth_private_key: self._authenticate_public_key() if self.session.is_authenticated(): return if (not self.plugin_transport_args.auth_password or not self.plugin_transport_args.auth_username): msg = ( f"Failed to authenticate to host {self._base_transport_args.host} with private " f"key `{self.plugin_transport_args.auth_private_key}`. Unable to continue " "authentication, missing username, password, or both.") raise ScrapliAuthenticationFailed(msg) self._authenticate_password()
def _pty_authenticate(self, pty_session: PtyProcess) -> None: """ Private method to check initial authentication when using pty_session Args: pty_session: PtyProcess session object Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if we receive an EOFError -- this usually indicates that host key checking is enabled and failed. """ self.session_lock.acquire() output = b"" while True: try: output += pty_session.read() except EOFError: msg = f"Failed to open connection to host {self.host}" if b"Host key verification failed" in output: msg = f"Host key verification failed for host {self.host}" elif b"Operation timed out" in output: msg = f"Timed out connecting to host {self.host}" raise ScrapliAuthenticationFailed(msg) if self._comms_ansi: output = strip_ansi(output) if b"password" in output.lower(): LOG.debug("Found password prompt, sending password") pty_session.write(self.auth_password.encode()) pty_session.write(self._comms_return_char.encode()) self.session_lock.release() break
async def open(self) -> None: self._pre_open_closing_log(closing=False) try: fut = asyncio.open_connection( host=self._base_transport_args.host, port=self._base_transport_args.port ) self.stdout, self.stdin = await asyncio.wait_for( fut, timeout=self._base_transport_args.timeout_socket ) except ConnectionError as exc: msg = f"Failed to open telnet session to host {self._base_transport_args.host}" if "connection refused" in str(exc).lower(): msg = ( f"Failed to open telnet session to host {self._base_transport_args.host}, " "connection refused" ) raise ScrapliConnectionError(msg) from exc except (OSError, socket.gaierror) as exc: msg = ( f"Failed to open telnet session to host {self._base_transport_args.host} -- " "do you have a bad host/port?" ) raise ScrapliConnectionError(msg) from exc except asyncio.TimeoutError as exc: msg = "timed out opening connection to device" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) from exc await self._handle_control_chars() self._post_open_closing_log(closing=False)
def _open_pty(self) -> bool: """ Private method to open session with PtyProcess Args: N/A Returns: bool: True/False session was opened and authenticated Raises: ScrapliAuthenticationFailed: if all authentication means fail """ self.logger.info( f"Attempting to open session with the following command: {self.open_cmd}" ) self.session = PtyProcess.spawn(self.open_cmd) self.logger.debug(f"Session to host {self.host} spawned") if not self.auth_bypass: self._authenticate() else: self.logger.info("`auth_bypass` is True, bypassing authentication") # if we skip auth, we'll manually set _isauthenticated to True self._isauthenticated = True if not self._isauthenticated and not self._system_isauthenticated(): msg = f"Authentication to host {self.host} failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) self.logger.debug(f"Authenticated to host {self.host} with password") return True
def _authenticate(self) -> None: """ Parent method to try all means of authentication Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if auth fails """ if self.auth_private_key: self._authenticate_public_key() if self._isauthenticated(): LOG.debug(f"Authenticated to host {self.host} with public key auth") return if not self.auth_password or not self.auth_username: msg = ( f"Failed to authenticate to host {self.host} with private key " f"`{self.auth_private_key}`. Unable to continue authentication, " "missing username, password, or both." ) LOG.critical(msg) raise ScrapliAuthenticationFailed(msg) self._authenticate_password() if self._isauthenticated(): LOG.debug(f"Authenticated to host {self.host} with password") return self._authenticate_keyboard_interactive() if self._isauthenticated(): LOG.debug(f"Authenticated to host {self.host} with keyboard interactive")
def _authenticate(self) -> None: """ Private method to check initial authentication when using pty_session Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if we get EOF and _ssh_message_handler does not raise an explicit exception/message """ output = b"" prompt_pattern = get_prompt_pattern( prompt="", class_prompt=self._comms_prompt_pattern) while True: try: new_output = self.session.read() output += new_output self.logger.debug( f"Attempting to authenticate. Read: {repr(new_output)}") except EOFError as exc: self._ssh_message_handler(output=output) # if _ssh_message_handler didn't raise any exception, we can raise the standard # "did you disable strict key message/exception" msg = ( f"Failed to open connection to host {self.host}. Do you need to disable " "`auth_strict_key`?") self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) from exc # even if we dont hit EOF, we may still have hit a message we need to process such # as insecure key permissions self._ssh_message_handler(output=output) if self._comms_ansi or b"\x1B" in output: output = strip_ansi(output=output) if b"password" in output.lower(): self.logger.info("Found password prompt, sending password") self.session.write(self.auth_password.encode()) self.session.write(self._comms_return_char.encode()) break if b"enter passphrase for key" in output.lower(): self.logger.info( "Found key passphrase prompt, sending passphrase") self.session.write(self.auth_private_key_passphrase.encode()) self.session.write(self._comms_return_char.encode()) break channel_match = re.search(pattern=prompt_pattern, string=output) if channel_match: self.logger.info( "Found channel prompt, assuming that key based authentication has succeeded" ) # set _isauthenticated to true, we've already authenticated, don't re-check self._isauthenticated = True break
def _ssh_message_handler(self, output: bytes) -> None: # noqa: C901 """ Parse EOF messages from _pty_authenticate and create log/stack exception message Args: output: bytes output from _pty_authenticate Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if any errors are read in the output """ msg = "" if b"host key verification failed" in output.lower(): msg = "Host key verification failed" elif b"operation timed out" in output.lower( ) or b"connection timed out" in output.lower(): msg = "Timed out connecting to host" elif b"no route to host" in output.lower(): msg = "No route to host" elif b"no matching key exchange" in output.lower(): msg = "No matching key exchange found for host" key_exchange_pattern = re.compile( pattern=rb"their offer: ([a-z0-9\-,]*)", flags=re.M | re.I) offered_key_exchanges_match = re.search( pattern=key_exchange_pattern, string=output) if offered_key_exchanges_match: offered_key_exchanges = offered_key_exchanges_match.group( 1).decode() msg += f", their offer: {offered_key_exchanges}" elif b"no matching cipher" in output.lower(): msg = "No matching cipher found for host" ciphers_pattern = re.compile( pattern=rb"their offer: ([a-z0-9\-,]*)", flags=re.M | re.I) offered_ciphers_match = re.search(pattern=ciphers_pattern, string=output) if offered_ciphers_match: offered_ciphers = offered_ciphers_match.group(1).decode() msg += f", their offer: {offered_ciphers}" elif b"bad configuration" in output.lower(): msg = "Bad SSH configuration option(s) for host" configuration_pattern = re.compile( pattern=rb"bad configuration option: ([a-z0-9\+\=,]*)", flags=re.M | re.I) configuration_issue_match = re.search( pattern=configuration_pattern, string=output) if configuration_issue_match: configuration_issues = configuration_issue_match.group( 1).decode() msg += f", bad option(s): {configuration_issues}" elif b"WARNING: UNPROTECTED PRIVATE KEY FILE!" in output: msg = "Permissions for private key are too open, authentication failed!" elif b"could not resolve hostname" in output.lower(): msg = "Could not resolve address for host" if msg: self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg)
def _authenticate(self) -> None: """ Parent private method to handle telnet authentication Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if an EOFError is encountered; we in theory *did* open the connection, so we won't raise a ConnectionNotOpened here """ output = b"" # capture the start time of the authentication event; we also set a "return_interval" which # is 1/10 the timout_ops value, we will send a return character at roughly this interval if # there is no output on the channel. we do this because sometimes telnet needs a kick to get # it to prompt for auth -- particularity when connecting to terminal server/console port auth_start_time = datetime.now().timestamp() return_interval = self._timeout_ops / 10 return_attempts = 1 with self.session_lock: while True: try: new_output = self.session.read_eager() output += new_output self.logger.debug(f"Attempting to authenticate. Read: {repr(new_output)}") except EOFError as exc: # EOF means telnet connection is dead :( msg = f"Failed to open connection to host {self.host}. Connection lost." self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) from exc if self.username_prompt.lower().encode() in output.lower(): self.logger.info("Found username prompt, sending username") # if/when we see username, reset the output to empty byte string output = b"" self.session.write(self.auth_username.encode()) self.session.write(self._comms_return_char.encode()) elif self.password_prompt.lower().encode() in output.lower(): self.logger.info("Found password prompt, sending password") self.session.write(self.auth_password.encode()) self.session.write(self._comms_return_char.encode()) break elif not output: current_iteration_time = datetime.now().timestamp() if (current_iteration_time - auth_start_time) > ( return_interval * return_attempts ): self.logger.debug( "No username or password prompt found, sending return character..." ) self.session.write(self._comms_return_char.encode()) return_attempts += 1
async def _authenticate(self) -> None: """ Parent method to try all means of authentication Args: N/A Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if authentication fails """ common_args = { "host": self.host, "port": self.port, "username": self.auth_username, "known_hosts": None, "agent_path": None, } if self.transport_options: common_args.update(**self.transport_options) if self.auth_private_key: if await self._authenticate_private_key(common_args=common_args): self.logger.debug( f"Authenticated to host {self.host} with public key auth") return if not self.auth_password or not self.auth_username: msg = ( f"Failed to authenticate to host {self.host} with private key " f"`{self.auth_private_key}`. Unable to continue authentication, " "missing username, password, or both.") self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) if not await self._authenticate_password(common_args=common_args): msg = f"Authentication to host {self.host} failed" self.logger.critical(msg) self.session_lock.release() raise ScrapliAuthenticationFailed(msg) self.logger.debug(f"Authenticated to host {self.host} with password")
def open(self) -> None: """ Parent method to open session, authenticate and acquire shell Args: N/A Returns: N/A # noqa: DAR202 Raises: exc: if socket handshake fails ScrapliAuthenticationFailed: if all authentication means fail """ if not self.socket.socket_isalive(): self.socket.socket_open() self.session_lock.acquire() self.session = self.lib_session() self.set_timeout(self.timeout_transport) try: self.session.handshake(self.socket.sock) except Exception as exc: LOG.critical( f"Failed to complete handshake with host {self.host}; " f"Exception: {exc}") self.session_lock.release() raise exc if self.auth_strict_key: LOG.debug(f"Attempting to validate {self.host} public key") self._verify_key() LOG.debug(f"Session to host {self.host} opened") self.authenticate() if not self.isauthenticated(): msg = f"Authentication to host {self.host} failed" LOG.critical(msg) self.session_lock.release() raise ScrapliAuthenticationFailed(msg) self._open_channel() if self.keepalive: self._session_keepalive() self.session_lock.release()
def open(self) -> None: """ Open telnet channel Args: N/A Returns: N/A # noqa: DAR202 Raises: ConnectionNotOpened: if connection cant be opened ScrapliAuthenticationFailed: if cant successfully authenticate """ with self.session_lock: # establish session with "socket" timeout, then reset timeout to "transport" timeout try: self.session = ScrapliTelnet( host=self.host, port=self.port, timeout=self.timeout_socket ) except ConnectionError as exc: msg = f"Failed to open telnet session to host {self.host}" if "connection refused" in str(exc).lower(): msg = f"Failed to open telnet session to host {self.host}, connection refused" raise ConnectionNotOpened(msg) from exc self.session.timeout = self.timeout_transport self.logger.debug(f"Session to host {self.host} spawned") if not self.auth_bypass: self._authenticate() else: self.logger.info("`auth_bypass` is True, bypassing authentication") # if we skip auth, we'll manually set _isauthenticated to True self._isauthenticated = True if not self._isauthenticated and not self._telnet_isauthenticated(): raise ScrapliAuthenticationFailed( f"Could not authenticate over telnet to host: {self.host}" ) self.logger.debug(f"Authenticated to host {self.host} with password")
def _verify_key(self) -> None: """ Verify target host public key, raise exception if invalid/unknown Args: N/A Returns: None Raises: ScrapliAuthenticationFailed: if host is not in known hosts """ known_hosts = SSHKnownHosts(self.plugin_transport_args.ssh_known_hosts_file) if self._base_transport_args.host not in known_hosts.hosts.keys(): raise ScrapliAuthenticationFailed( f"{self._base_transport_args.host} not in known_hosts!" )
def _pty_authenticate(self, pty_session: PtyProcess) -> None: """ Private method to check initial authentication when using pty_session Args: pty_session: PtyProcess session object Returns: N/A # noqa: DAR202 Raises: ScrapliAuthenticationFailed: if we get EOF and _ssh_message_handler does not raise an explicit exception/message """ self.session_lock.acquire() output = b"" while True: try: new_output = pty_session.read() output += new_output self.logger.debug( f"Attempting to authenticate. Read: {repr(new_output)}") except EOFError: self._ssh_message_handler(output=output) # if _ssh_message_handler didn't raise any exception, we can raise the standard -- # did you disable strict key message/exception msg = ( f"Failed to open connection to host {self.host}. Do you need to disable " "`auth_strict_key`?") self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) if self._comms_ansi: output = strip_ansi(output) if b"password" in output.lower(): self.logger.info("Found password prompt, sending password") pty_session.write(self.auth_password.encode()) pty_session.write(self._comms_return_char.encode()) self.session_lock.release() break
async def open(self) -> None: """ Open telnet channel Args: N/A Returns: N/A # noqa: DAR202 Raises: ConnectionNotOpened: if connection cant be opened ScrapliAuthenticationFailed: if cant successfully authenticate """ try: output = await asyncio.wait_for(self._open(), timeout=self.timeout_socket) except asyncio.TimeoutError as exc: raise ConnectionNotOpened( "timed out opening connection and processing telnet control characters" ) from exc self.logger.debug(f"Session to host {self.host} spawned") if self.auth_bypass: self.logger.info("`auth_bypass` is True, bypassing authentication") # if we skip auth, we'll manually set _isauthenticated to True, the rest is up to the # user to handle, or things will just time out later... either way, not our problem :) self._isauthenticated = True return await self._authenticate(output=output) _telnet_isauthenticated = await self._telnet_isauthenticated() if not self._isauthenticated and not _telnet_isauthenticated: raise ScrapliAuthenticationFailed( f"Could not authenticate over telnet to host: {self.host}") self.logger.debug(f"Authenticated to host {self.host} with password")
def open(self) -> None: self._pre_open_closing_log(closing=False) if not self.socket: self.socket = Socket( host=self._base_transport_args.host, port=self._base_transport_args.port, timeout=self._base_transport_args.timeout_socket, ) if not self.socket.isalive(): self.socket.open() self.session = Session() self._set_timeout(value=self._base_transport_args.timeout_transport) try: self.session.handshake(self.socket.sock) except Exception as exc: self.logger.critical("failed to complete handshake with host") raise ScrapliConnectionNotOpened from exc if self.plugin_transport_args.auth_strict_key: self.logger.debug( f"attempting to validate {self._base_transport_args.host} public key" ) self._verify_key() self._authenticate() if not self.session.userauth_authenticated(): msg = "all authentication methods failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) self._open_channel() self._post_open_closing_log(closing=False)
def open(self) -> None: """ Parent method to open session, authenticate and acquire shell Args: N/A Returns: N/A # noqa: DAR202 Raises: Exception: if socket handshake fails ScrapliAuthenticationFailed: if all authentication means fail """ if not self.socket.socket_isalive(): self.socket.socket_open() try: self.session = ParamikoTransport(self.socket.sock) self.session.start_client() except Exception as exc: LOG.critical(f"Failed to complete handshake with host {self.host}; Exception: {exc}") raise exc if self.auth_strict_key: LOG.debug(f"Attempting to validate {self.host} public key") self._verify_key() self._authenticate() if not self._isauthenticated(): msg = f"Authentication to host {self.host} failed" LOG.critical(msg) raise ScrapliAuthenticationFailed(msg) self._open_channel()
def _pipes_isauthenticated(self, pipes_session: "PopenBytes") -> bool: """ Private method to check initial authentication when using subprocess.Popen Since we always run ssh with `-v` we can simply check the stderr (where verbose output goes) to see if `Authenticated to [our host]` is in the output. Args: pipes_session: Popen pipes session object Returns: bool: True/False session was authenticated Raises: ScrapliTimeout: if `Operation timed out` in stderr output ScrapliAuthenticationFailed: if private key permissions are too open """ if pipes_session.stderr is None: raise ScrapliTimeout(f"Could not read stderr while connecting to host {self.host}") output = b"" while True: output += pipes_session.stderr.read(65535) if f"Authenticated to {self.host}".encode() in output: self._isauthenticated = True return True if b"Operation timed out" in output: msg = f"Timed opening connection to host {self.host}" raise ScrapliTimeout(msg) if b"WARNING: UNPROTECTED PRIVATE KEY FILE!" in output: msg = ( f"Permissions for private key `{self.auth_private_key}` are too open, " "authentication failed!" ) raise ScrapliAuthenticationFailed(msg)
def channel_authenticate_telnet(self, auth_username: str = "", auth_password: str = "") -> None: """ Handle Telnet Authentication Args: auth_username: username to use for telnet authentication auth_password: password to use for telnet authentication Returns: None Raises: ScrapliAuthenticationFailed: if password prompt seen more than twice ScrapliAuthenticationFailed: if login prompt seen more than twice """ self.logger.debug("attempting in channel telnet authentication") username_count = 0 password_count = 0 authenticate_buf = b"" ( username_pattern, password_pattern, prompt_pattern, auth_start_time, return_interval, ) = self._pre_channel_authenticate_telnet() return_attempts = 1 with self._channel_lock(): while True: buf = self.read() if not buf: current_iteration_time = datetime.now().timestamp() if (current_iteration_time - auth_start_time) > ( return_interval * return_attempts ): self.send_return() return_attempts += 1 authenticate_buf += buf.lower() if re.search( pattern=username_pattern, string=authenticate_buf, ): # clear the authentication buffer so we don't re-read the username prompt authenticate_buf = b"" username_count += 1 if username_count > 2: msg = "username/login prompt seen more than once, assuming auth failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) self.write(channel_input=auth_username) self.send_return() if re.search( pattern=password_pattern, string=authenticate_buf, ): # clear the authentication buffer so we don't re-read the password prompt authenticate_buf = b"" password_count += 1 if password_count > 2: msg = "password prompt seen more than once, assuming auth failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) self.write(channel_input=auth_password, redacted=True) self.send_return() if re.search( pattern=prompt_pattern, string=authenticate_buf, ): return
def channel_authenticate_ssh( self, auth_password: str, auth_private_key_passphrase: str ) -> None: """ Handle SSH Authentication for transports that only operate "in the channel" (i.e. system) Args: auth_password: password to authenticate with auth_private_key_passphrase: passphrase for ssh key if necessary Returns: None Raises: ScrapliAuthenticationFailed: if password prompt seen more than twice ScrapliAuthenticationFailed: if passphrase prompt seen more than twice """ self.logger.debug("attempting in channel ssh authentication") password_count = 0 passphrase_count = 0 authenticate_buf = b"" ( password_pattern, passphrase_pattern, prompt_pattern, ) = self._pre_channel_authenticate_ssh() with self._channel_lock(): while True: buf = self.read() authenticate_buf += buf.lower() self._ssh_message_handler(output=authenticate_buf) if re.search( pattern=password_pattern, string=authenticate_buf, ): # clear the authentication buffer so we don't re-read the password prompt authenticate_buf = b"" password_count += 1 if password_count > 2: msg = "password prompt seen more than once, assuming auth failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) self.write(channel_input=auth_password, redacted=True) self.send_return() if re.search( pattern=passphrase_pattern, string=authenticate_buf, ): # clear the authentication buffer so we don't re-read the passphrase prompt authenticate_buf = b"" passphrase_count += 1 if passphrase_count > 2: msg = "passphrase prompt seen more than once, assuming auth failed" self.logger.critical(msg) raise ScrapliAuthenticationFailed(msg) self.write(channel_input=auth_private_key_passphrase, redacted=True) self.send_return() if re.search( pattern=prompt_pattern, string=authenticate_buf, ): return