예제 #1
0
    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)
예제 #2
0
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
예제 #3
0
 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)
예제 #4
0
 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
예제 #5
0
 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)
예제 #6
0
 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
예제 #7
0
    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)
예제 #8
0
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))
예제 #9
0
 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
예제 #10
0
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)
예제 #11
0
 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)
예제 #12
0
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
예제 #13
0
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
예제 #14
0
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
예제 #15
0
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
예제 #16
0
 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
예제 #17
0
 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
예제 #18
0
 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
예제 #19
0
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
예제 #20
0
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
예제 #21
0
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