def reconnect(s: 'SSH_Socket', gex_alg: str) -> bool: if s.is_connected(): return True err = s.connect() if err is not None: return False unused = None # pylint: disable=unused-variable unused2 = None # pylint: disable=unused-variable unused, unused2, err = s.get_banner() if err is not None: s.close() return False # Parse the server's initial KEX. packet_type = 0 # pylint: disable=unused-variable packet_type, payload = s.read_packet(2) kex = SSH2_Kex.parse(payload) # Send our KEX using the specified group-exchange and most of the # server's own values. client_kex = SSH2_Kex(os.urandom(16), [gex_alg], kex.key_algorithms, kex.client, kex.server, False, 0) s.write_byte(Protocol.MSG_KEXINIT) client_kex.write(s) s.send_packet() return True
def send_algorithms(self) -> None: '''Sends the list of supported host keys, key exchanges, ciphers, and MACs. Emulates OpenSSH v8.2.''' key_exchanges = [ 'curve25519-sha256', '*****@*****.**', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group14-sha256' ] hostkeys = [ 'rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ssh-ed25519' ] ciphers = [ '*****@*****.**', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', '*****@*****.**', '*****@*****.**' ] macs = [ '*****@*****.**', '*****@*****.**', '*****@*****.**', '*****@*****.**', '*****@*****.**', '*****@*****.**', '*****@*****.**', 'hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1' ] compressions = ['none', '*****@*****.**'] languages = [''] kexparty = SSH2_KexParty(ciphers, macs, compressions, languages) kex = SSH2_Kex(os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0) self.write_byte(Protocol.MSG_KEXINIT) kex.write(self) self.send_packet()
def kex(ssh_audit): kex_algs, key_algs = [], [] enc, mac, compression, languages = [], [], ['none'], [] cli = SSH2_KexParty(enc, mac, compression, languages) enc, mac, compression, languages = [], [], ['none'], [] srv = SSH2_KexParty(enc, mac, compression, languages) cookie = os.urandom(16) kex = SSH2_Kex(cookie, kex_algs, key_algs, cli, srv, 0) return kex
def reconnect(out: 'OutputBuffer', s: 'SSH_Socket', kex: 'SSH2_Kex', gex_alg: str) -> bool: if s.is_connected(): return True err = s.connect() if err is not None: out.v(err, write_now=True) return False _, _, err = s.get_banner() if err is not None: out.v(err, write_now=True) s.close() return False # Send our KEX using the specified group-exchange and most of the # server's own values. s.send_kexinit(key_exchanges=[gex_alg], hostkeys=kex.key_algorithms, ciphers=kex.server.encryption, macs=kex.server.mac, compressions=kex.server.compression, languages=kex.server.languages) # Parse the server's KEX. _, payload = s.read_packet(2) SSH2_Kex.parse(payload) return True
def send_kexinit( self, key_exchanges: List[str] = [ 'curve25519-sha256', '*****@*****.**', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group14-sha256' ], hostkeys: List[str] = [ 'rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ssh-ed25519' ], ciphers: List[str] = [ '*****@*****.**', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', '*****@*****.**', '*****@*****.**' ], macs: List[str] = [ '*****@*****.**', '*****@*****.**', '*****@*****.**', '*****@*****.**', '*****@*****.**', '*****@*****.**', '*****@*****.**', 'hmac-sha2-256', 'hmac-sha2-512', 'hmac-sha1' ], compressions: List[str] = ['none', '*****@*****.**'], languages: List[str] = [''] ) -> None: # pylint: disable=dangerous-default-value '''Sends the list of supported host keys, key exchanges, ciphers, and MACs. Emulates OpenSSH v8.2.''' self.__outputbuffer.d('KEX initialisation...', write_now=True) kexparty = SSH2_KexParty(ciphers, macs, compressions, languages) kex = SSH2_Kex(os.urandom(16), key_exchanges, hostkeys, kexparty, kexparty, False, 0) self.write_byte(Protocol.MSG_KEXINIT) kex.write(self) self.send_packet()
def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = False) -> int: program_retval = exitcodes.GOOD out.batch = aconf.batch out.verbose = aconf.verbose out.debug = aconf.debug out.level = aconf.level out.use_colors = aconf.colors s = SSH_Socket(out, aconf.host, aconf.port, aconf.ip_version_preference, aconf.timeout, aconf.timeout_set) if aconf.client_audit: out.v("Listening for client connection on port %d..." % aconf.port, write_now=True) s.listen_and_accept() else: out.v("Starting audit of %s:%d..." % ('[%s]' % aconf.host if Utils.is_ipv6_address(aconf.host) else aconf.host, aconf.port), write_now=True) err = s.connect() if err is not None: out.fail(err) # If we're running against multiple targets, return a connection error to the calling worker thread. Otherwise, write the error message to the console and exit. if len(aconf.target_list) > 0: return exitcodes.CONNECTION_ERROR else: out.write() sys.exit(exitcodes.CONNECTION_ERROR) if sshv is None: sshv = 2 if aconf.ssh2 else 1 err = None banner, header, err = s.get_banner(sshv) if banner is None: if err is None: err = '[exception] did not receive banner.' else: err = '[exception] did not receive banner: {}'.format(err) if err is None: s.send_kexinit() # Send the algorithms we support (except we don't since this isn't a real SSH connection). packet_type, payload = s.read_packet(sshv) if packet_type < 0: try: if len(payload) > 0: payload_txt = payload.decode('utf-8') else: payload_txt = 'empty' except UnicodeDecodeError: payload_txt = '"{}"'.format(repr(payload).lstrip('b')[1:-1]) if payload_txt == 'Protocol major versions differ.': if sshv == 2 and aconf.ssh1: ret = audit(out, aconf, 1) out.write() return ret err = '[exception] error reading packet ({})'.format(payload_txt) else: err_pair = None if sshv == 1 and packet_type != Protocol.SMSG_PUBLIC_KEY: err_pair = ('SMSG_PUBLIC_KEY', Protocol.SMSG_PUBLIC_KEY) elif sshv == 2 and packet_type != Protocol.MSG_KEXINIT: err_pair = ('MSG_KEXINIT', Protocol.MSG_KEXINIT) if err_pair is not None: fmt = '[exception] did not receive {0} ({1}), ' + \ 'instead received unknown message ({2})' err = fmt.format(err_pair[0], err_pair[1], packet_type) if err is not None: output(out, aconf, banner, header) out.fail(err) return exitcodes.CONNECTION_ERROR if sshv == 1: program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload)) elif sshv == 2: try: kex = SSH2_Kex.parse(payload) except Exception: out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc())) return exitcodes.CONNECTION_ERROR if aconf.client_audit is False: HostKeyTest.run(out, s, kex) GEXTest.run(out, s, kex) # This is a standard audit scan. if (aconf.policy is None) and (aconf.make_policy is False): program_retval = output(out, aconf, banner, header, client_host=s.client_host, kex=kex, print_target=print_target) # This is a policy test. elif (aconf.policy is not None) and (aconf.make_policy is False): program_retval = exitcodes.GOOD if evaluate_policy(out, aconf, banner, s.client_host, kex=kex) else exitcodes.FAILURE # A new policy should be made from this scan. elif (aconf.policy is None) and (aconf.make_policy is True): make_policy(aconf, banner, kex, s.client_host) else: raise RuntimeError('Internal error while handling output: %r %r' % (aconf.policy is None, aconf.make_policy)) return program_retval
def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = False) -> int: program_retval = exitcodes.GOOD out.batch = aconf.batch out.verbose = aconf.verbose out.level = aconf.level out.use_colors = aconf.colors s = SSH_Socket(aconf.host, aconf.port, aconf.ipvo, aconf.timeout, aconf.timeout_set) if aconf.client_audit: s.listen_and_accept() else: err = s.connect() if err is not None: out.fail(err) sys.exit(exitcodes.CONNECTION_ERROR) if sshv is None: sshv = 2 if aconf.ssh2 else 1 err = None banner, header, err = s.get_banner(sshv) if banner is None: if err is None: err = '[exception] did not receive banner.' else: err = '[exception] did not receive banner: {}'.format(err) if err is None: s.send_algorithms( ) # Send the algorithms we support (except we don't since this isn't a real SSH connection). packet_type, payload = s.read_packet(sshv) if packet_type < 0: try: if len(payload) > 0: payload_txt = payload.decode('utf-8') else: payload_txt = u'empty' except UnicodeDecodeError: payload_txt = u'"{}"'.format(repr(payload).lstrip('b')[1:-1]) if payload_txt == u'Protocol major versions differ.': if sshv == 2 and aconf.ssh1: return audit(aconf, 1) err = '[exception] error reading packet ({})'.format(payload_txt) else: err_pair = None if sshv == 1 and packet_type != Protocol.SMSG_PUBLIC_KEY: err_pair = ('SMSG_PUBLIC_KEY', Protocol.SMSG_PUBLIC_KEY) elif sshv == 2 and packet_type != Protocol.MSG_KEXINIT: err_pair = ('MSG_KEXINIT', Protocol.MSG_KEXINIT) if err_pair is not None: fmt = '[exception] did not receive {0} ({1}), ' + \ 'instead received unknown message ({2})' err = fmt.format(err_pair[0], err_pair[1], packet_type) if err is not None: output(aconf, banner, header) out.fail(err) return exitcodes.CONNECTION_ERROR if sshv == 1: program_retval = output(aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload)) elif sshv == 2: kex = SSH2_Kex.parse(payload) if aconf.client_audit is False: HostKeyTest.run(s, kex) GEXTest.run(s, kex) # This is a standard audit scan. if (aconf.policy is None) and (aconf.make_policy is False): program_retval = output(aconf, banner, header, client_host=s.client_host, kex=kex, print_target=print_target) # This is a policy test. elif (aconf.policy is not None) and (aconf.make_policy is False): program_retval = exitcodes.GOOD if evaluate_policy( aconf, banner, s.client_host, kex=kex) else exitcodes.FAILURE # A new policy should be made from this scan. elif (aconf.policy is None) and (aconf.make_policy is True): make_policy(aconf, banner, kex, s.client_host) else: raise RuntimeError('Internal error while handling output: %r %r' % (aconf.policy is None, aconf.make_policy)) return program_retval
def perform_test(s: 'SSH_Socket', server_kex: 'SSH2_Kex', kex_str: str, kex_group: 'KexDH', host_key_types: Dict[str, Dict[str, bool]]) -> None: hostkey_modulus_size = 0 ca_modulus_size = 0 # If the connection still exists, close it so we can test # using a clean slate (otherwise it may exist in a non-testable # state). if s.is_connected(): s.close() # For each host key type... for host_key_type in host_key_types: # Skip those already handled (i.e.: those in the RSA family, as testing one tests them all). if 'parsed' in host_key_types[host_key_type] and host_key_types[ host_key_type]['parsed']: continue # If this host key type is supported by the server, we test it. if host_key_type in server_kex.key_algorithms: cert = host_key_types[host_key_type]['cert'] variable_key_len = host_key_types[host_key_type][ 'variable_key_len'] # If the connection is closed, re-open it and get the kex again. if not s.is_connected(): err = s.connect() if err is not None: return _, _, err = s.get_banner() if err is not None: s.close() return # Parse the server's initial KEX. packet_type = 0 # pylint: disable=unused-variable packet_type, payload = s.read_packet() SSH2_Kex.parse(payload) # Send the server our KEXINIT message, using only our # selected kex and host key type. Send the server's own # list of ciphers and MACs back to it (this doesn't # matter, really). client_kex = SSH2_Kex(os.urandom(16), [kex_str], [host_key_type], server_kex.client, server_kex.server, False, 0) s.write_byte(Protocol.MSG_KEXINIT) client_kex.write(s) s.send_packet() # Do the initial DH exchange. The server responds back # with the host key and its length. Bingo. We also get back the host key fingerprint. kex_group.send_init(s) host_key = kex_group.recv_reply(s, variable_key_len) if host_key is not None: server_kex.set_host_key(host_key_type, host_key) hostkey_modulus_size = kex_group.get_hostkey_size() ca_modulus_size = kex_group.get_ca_size() # Close the socket, as the connection has # been put in a state that later tests can't use. s.close() # If the host key modulus or CA modulus was successfully parsed, check to see that its a safe size. if hostkey_modulus_size > 0 or ca_modulus_size > 0: # Set the hostkey size for all RSA key types since 'ssh-rsa', # 'rsa-sha2-256', etc. are all using the same host key. # Note, however, that this may change in the future. if cert is False and host_key_type in HostKeyTest.RSA_FAMILY: for rsa_type in HostKeyTest.RSA_FAMILY: server_kex.set_rsa_key_size( rsa_type, hostkey_modulus_size) elif cert is True: server_kex.set_rsa_key_size(host_key_type, hostkey_modulus_size, ca_modulus_size) # Keys smaller than 2048 result in a failure. Update the database accordingly. if (cert is False) and (hostkey_modulus_size < 2048): for rsa_type in HostKeyTest.RSA_FAMILY: alg_list = SSH2_KexDB.ALGORITHMS['key'][rsa_type] alg_list.append([ 'using small %d-bit modulus' % hostkey_modulus_size ]) elif (cert is True) and ( (hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048)): # pylint: disable=chained-comparison alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type] min_modulus = min(hostkey_modulus_size, ca_modulus_size) min_modulus = min_modulus if min_modulus > 0 else max( hostkey_modulus_size, ca_modulus_size) alg_list.append( ['using small %d-bit modulus' % min_modulus]) # If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all). if host_key_type in HostKeyTest.RSA_FAMILY: for rsa_type in HostKeyTest.RSA_FAMILY: host_key_types[rsa_type]['parsed'] = True else: host_key_types[host_key_type]['parsed'] = True
def perform_test(out: 'OutputBuffer', s: 'SSH_Socket', server_kex: 'SSH2_Kex', kex_str: str, kex_group: 'KexDH', host_key_types: Dict[str, Dict[str, bool]]) -> None: hostkey_modulus_size = 0 ca_modulus_size = 0 # If the connection still exists, close it so we can test # using a clean slate (otherwise it may exist in a non-testable # state). if s.is_connected(): s.close() # For each host key type... for host_key_type in host_key_types: # Skip those already handled (i.e.: those in the RSA family, as testing one tests them all). if 'parsed' in host_key_types[host_key_type] and host_key_types[ host_key_type]['parsed']: continue # If this host key type is supported by the server, we test it. if host_key_type in server_kex.key_algorithms: out.d('Preparing to obtain ' + host_key_type + ' host key...', write_now=True) cert = host_key_types[host_key_type]['cert'] variable_key_len = host_key_types[host_key_type][ 'variable_key_len'] # If the connection is closed, re-open it and get the kex again. if not s.is_connected(): err = s.connect() if err is not None: out.v(err, write_now=True) return _, _, err = s.get_banner() if err is not None: out.v(err, write_now=True) s.close() return # Send our KEX using the specified group-exchange and most of the server's own values. s.send_kexinit(key_exchanges=[kex_str], hostkeys=[host_key_type], ciphers=server_kex.server.encryption, macs=server_kex.server.mac, compressions=server_kex.server.compression, languages=server_kex.server.languages) try: # Parse the server's KEX. _, payload = s.read_packet() SSH2_Kex.parse(payload) except Exception: out.v( "Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True) return # Do the initial DH exchange. The server responds back # with the host key and its length. Bingo. We also get back the host key fingerprint. kex_group.send_init(s) try: host_key = kex_group.recv_reply(s, variable_key_len) if host_key is not None: server_kex.set_host_key(host_key_type, host_key) except Exception: pass hostkey_modulus_size = kex_group.get_hostkey_size() ca_modulus_size = kex_group.get_ca_size() # Close the socket, as the connection has # been put in a state that later tests can't use. s.close() # If the host key modulus or CA modulus was successfully parsed, check to see that its a safe size. if hostkey_modulus_size > 0 or ca_modulus_size > 0: # Set the hostkey size for all RSA key types since 'ssh-rsa', # 'rsa-sha2-256', etc. are all using the same host key. # Note, however, that this may change in the future. if cert is False and host_key_type in HostKeyTest.RSA_FAMILY: for rsa_type in HostKeyTest.RSA_FAMILY: server_kex.set_rsa_key_size( rsa_type, hostkey_modulus_size) elif cert is True: server_kex.set_rsa_key_size(host_key_type, hostkey_modulus_size, ca_modulus_size) # Keys smaller than 2048 result in a failure. Update the database accordingly. if (cert is False) and (hostkey_modulus_size < 2048): for rsa_type in HostKeyTest.RSA_FAMILY: alg_list = SSH2_KexDB.ALGORITHMS['key'][rsa_type] # If no failure list exists, add an empty failure list. if len(alg_list) < 2: alg_list.append([]) alg_list[1].append('using small %d-bit modulus' % hostkey_modulus_size) elif (cert is True) and ( (hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048)): # pylint: disable=chained-comparison alg_list = SSH2_KexDB.ALGORITHMS['key'][host_key_type] min_modulus = min(hostkey_modulus_size, ca_modulus_size) min_modulus = min_modulus if min_modulus > 0 else max( hostkey_modulus_size, ca_modulus_size) # If no failure list exists, add an empty failure list. if len(alg_list) < 2: alg_list.append([]) alg_list[1].append('using small %d-bit modulus' % min_modulus) # If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all). if host_key_type in HostKeyTest.RSA_FAMILY: for rsa_type in HostKeyTest.RSA_FAMILY: host_key_types[rsa_type]['parsed'] = True else: host_key_types[host_key_type]['parsed'] = True