Full Code of evict/SSHScan for AI

master b40ecf8adb50 cached
5 files
11.1 KB
3.2k tokens
11 symbols
1 requests
Download .txt
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.<br>
Using SSHScan, weak ciphers can be easily detected.

Usage
=====

You can install SSHScan by cloning the [Git](https://github.com/evict/SSHScan) repository:<br>
`git clone https://github.com/evict/SSHScan SSHScan`

To get a list of basic options use: <br>
`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()
Download .txt
gitextract_tna4p_bf/

├── LICENSE
├── README.md
├── config.yml
├── requirements.txt
└── sshscan.py
Download .txt
SYMBOL INDEX (11 symbols across 1 files)

FILE: sshscan.py
  function banner (line 13) | def banner():
  function print_columns (line 26) | def print_columns(cipherlist):
  function return_diff_list (line 41) | def return_diff_list(detected, strong):
  function parse_results (line 51) | def parse_results(version, kex, salg, enc, mac, cmpv):
  function unpack_ssh_name_list (line 110) | def unpack_ssh_name_list(kex, n):
  function unpack_msg_kex_init (line 129) | def unpack_msg_kex_init(kex):
  function pack_msg_kexinit_for_server (line 185) | def pack_msg_kexinit_for_server(kex, salg, enc, mac, cmpv):
  function retrieve_initial_kexinit (line 231) | def retrieve_initial_kexinit(host: str, port: int) -> Tuple[List, List]:
  function return_socket_for_host (line 244) | def return_socket_for_host(host, port):
  function _pack_packet (line 252) | def _pack_packet(packet):
  function main (line 270) | def main():
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (12K chars).
[
  {
    "path": "LICENSE",
    "chars": 1064,
    "preview": "MIT License\n\nCopyright (c) 2021 Vincent\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
  },
  {
    "path": "README.md",
    "chars": 406,
    "preview": "SSHScan\n=======\n\nSSHScan is a testing tool that enumerates SSH Ciphers.<br>\nUsing SSHScan, weak ciphers can be easily de"
  },
  {
    "path": "config.yml",
    "chars": 539,
    "preview": "ciphers: ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr"
  },
  {
    "path": "requirements.txt",
    "chars": 7,
    "preview": "PyYAML\n"
  },
  {
    "path": "sshscan.py",
    "chars": 9345,
    "preview": "#!/usr/bin/env python3\n\nimport sys\nimport socket\nimport struct\nfrom yaml import safe_load\nfrom typing import List, Tuple"
  }
]

About this extraction

This page contains the full source code of the evict/SSHScan GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (11.1 KB), approximately 3.2k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!