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