Repository: evict/SSHScan Branch: master Commit: b40ecf8adb50 Files: 5 Total size: 11.1 KB Directory structure: gitextract_tna4p_bf/ ├── LICENSE ├── README.md ├── config.yml ├── requirements.txt └── sshscan.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Vincent Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ SSHScan ======= SSHScan is a testing tool that enumerates SSH Ciphers.
Using SSHScan, weak ciphers can be easily detected. Usage ===== You can install SSHScan by cloning the [Git](https://github.com/evict/SSHScan) repository:
`git clone https://github.com/evict/SSHScan SSHScan` To get a list of basic options use:
`python sshscan.py -h` Edit `config.yml` to add / remove strong ciphers. ================================================ FILE: config.yml ================================================ ciphers: ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'] macs: ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128', 'umac-128-etm@openssh.com', 'hmac-sha2-512', 'hmac-sha2-256', 'umac-128@openssh.com'] kex: ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group-exchange-sha256'] hka: ['ssh-rsa-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'ssh-rsa-cert-v00@openssh.com', 'ssh-rsa', 'ssh-ed25519'] ================================================ FILE: requirements.txt ================================================ PyYAML ================================================ FILE: sshscan.py ================================================ #!/usr/bin/env python3 import sys import socket import struct from yaml import safe_load from typing import List, Tuple from secrets import token_bytes from binascii import hexlify from optparse import OptionParser, OptionGroup def banner(): banner = """ _____ _____ _ _ _____ / ___/ ___| | | / ___| \ `--.\ `--.| |_| \ `--. ___ __ _ _ __ `--. \`--. | _ |`--. \/ __/ _` | '_ \\ /\__/ /\__/ | | | /\__/ | (_| (_| | | | | \____/\____/\_| |_\____/ \___\__,_|_| |_| evict """ return banner def print_columns(cipherlist): # adjust the amount of columns to display cols = 2 while len(cipherlist) % cols != 0: cipherlist.append("") else: split = [ cipherlist[i : i + int(len(cipherlist) / cols)] for i in range(0, len(cipherlist), int(len(cipherlist) / cols)) ] for row in zip(*split): print(" " + "".join(str.ljust(c, 37) for c in row)) print("\n") def return_diff_list(detected, strong): results = [] for item in detected: if item not in strong: results.append(item) return results def parse_results(version, kex, salg, enc, mac, cmpv): version = version.decode("utf-8").rstrip() kex = kex.decode("utf-8").split(",") salg = salg.decode("utf-8").split(",") enc = enc.decode("utf-8").split(",") mac = mac.decode("utf-8").split(",") cmpv = cmpv.decode("utf-8").split(",") with open("config.yml") as fd: config = safe_load(fd) weak_ciphers = return_diff_list(enc, config["ciphers"]) weak_macs = return_diff_list(mac, config["macs"]) weak_kex = return_diff_list(kex, config["kex"]) weak_hka = return_diff_list(salg, config["hka"]) compression = True if "zlib@openssh.com" in cmpv else False print(" [+] Detected the following ciphers: ") print_columns(enc) print(" [+] Detected the following KEX algorithms: ") print_columns(kex) print(" [+] Detected the following MACs: ") print_columns(mac) print(" [+] Detected the following HostKey algorithms: ") print_columns(salg) print(" [+] Target SSH version is: %s" % version) print(" [+] Retrieving ciphers...") if weak_ciphers: print(" [+] Detected the following weak ciphers: ") print_columns(weak_ciphers) else: print(" [+] No weak ciphers detected!") if weak_kex: print(" [+] Detected the following weak KEX algorithms: ") print_columns(weak_kex) else: print(" [+] No weak KEX detected!") if weak_macs: print(" [+] Detected the following weak MACs: ") print_columns(weak_macs) else: print(" [+] No weak MACs detected!") if weak_hka: print(" [+] Detected the following weak HostKey algorithms: ") print_columns(weak_hka) else: print(" [+] No weak HostKey algorithms detected!") if compression: print(" [+] Compression has been enabled!") def unpack_ssh_name_list(kex, n): """ Unpack the name-list from the packet The comma separated list is preceded by an unsigned integer which specifies the size of the list. """ size = struct.unpack("!I", kex[n : n + 4])[0] + 1 # jump to the name-list n += 3 payload = struct.unpack(f"!{size}p", kex[n : n + size])[0] # to the next integer n += size return payload, n def unpack_msg_kex_init(kex): # the MSG for KEXINIT looks as follows # byte SSH_MSG_KEXINIT # byte[16] cookie (random bytes) # name-list kex_algorithms # name-list server_host_key_algorithms # name-list encryption_algorithms_client_to_server # name-list encryption_algorithms_server_to_client # name-list mac_algorithms_client_to_server # name-list mac_algorithms_server_to_client # name-list compression_algorithms_client_to_server # name-list compression_algorithms_server_to_client # name-list languages_client_to_server # name-list languages_server_to_client # boolean first_kex_packet_follows # uint32 0 (reserved for future extension) packet_size = struct.unpack("!I", kex[0:4])[0] print(f"[*] KEX size: {packet_size}") message = kex[5] # 20 == SSH_MSG_KEXINIT if message != 20: raise ValueError("did not receive SSH_MSG_KEXINIT") cookie = struct.unpack("!16p", kex[6:22])[0] print(f"[*] server cookie: {hexlify(cookie).decode('utf-8')}") kex_size = struct.unpack("!I", kex[22:26])[0] kex_size += 1 kex_algos = struct.unpack(f"!{kex_size}p", kex[25 : 25 + kex_size])[0] n = 25 + kex_size server_host_key_algo, n = unpack_ssh_name_list(kex, n) enc_client_to_server, n = unpack_ssh_name_list(kex, n) enc_server_to_client, n = unpack_ssh_name_list(kex, n) mac_client_to_server, n = unpack_ssh_name_list(kex, n) mac_server_to_client, n = unpack_ssh_name_list(kex, n) cmp_client_to_server, n = unpack_ssh_name_list(kex, n) cmp_server_to_client, n = unpack_ssh_name_list(kex, n) return ( kex_algos, server_host_key_algo, enc_server_to_client, mac_server_to_client, cmp_server_to_client, ) def pack_msg_kexinit_for_server(kex, salg, enc, mac, cmpv): kex_fmt = f"!I{len(kex)}s" sal_fmt = f"!I{len(salg)}s" enc_fmt = f"!I{len(enc)}s" mac_fmt = f"!I{len(mac)}s" cmp_fmt = f"!I{len(cmpv)}s" kex = struct.pack(kex_fmt, len(kex), kex) sal = struct.pack(sal_fmt, len(salg), salg) enc = struct.pack(enc_fmt, len(enc), enc) mac = struct.pack(mac_fmt, len(mac), mac) cmpv = struct.pack(cmp_fmt, len(cmpv), cmpv) # languages are not used, therefore null # 4 bytes are reserved remain = b"\x00\x00\x00\x00" packet = b"\x20" packet += token_bytes(16) packet += kex packet += sal # we are lazy and have the ctos and stoc options same. # this should not be the case packet += enc packet += enc packet += mac packet += mac packet += cmpv packet += cmpv packet += b"\x00" packet += remain packet += b"\x00" * 8 # + unsigned int + header size = len(packet) + 4 + 2 # properly calculate the padding with length % 8 padding_len = size % 8 if padding_len < 4: padding_len = 4 return _pack_packet(packet) def retrieve_initial_kexinit(host: str, port: int) -> Tuple[List, List]: s = return_socket_for_host(host, port) version = s.recv(2048) s.send(version) kex_init = s.recv(4096) s.close() return kex_init, version def return_socket_for_host(host, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) return s def _pack_packet(packet): block_size = 8 # https://github.com/paramiko/paramiko/blob/master/paramiko/packet.py#L631 padding_len = 3 + block_size - ((len(packet) + 8) % block_size) + 1 if padding_len < block_size: padding_len = block_size header = struct.pack(">IB", len(packet) + padding_len, padding_len) padding = b"\x00" * padding_len packet = header + packet + padding return packet def main(): print(banner()) parser = OptionParser(usage="usage %prog [options]", version="%prog 2.0") parameters = OptionGroup(parser, "Options") parameters.add_option( "-t", "--target", type="string", help="Specify target as 'target' or 'target:port' (port 22 is default)", dest="target", ) parameters.add_option( "-l", "--target-list", type="string", help="File with targets: 'target' or 'target:port' seperated by a newline (port 22 is default)", dest="targetlist", ) parser.add_option_group(parameters) options, arguments = parser.parse_args() targets = [] target = options.target targetlist = options.targetlist if target: targets.append(target) else: if targetlist: with open(targetlist) as fd: for item in fd.readlines(): targets.append(item.rstrip()) else: print("[-] No target specified!") sys.exit(0) # we send first packets to make sure we match keys for target in targets: if ":" not in target: target += ":22" host, port = target.split(":") port = int(port) try: kex_init, version = retrieve_initial_kexinit(host, port) except socket.timeout: print(" [-] Timeout while connecting to %s on port %i\n" % (host, port)) except socket.error as e: if e.errno == 61: print(" [-] %s\n" % (e.strerror)) else: print( " [-] Error while connecting to %s on port %i\n" % (host, port) ) # parse the server KEXINIT message kex, salg, enc, mac, cmpv = unpack_msg_kex_init(kex_init) parse_results(version, kex, salg, enc, mac, cmpv) if __name__ == "__main__": main()