def __setattr__( self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None: valid = False if name in [ 'ssh1', 'ssh2', 'batch', 'client_audit', 'colors', 'verbose', 'timeout_set', 'json', 'make_policy', 'list_policies' ]: valid, value = True, bool(value) elif name in ['ipv4', 'ipv6']: valid = False value = bool(value) ipv = 4 if name == 'ipv4' else 6 if value: value = tuple(list(self.ipvo) + [ipv]) else: # pylint: disable=else-if-used if len(self.ipvo) == 0: value = (6, ) if ipv == 4 else (4, ) else: value = tuple([x for x in self.ipvo if x != ipv]) self.__setattr__('ipvo', value) elif name == 'ipvo': if isinstance(value, (tuple, list)): uniq_value = Utils.unique_seq(value) value = tuple([x for x in uniq_value if x in (4, 6)]) valid = True ipv_both = len(value) == 0 object.__setattr__(self, 'ipv4', ipv_both or 4 in value) object.__setattr__(self, 'ipv6', ipv_both or 6 in value) elif name == 'port': valid, port = True, Utils.parse_int(value) if port < 1 or port > 65535: raise ValueError('invalid port: {}'.format(value)) value = port elif name in ['level']: if value not in ('info', 'warn', 'fail'): raise ValueError('invalid level: {}'.format(value)) valid = True elif name == 'host': valid = True elif name == 'timeout': value = Utils.parse_float(value) if value == -1.0: raise ValueError('invalid timeout: {}'.format(value)) valid = True elif name in [ 'policy_file', 'policy', 'target_file', 'target_list', 'lookup' ]: valid = True if valid: object.__setattr__(self, name, value)
def evaluate_policy(aconf: AuditConf, banner: Optional['Banner'], client_host: Optional[str], kex: Optional['SSH2_Kex'] = None) -> bool: if aconf.policy is None: raise RuntimeError( 'Internal error: cannot evaluate against null Policy!') passed, error_struct, error_str = aconf.policy.evaluate(banner, kex) if aconf.json: json_struct = { 'host': aconf.host, 'policy': aconf.policy.get_name_and_version(), 'passed': passed, 'errors': error_struct } print(json.dumps(json_struct, sort_keys=True)) else: spacing = '' if aconf.client_audit: print("Client IP: %s" % client_host) spacing = " " # So the fields below line up with 'Client IP: '. else: host = aconf.host if aconf.port != 22: # Check if this is an IPv6 address, as that is printed in a different format. if Utils.is_ipv6_address(aconf.host): host = '[%s]:%d' % (aconf.host, aconf.port) else: host = '%s:%d' % (aconf.host, aconf.port) print("Host: %s" % host) print("Policy: %s%s" % (spacing, aconf.policy.get_name_and_version())) print("Result: %s" % spacing, end='') # Use these nice unicode characters in the result message, unless we're on Windows (the cmd.exe terminal doesn't display them properly). icon_good = "✔ " icon_fail = "❌ " if Utils.is_windows(): icon_good = "" icon_fail = "" if passed: out.good("%sPassed" % icon_good) else: out.fail("%sFailed!" % icon_fail) out.warn("\nErrors:\n%s" % error_str) return passed
def parse(cls, banner: str) -> Optional['Banner']: valid_ascii = Utils.is_print_ascii(banner) ascii_banner = Utils.to_print_ascii(banner) mx = cls.RX_BANNER.match(ascii_banner) if mx is None: return None protocol = min(re.findall(cls.RX_PROTOCOL, mx.group(1))) protocol = (int(protocol[0]), int(protocol[1])) software = (mx.group(3) or '').strip() or None if software is None and (mx.group(2) or '').startswith('-'): software = '' comments = (mx.group(4) or '').strip() or None if comments is not None: comments = re.sub(r'\s+', ' ', comments) return cls(protocol, software, comments, valid_ascii)
def __init__( self, outputbuffer: 'OutputBuffer', host: Optional[str], port: int, ip_version_preference: List[int] = [], timeout: Union[int, float] = 5, timeout_set: bool = False ) -> None: # pylint: disable=dangerous-default-value super(SSH_Socket, self).__init__() self.__outputbuffer = outputbuffer self.__sock: Optional[socket.socket] = None self.__sock_map: Dict[int, socket.socket] = {} self.__block_size = 8 self.__state = 0 self.__header: List[str] = [] self.__banner: Optional[Banner] = None if host is None: raise ValueError('undefined host') nport = Utils.parse_int(port) if nport < 1 or nport > 65535: raise ValueError('invalid port: {}'.format(port)) self.__host = host self.__port = nport self.__ip_version_preference = ip_version_preference # Holds only 5 possible values: [] (no preference), [4] (use IPv4 only), [6] (use IPv6 only), [46] (use both IPv4 and IPv6, but prioritize v4), and [64] (use both IPv4 and IPv6, but prioritize v6). self.__timeout = timeout self.__timeout_set = timeout_set self.client_host: Optional[str] = None self.client_port = None
def connect(self) -> Optional[str]: '''Returns None on success, or an error string.''' err = None for af, addr in self._resolve(): s = None try: s = socket.socket(af, socket.SOCK_STREAM) s.settimeout(self.__timeout) self.__outputbuffer.d( ("Connecting to %s:%d..." % ('[%s]' % addr[0] if Utils.is_ipv6_address(addr[0]) else addr[0], addr[1])), write_now=True) s.connect(addr) self.__sock = s return None except socket.error as e: err = e self._close_socket(s) if err is None: errm = 'host {} has no DNS records'.format(self.__host) else: errt = (self.__host, self.__port, err) errm = 'cannot connect to {} port {}: {}'.format(*errt) return '[exception] {}'.format(errm)
def __init__(self, host: Optional[str], port: int, ipvo: Optional[Sequence[int]] = None, timeout: Union[int, float] = 5, timeout_set: bool = False) -> None: super(SSH_Socket, self).__init__() self.__sock = None # type: Optional[socket.socket] self.__sock_map = {} # type: Dict[int, socket.socket] self.__block_size = 8 self.__state = 0 self.__header = [] # type: List[str] self.__banner = None # type: Optional[Banner] if host is None: raise ValueError('undefined host') nport = Utils.parse_int(port) if nport < 1 or nport > 65535: raise ValueError('invalid port: {}'.format(port)) self.__host = host self.__port = nport if ipvo is not None: self.__ipvo = ipvo else: self.__ipvo = () self.__timeout = timeout self.__timeout_set = timeout_set self.client_host = None # type: Optional[str] self.client_port = None
def __setattr__( self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None: valid = False if name in [ 'batch', 'client_audit', 'colors', 'json', 'list_policies', 'manual', 'make_policy', 'ssh1', 'ssh2', 'timeout_set', 'verbose', 'debug' ]: valid, value = True, bool(value) elif name in ['ipv4', 'ipv6']: valid, value = True, bool(value) if len(self.ip_version_preference ) == 2: # Being called more than twice is not valid. valid = False elif value: self.ip_version_preference.append(4 if name == 'ipv4' else 6) elif name == 'port': valid, port = True, Utils.parse_int(value) if port < 1 or port > 65535: raise ValueError('invalid port: {}'.format(value)) value = port elif name in ['level']: if value not in ('info', 'warn', 'fail'): raise ValueError('invalid level: {}'.format(value)) valid = True elif name == 'host': valid = True elif name == 'timeout': value = Utils.parse_float(value) if value == -1.0: raise ValueError('invalid timeout: {}'.format(value)) valid = True elif name in [ 'ip_version_preference', 'lookup', 'policy_file', 'policy', 'target_file', 'target_list' ]: valid = True elif name == "threads": valid, num_threads = True, Utils.parse_int(value) if num_threads < 1: raise ValueError('invalid number of threads: {}'.format(value)) value = num_threads if valid: object.__setattr__(self, name, value)
class Output: LEVELS = ('info', 'warn', 'fail') # type: Sequence[str] COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31} # Use brighter colors on Windows for better readability. if Utils.is_windows(): COLORS = {'head': 96, 'good': 92, 'warn': 93, 'fail': 91} def __init__(self) -> None: self.batch = False self.verbose = False self.use_colors = True self.json = False self.__level = 0 self.__colsupport = 'colorama' in sys.modules or os.name == 'posix' @property def level(self) -> str: if self.__level < len(self.LEVELS): return self.LEVELS[self.__level] return 'unknown' @level.setter def level(self, name: str) -> None: self.__level = self.get_level(name) def get_level(self, name: str) -> int: cname = 'info' if name == 'good' else name if cname not in self.LEVELS: return sys.maxsize return self.LEVELS.index(cname) def sep(self) -> None: if not self.batch: print() @property def colors_supported(self) -> bool: return self.__colsupport @staticmethod def _colorized(color: str) -> Callable[[str], None]: return lambda x: print(u'{}{}\033[0m'.format(color, x)) def __getattr__(self, name: str) -> Callable[[str], None]: if name == 'head' and self.batch: return lambda x: None if not self.get_level(name) >= self.__level: return lambda x: None if self.use_colors and self.colors_supported and name in self.COLORS: color = '\033[0;{}m'.format(self.COLORS[name]) return self._colorized(color) else: return lambda x: print(u'{}'.format(x))
def get_ssh_timeframe(self, for_server: Optional[bool] = None) -> 'Timeframe': timeframe = Timeframe() for alg_pair in self.values: alg_db = alg_pair.db for alg_type, alg_list in alg_pair.items(): for alg_name in alg_list: alg_name_native = Utils.to_text(alg_name) alg_desc = alg_db[alg_type].get(alg_name_native) if alg_desc is None: continue versions = alg_desc[0] timeframe.update(versions, for_server) return timeframe
def main() -> int: aconf = process_commandline(sys.argv[1:], usage) if aconf.lookup != '': retval = algorithm_lookup(aconf.lookup) sys.exit(retval) # If multiple targets were specified... if len(aconf.target_list) > 0: ret = exitcodes.GOOD # If JSON output is desired, each target's results will be reported in its own list entry. if aconf.json: print('[', end='') # Loop through each target in the list. for i, target in enumerate(aconf.target_list): aconf.host, port = Utils.parse_host_and_port(target) if port == 0: port = 22 aconf.port = port new_ret = audit(aconf, print_target=True) # Set the return value only if an unknown error occurred, a failure occurred, or if a warning occurred and the previous value was good. if (new_ret == exitcodes.UNKNOWN_ERROR) or ( new_ret == exitcodes.FAILURE) or ( (new_ret == exitcodes.WARNING) and (ret == exitcodes.GOOD)): ret = new_ret # Don't print a delimiter after the last target was handled. if i + 1 != len(aconf.target_list): if aconf.json: print(", ", end='') else: print(("-" * 80) + "\n") if aconf.json: print(']') return ret else: return audit(aconf)
def _resolve(self, ipvo: Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]: ipvo = tuple([x for x in Utils.unique_seq(ipvo) if x in (4, 6)]) ipvo_len = len(ipvo) prefer_ipvo = ipvo_len > 0 prefer_ipv4 = prefer_ipvo and ipvo[0] == 4 if ipvo_len == 1: family = socket.AF_INET if ipvo[0] == 4 else socket.AF_INET6 else: family = socket.AF_UNSPEC try: stype = socket.SOCK_STREAM r = socket.getaddrinfo(self.__host, self.__port, family, stype) if prefer_ipvo: r = sorted(r, key=lambda x: x[0], reverse=not prefer_ipv4) check = any(stype == rline[2] for rline in r) for af, socktype, _proto, _canonname, addr in r: if not check or socktype == socket.SOCK_STREAM: yield af, addr except socket.error as e: Output().fail('[exception] {}'.format(e)) sys.exit(exitcodes.CONNECTION_ERROR)
def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header: List[str], client_host: Optional[str] = None, kex: Optional[SSH2_Kex] = None, pkm: Optional[SSH1_PublicKeyMessage] = None, print_target: bool = False) -> int: program_retval = exitcodes.GOOD client_audit = client_host is not None # If set, this is a client audit. sshv = 1 if pkm is not None else 2 algs = Algorithms(pkm, kex) with out: if print_target: host = aconf.host # Print the port if it's not the default of 22. if aconf.port != 22: # Check if this is an IPv6 address, as that is printed in a different format. if Utils.is_ipv6_address(aconf.host): host = '[%s]:%d' % (aconf.host, aconf.port) else: host = '%s:%d' % (aconf.host, aconf.port) out.good('(gen) target: {}'. format(host)) if client_audit: out.good('(gen) client IP: {}'.format(client_host)) if len(header) > 0: out.info('(gen) header: ' + '\n'.join(header)) if banner is not None: banner_line = '(gen) banner: {}'.format(banner) if sshv == 1 or banner.protocol[0] == 1: out.fail(banner_line) out.fail('(gen) protocol SSH1 enabled') else: out.good(banner_line) if not banner.valid_ascii: # NOTE: RFC 4253, Section 4.2 out.warn('(gen) banner contains non-printable ASCII') software = Software.parse(banner) if software is not None: out.good('(gen) software: {}'.format(software)) else: software = None output_compatibility(out, algs, client_audit) if kex is not None: compressions = [x for x in kex.server.compression if x != 'none'] if len(compressions) > 0: cmptxt = 'enabled ({})'.format(', '.join(compressions)) else: cmptxt = 'disabled' out.good('(gen) compression: {}'.format(cmptxt)) if not out.is_section_empty() and not aconf.json: # Print output when it exists and JSON output isn't requested. out.head('# general') out.flush_section() out.sep() maxlen = algs.maxlen + 1 output_security(out, banner, client_audit, maxlen, aconf.json) # Filled in by output_algorithms() with unidentified algs. unknown_algorithms: List[str] = [] if pkm is not None: adb = SSH1_KexDB.ALGORITHMS ciphers = pkm.supported_ciphers auths = pkm.supported_authentications title, atype = 'SSH1 host-key algorithms', 'key' program_retval = output_algorithms(out, title, adb, atype, ['ssh-rsa1'], unknown_algorithms, aconf.json, program_retval, maxlen) title, atype = 'SSH1 encryption algorithms (ciphers)', 'enc' program_retval = output_algorithms(out, title, adb, atype, ciphers, unknown_algorithms, aconf.json, program_retval, maxlen) title, atype = 'SSH1 authentication types', 'aut' program_retval = output_algorithms(out, title, adb, atype, auths, unknown_algorithms, aconf.json, program_retval, maxlen) if kex is not None: adb = SSH2_KexDB.ALGORITHMS title, atype = 'key exchange algorithms', 'kex' program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.dh_modulus_sizes()) title, atype = 'host-key algorithms', 'key' program_retval = output_algorithms(out, title, adb, atype, kex.key_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, kex.rsa_key_sizes()) title, atype = 'encryption algorithms (ciphers)', 'enc' program_retval = output_algorithms(out, title, adb, atype, kex.server.encryption, unknown_algorithms, aconf.json, program_retval, maxlen) title, atype = 'message authentication code algorithms', 'mac' program_retval = output_algorithms(out, title, adb, atype, kex.server.mac, unknown_algorithms, aconf.json, program_retval, maxlen) output_fingerprints(out, algs, aconf.json) perfect_config = output_recommendations(out, algs, software, aconf.json, maxlen) output_info(out, software, client_audit, not perfect_config, aconf.json) if aconf.json: out.reset() # Build & write the JSON struct. out.info(json.dumps(build_struct(aconf.host + ":" + str(aconf.port), banner, kex=kex, client_host=client_host), indent=4 if aconf.json_print_indent else None, sort_keys=True)) elif len(unknown_algorithms) > 0: # If we encountered any unknown algorithms, ask the user to report them. out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer ([email protected]), or create a Github issue at <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms)) return program_retval
def output_algorithm(out: OutputBuffer, alg_db: Dict[str, Dict[str, List[List[Optional[str]]]]], alg_type: str, alg_name: str, unknown_algs: List[str], program_retval: int, alg_max_len: int = 0, alg_sizes: Optional[Dict[str, Tuple[int, int]]] = None) -> int: prefix = '(' + alg_type + ') ' if alg_max_len == 0: alg_max_len = len(alg_name) padding = '' if out.batch else ' ' * (alg_max_len - len(alg_name)) # If this is an RSA host key or DH GEX, append the size to its name and fix # the padding. alg_name_with_size = None if (alg_sizes is not None) and (alg_name in alg_sizes): hostkey_size, ca_size = alg_sizes[alg_name] if ca_size > 0: alg_name_with_size = '%s (%d-bit cert/%d-bit CA)' % (alg_name, hostkey_size, ca_size) padding = padding[0:-15] else: alg_name_with_size = '%s (%d-bit)' % (alg_name, hostkey_size) padding = padding[0:-11] texts = [] if len(alg_name.strip()) == 0: return program_retval alg_name_native = Utils.to_text(alg_name) if alg_name_native in alg_db[alg_type]: alg_desc = alg_db[alg_type][alg_name_native] ldesc = len(alg_desc) for idx, level in enumerate(['fail', 'warn', 'info']): if level == 'info': versions = alg_desc[0] since_text = Algorithm.get_since_text(versions) if since_text is not None and len(since_text) > 0: texts.append((level, since_text)) idx = idx + 1 if ldesc > idx: for t in alg_desc[idx]: if t is None: continue texts.append((level, t)) if len(texts) == 0: texts.append(('info', '')) else: texts.append(('warn', 'unknown algorithm')) unknown_algs.append(alg_name) alg_name = alg_name_with_size if alg_name_with_size is not None else alg_name first = True for level, text in texts: if level == 'fail': program_retval = exitcodes.FAILURE elif level == 'warn' and program_retval != exitcodes.FAILURE: # If a failure was found previously, don't downgrade to warning. program_retval = exitcodes.WARNING f = getattr(out, level) comment = (padding + ' -- [' + level + '] ' + text) if text != '' else '' if first: if first and level == 'info': f = out.good f(prefix + alg_name + comment) first = False else: # pylint: disable=else-if-used if out.verbose: f(prefix + alg_name + comment) elif text != '': comment = (padding + ' `- [' + level + '] ' + text) f(' ' * len(prefix + alg_name) + comment) return program_retval
def main() -> int: out = OutputBuffer() aconf = process_commandline(out, sys.argv[1:], usage) # If we're on Windows, but the colorama module could not be imported, print a warning if we're in verbose mode. if (sys.platform == 'win32') and ('colorama' not in sys.modules): out.v("WARNING: colorama module not found. Colorized output will be disabled.", write_now=True) # If we're outputting JSON, turn off colors and ensure 'info' level messages go through. if aconf.json: out.json = True out.use_colors = False if aconf.manual: # If the colorama module was not be imported, turn off colors in order # to output a plain text version of the man page. if (sys.platform == 'win32') and ('colorama' not in sys.modules): out.use_colors = False retval = windows_manual(out) out.write() sys.exit(retval) if aconf.lookup != '': retval = algorithm_lookup(out, aconf.lookup) out.write() sys.exit(retval) # If multiple targets were specified... if len(aconf.target_list) > 0: ret = exitcodes.GOOD # If JSON output is desired, each target's results will be reported in its own list entry. if aconf.json: print('[', end='') # Loop through each target in the list. target_servers = [] for _, target in enumerate(aconf.target_list): host, port = Utils.parse_host_and_port(target, default_port=22) target_servers.append((host, port)) # A ranked list of return codes. Those with higher indices will take precedence over lower ones. For example, if three servers are scanned, yielding WARNING, GOOD, and UNKNOWN_ERROR, the overall result will be UNKNOWN_ERROR, since its index is the highest. Errors have highest priority, followed by failures, then warnings. ranked_return_codes = [exitcodes.GOOD, exitcodes.WARNING, exitcodes.FAILURE, exitcodes.CONNECTION_ERROR, exitcodes.UNKNOWN_ERROR] # Queue all worker threads. num_target_servers = len(target_servers) num_processed = 0 out.v("Scanning %u targets with %s%u threads..." % (num_target_servers, '(at most) ' if aconf.threads > num_target_servers else '', aconf.threads), write_now=True) with concurrent.futures.ThreadPoolExecutor(max_workers=aconf.threads) as executor: future_to_server = {executor.submit(target_worker_thread, target_server[0], target_server[1], aconf): target_server for target_server in target_servers} for future in concurrent.futures.as_completed(future_to_server): worker_ret, worker_output = future.result() # If this worker's return code is ranked higher that what we've cached so far, update our cache. if ranked_return_codes.index(worker_ret) > ranked_return_codes.index(ret): ret = worker_ret # print("Worker for %s:%d returned %d: [%s]" % (target_server[0], target_server[1], worker_ret, worker_output)) print(worker_output, end='' if aconf.json else "\n") # Don't print a delimiter after the last target was handled. num_processed += 1 if num_processed < num_target_servers: if aconf.json: print(", ", end='') else: print(("-" * 80) + "\n") if aconf.json: print(']') else: # Just a scan against a single target. ret = audit(out, aconf) out.write() return ret
class OutputBuffer: LEVELS: Sequence[str] = ('info', 'warn', 'fail') COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31} # Use brighter colors on Windows for better readability. if Utils.is_windows(): COLORS = {'head': 96, 'good': 92, 'warn': 93, 'fail': 91} def __init__(self, buffer_output: bool = True) -> None: self.buffer_output = buffer_output self.buffer: List[str] = [] self.in_section = False self.section: List[str] = [] self.batch = False self.verbose = False self.debug = False self.use_colors = True self.json = False self.__level = 0 self.__is_color_supported = ('colorama' in sys.modules) or (os.name == 'posix') self.line_ended = True def _print(self, level: str, s: str = '', line_ended: bool = True) -> None: '''Saves output to buffer (if in buffered mode), or immediately prints to stdout otherwise.''' # If we're logging only 'warn' or above, and this is an 'info', ignore message. if self.get_level(level) < self.__level: return if self.use_colors and self.colors_supported and len( s) > 0 and level != 'info': s = "\033[0;%dm%s\033[0m" % (self.COLORS[level], s) if self.buffer_output: # Select which list to add to. If we are in a 'with' statement, then this goes in the section buffer, otherwise the general buffer. buf = self.section if self.in_section else self.buffer # Determine if a new line should be added, or if the last line should be appended. if not self.line_ended: last_entry = -1 if len(buf) > 0 else 0 buf[last_entry] = buf[last_entry] + s else: buf.append(s) # When False, this tells the next call to append to the last line we just added. self.line_ended = line_ended else: print(s) def get_buffer(self) -> str: '''Returns all buffered output, then clears the buffer.''' self.flush_section() buffer_str = "\n".join(self.buffer) self.buffer = [] return buffer_str def write(self) -> None: '''Writes the output to stdout.''' self.flush_section() print(self.get_buffer(), flush=True) def reset(self) -> None: self.flush_section() self.get_buffer() @property def level(self) -> str: '''Returns the minimum level for output.''' if self.__level < len(self.LEVELS): return self.LEVELS[self.__level] return 'unknown' @level.setter def level(self, name: str) -> None: '''Sets the minimum level for output (one of: 'info', 'warn', 'fail').''' self.__level = self.get_level(name) def get_level(self, name: str) -> int: cname = 'info' if name == 'good' else name if cname not in self.LEVELS: return sys.maxsize return self.LEVELS.index(cname) @property def colors_supported(self) -> bool: '''Returns True if the system supports color output.''' return self.__is_color_supported # When used in a 'with' block, the output to goes into a section; this can be sorted separately when add_section_to_buffer() is later called. def __enter__(self) -> 'OutputBuffer': self.in_section = True return self def __exit__(self, *args: Any) -> None: self.in_section = False def flush_section(self, sort_section: bool = False) -> None: '''Appends section output (optionally sorting it first) to the end of the buffer, then clears the section output.''' if sort_section: self.section.sort() self.buffer.extend(self.section) self.section = [] def is_section_empty(self) -> bool: '''Returns True if the section buffer is empty, otherwise False.''' return len(self.section) == 0 def head(self, s: str, line_ended: bool = True) -> 'OutputBuffer': if not self.batch: self._print('head', s, line_ended) return self def fail(self, s: str, line_ended: bool = True) -> 'OutputBuffer': self._print('fail', s, line_ended) return self def warn(self, s: str, line_ended: bool = True) -> 'OutputBuffer': self._print('warn', s, line_ended) return self def info(self, s: str, line_ended: bool = True) -> 'OutputBuffer': self._print('info', s, line_ended) return self def good(self, s: str, line_ended: bool = True) -> 'OutputBuffer': self._print('good', s, line_ended) return self def sep(self) -> 'OutputBuffer': if not self.batch: self._print('info') return self def v(self, s: str, write_now: bool = False) -> 'OutputBuffer': '''Prints a message if verbose output is enabled.''' if self.verbose or self.debug: self.info(s) if write_now: self.write() return self def d(self, s: str, write_now: bool = False) -> 'OutputBuffer': '''Prints a message if verbose output is enabled.''' if self.debug: self.info(s) if write_now: self.write() return self
def supported_ciphers(self) -> List[str]: ciphers = [] for i in range(len(SSH1.CIPHERS)): if self.__supported_ciphers_mask & (1 << i) != 0: ciphers.append(Utils.to_text(SSH1.CIPHERS[i])) return ciphers
def supported_authentications(self) -> List[str]: auths = [] for i in range(1, len(SSH1.AUTHS)): if self.__supported_authentications_mask & (1 << i) != 0: auths.append(Utils.to_text(SSH1.AUTHS[i])) return auths
def supported_ciphers(self) -> List[str]: ciphers = [] for i in range(len(SSH1.CIPHERS)): # pylint: disable=consider-using-enumerate if self.__supported_ciphers_mask & (1 << i) != 0: ciphers.append(Utils.to_text(SSH1.CIPHERS[i])) return ciphers
def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[..., None]) -> 'AuditConf': # pylint: disable=too-many-statements # pylint: disable=too-many-branches aconf = AuditConf() try: sopts = 'h1246M:p:P:jbcnvl:t:T:Lmd' lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'make-policy=', 'port=', 'policy=', 'json', 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout=', 'targets=', 'list-policies', 'lookup=', 'threads=', 'manual', 'debug'] opts, args = getopt.gnu_getopt(args, sopts, lopts) except getopt.GetoptError as err: usage_cb(str(err)) aconf.ssh1, aconf.ssh2 = False, False host: str = '' oport: Optional[str] = None port: int = 0 for o, a in opts: if o in ('-h', '--help'): usage_cb() elif o in ('-1', '--ssh1'): aconf.ssh1 = True elif o in ('-2', '--ssh2'): aconf.ssh2 = True elif o in ('-4', '--ipv4'): aconf.ipv4 = True elif o in ('-6', '--ipv6'): aconf.ipv6 = True elif o in ('-p', '--port'): oport = a elif o in ('-b', '--batch'): aconf.batch = True aconf.verbose = True elif o in ('-c', '--client-audit'): aconf.client_audit = True elif o in ('-n', '--no-colors'): aconf.colors = False out.use_colors = False elif o in ('-j', '--json'): if aconf.json: # If specified twice, enable indent printing. aconf.json_print_indent = True else: aconf.json = True elif o in ('-v', '--verbose'): aconf.verbose = True out.verbose = True elif o in ('-l', '--level'): if a not in ('info', 'warn', 'fail'): usage_cb('level {} is not valid'.format(a)) aconf.level = a elif o in ('-t', '--timeout'): aconf.timeout = float(a) aconf.timeout_set = True elif o in ('-M', '--make-policy'): aconf.make_policy = True aconf.policy_file = a elif o in ('-P', '--policy'): aconf.policy_file = a elif o in ('-T', '--targets'): aconf.target_file = a elif o == '--threads': aconf.threads = int(a) elif o in ('-L', '--list-policies'): aconf.list_policies = True elif o == '--lookup': aconf.lookup = a elif o in ('-m', '--manual'): aconf.manual = True elif o in ('-d', '--debug'): aconf.debug = True out.debug = True if len(args) == 0 and aconf.client_audit is False and aconf.target_file is None and aconf.list_policies is False and aconf.lookup == '' and aconf.manual is False: usage_cb() if aconf.manual: return aconf if aconf.lookup != '': return aconf if aconf.list_policies: list_policies(out) sys.exit(exitcodes.GOOD) if aconf.client_audit is False and aconf.target_file is None: if oport is not None: host = args[0] else: host, port = Utils.parse_host_and_port(args[0]) if not host and aconf.target_file is None: usage_cb('host is empty') if port == 0 and oport is None: if aconf.client_audit: # The default port to listen on during a client audit is 2222. port = 2222 else: port = 22 if oport is not None: port = Utils.parse_int(oport) if port <= 0 or port > 65535: usage_cb('port {} is not valid'.format(oport)) aconf.host = host aconf.port = port if not (aconf.ssh1 or aconf.ssh2): aconf.ssh1, aconf.ssh2 = True, True # If a file containing a list of targets was given, read it. if aconf.target_file is not None: try: with open(aconf.target_file, 'r', encoding='utf-8') as f: aconf.target_list = f.readlines() except PermissionError as e: # If installed as a Snap package, print a more useful message with potential work-arounds. if SNAP_PACKAGE: print(SNAP_PERMISSIONS_ERROR) else: print("Error: insufficient permissions: %s" % str(e)) sys.exit(exitcodes.UNKNOWN_ERROR) # Strip out whitespace from each line in target file, and skip empty lines. aconf.target_list = [target.strip() for target in aconf.target_list if target not in ("", "\n")] # If a policy file was provided, validate it. if (aconf.policy_file is not None) and (aconf.make_policy is False): # First, see if this is a built-in policy name. If not, assume a file path was provided, and try to load it from disk. aconf.policy = Policy.load_builtin_policy(aconf.policy_file) if aconf.policy is None: try: aconf.policy = Policy(policy_file=aconf.policy_file) except Exception as e: out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc())) out.write() sys.exit(exitcodes.UNKNOWN_ERROR) # If the user wants to do a client audit, but provided a server policy, terminate. if aconf.client_audit and aconf.policy.is_server_policy(): out.fail("Error: client audit selected, but server policy provided.") out.write() sys.exit(exitcodes.UNKNOWN_ERROR) # If the user wants to do a server audit, but provided a client policy, terminate. if aconf.client_audit is False and aconf.policy.is_server_policy() is False: out.fail("Error: server audit selected, but client policy provided.") out.write() sys.exit(exitcodes.UNKNOWN_ERROR) return aconf
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 process_commandline( args: List[str], usage_cb: Callable[..., None] ) -> 'AuditConf': # pylint: disable=too-many-statements # pylint: disable=too-many-branches aconf = AuditConf() try: sopts = 'h1246M:p:P:jbcnvl:t:T:L' lopts = [ 'help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'make-policy=', 'port=', 'policy=', 'json', 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout=', 'targets=', 'list-policies', 'lookup=' ] opts, args = getopt.gnu_getopt(args, sopts, lopts) except getopt.GetoptError as err: usage_cb(str(err)) aconf.ssh1, aconf.ssh2 = False, False host = '' # type: str oport = None # type: Optional[str] port = 0 # type: int for o, a in opts: if o in ('-h', '--help'): usage_cb() elif o in ('-1', '--ssh1'): aconf.ssh1 = True elif o in ('-2', '--ssh2'): aconf.ssh2 = True elif o in ('-4', '--ipv4'): aconf.ipv4 = True elif o in ('-6', '--ipv6'): aconf.ipv6 = True elif o in ('-p', '--port'): oport = a elif o in ('-b', '--batch'): aconf.batch = True aconf.verbose = True elif o in ('-c', '--client-audit'): aconf.client_audit = True elif o in ('-n', '--no-colors'): aconf.colors = False elif o in ('-j', '--json'): aconf.json = True elif o in ('-v', '--verbose'): aconf.verbose = True elif o in ('-l', '--level'): if a not in ('info', 'warn', 'fail'): usage_cb('level {} is not valid'.format(a)) aconf.level = a elif o in ('-t', '--timeout'): aconf.timeout = float(a) aconf.timeout_set = True elif o in ('-M', '--make-policy'): aconf.make_policy = True aconf.policy_file = a elif o in ('-P', '--policy'): aconf.policy_file = a elif o in ('-T', '--targets'): aconf.target_file = a elif o in ('-L', '--list-policies'): aconf.list_policies = True elif o == '--lookup': aconf.lookup = a if len( args ) == 0 and aconf.client_audit is False and aconf.target_file is None and aconf.list_policies is False and aconf.lookup == '': usage_cb() if aconf.lookup != '': return aconf if aconf.list_policies: list_policies() sys.exit(exitcodes.GOOD) if aconf.client_audit is False and aconf.target_file is None: if oport is not None: host = args[0] else: host, port = Utils.parse_host_and_port(args[0]) if not host and aconf.target_file is None: usage_cb('host is empty') if port == 0 and oport is None: if aconf.client_audit: # The default port to listen on during a client audit is 2222. port = 2222 else: port = 22 if oport is not None: port = Utils.parse_int(oport) if port <= 0 or port > 65535: usage_cb('port {} is not valid'.format(oport)) aconf.host = host aconf.port = port if not (aconf.ssh1 or aconf.ssh2): aconf.ssh1, aconf.ssh2 = True, True # If a file containing a list of targets was given, read it. if aconf.target_file is not None: with open(aconf.target_file, 'r') as f: aconf.target_list = f.readlines() # Strip out whitespace from each line in target file, and skip empty lines. aconf.target_list = [ target.strip() for target in aconf.target_list if target not in ("", "\n") ] # If a policy file was provided, validate it. if (aconf.policy_file is not None) and (aconf.make_policy is False): try: aconf.policy = Policy(policy_file=aconf.policy_file) except Exception as e: print("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc())) sys.exit(exitcodes.UNKNOWN_ERROR) # If the user wants to do a client audit, but provided a server policy, terminate. if aconf.client_audit and aconf.policy.is_server_policy(): print("Error: client audit selected, but server policy provided.") sys.exit(exitcodes.UNKNOWN_ERROR) # If the user wants to do a server audit, but provided a client policy, terminate. if aconf.client_audit is False and aconf.policy.is_server_policy( ) is False: print("Error: server audit selected, but client policy provided.") sys.exit(exitcodes.UNKNOWN_ERROR) return aconf