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()
gitextract_tna4p_bf/ ├── LICENSE ├── README.md ├── config.yml ├── requirements.txt └── sshscan.py
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.