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()