#! /usr/bin/python3
# -*- coding: utf-8 -*-
from argparse import RawTextHelpFormatter
import readline, glob
import sys, time, os
import subprocess
import xml.etree.ElementTree as ET
import re
import argparse
import threading
import itertools
import tempfile
import shutil
import json
from multiprocessing import Process

services = {}
loading = False

class colors:
    white = "\033[1;37m"
    normal = "\033[0;00m"
    red = "\033[1;31m"
    blue = "\033[1;34m"
    green = "\033[1;32m"
    lightblue = "\033[0;34m"

banner = colors.red + r"""
                              #@                           @/              
                           @@@                               @@@           
                        %@@@                                   @@@.        
                      @@@@@                                     @@@@%      
                     @@@@@                                       @@@@@     
                    @@@@@@@                  @                  @@@@@@@    
                    @(@@@@@@@%            @@@@@@@            &@@@@@@@@@    
                    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    
                     @@*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@     
                       @@@( @@@@@#@@@@@@@@@*@@@,@@@@@@@@@@@@@@@  @@@       
                           @@@@@@ .@@@/@@@@@@@@@@@@@/@@@@ @@@@@@           
                                  @@@   @@@@@@@@@@@   @@@                  
                                 @@@@*  ,@@@@@@@@@(  ,@@@@                 
                                 @@@@@@@@@@@@@@@@@@@@@@@@@                 
                                  @@@.@@@@@@@@@@@@@@@ @@@                  
                                    @@@@@@ @@@@@ @@@@@@                    
                                       @@@@@@@@@@@@@                       
                                       @@   @@@   @@                       
                                       @@ @@@@@@@ @@                       
                                         @@% @  @@                 

"""+'\n' \
+ r"""
        ██████╗ ██████╗ ██╗   ██╗████████╗███████╗███████╗██████╗ ██████╗  █████╗ ██╗   ██╗
        ██╔══██╗██╔══██╗██║   ██║╚══██╔══╝██╔════╝██╔════╝██╔══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝
        ██████╔╝██████╔╝██║   ██║   ██║   █████╗  ███████╗██████╔╝██████╔╝███████║ ╚████╔╝ 
        ██╔══██╗██╔══██╗██║   ██║   ██║   ██╔══╝  ╚════██║██╔═══╝ ██╔══██╗██╔══██║  ╚██╔╝  
        ██████╔╝██║  ██║╚██████╔╝   ██║   ███████╗███████║██║     ██║  ██║██║  ██║   ██║   
        ╚═════╝ ╚═╝  ╚═╝ ╚═════╝    ╚═╝   ╚══════╝╚══════╝╚═╝     ╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝   
                                                                                   
"""+'\n' \
+ '\n brutespray.py v1.6.8' \
+ '\n Created by: Shane Young/@x90skysn3k && Jacob Robles/@shellfail' \
+ '\n Inspired by: Leon Johnson/@sho-luv' \
+ '\n Credit to Medusa: JoMo-Kun / Foofus Networks <jmk@foofus.net>\n' + colors.normal
#ascii art by: Cara Pearson

quiet_banner = colors.red + '~ BruteSpray ~' + colors.normal

class tabCompleter(object):

    def pathCompleter(self,text,state):
        line   = readline.get_line_buffer().split()

        return [x for x in glob.glob(text+'*')][state]

def interactive():
    t = tabCompleter()
    singluser = ""
    if args.interactive is True:
        print(colors.white + "\n\nWelcome to interactive mode!\n\n" + colors.normal)
        print(colors.red + "WARNING:" + colors.white + " Leaving an option blank will leave it empty and refer to default\n\n" + colors.normal)
        print("Available services to brute-force:")
        for serv in services:
            srv = serv
            for prt in services[serv]:
                iplist = services[serv][prt]
                port = prt
                plist = len(iplist)
                print("Service: " + colors.green + str(serv) + colors.normal + " on port " + colors.red + str(port) + colors.normal + " with " + colors.red + str(plist) + colors.normal + " hosts")

        args.service = input('\n' + colors.lightblue + 'Enter services you want to brute - default all (ssh,ftp,etc): ' + colors.red)

        args.threads = input(colors.lightblue + 'Enter the number of parallel threads (default is 2): ' + colors.red)

        args.hosts = input(colors.lightblue + 'Enter the number of parallel hosts to scan per service (default is 1): ' + colors.red)

        if args.passlist is None or args.userlist is None:
            customword = input(colors.lightblue + 'Would you like to specify a wordlist? (y/n): ' + colors.red)
        if customword == "y":
            readline.set_completer_delims('\t')
            readline.parse_and_bind("tab: complete")
            readline.set_completer(t.pathCompleter)
            if args.userlist is None and args.username is None:
                args.userlist = input(colors.lightblue + 'Enter a userlist you would like to use: ' + colors.red)
                if args.userlist == "":
                    args.userlist = None
            if args.passlist is None and args.password is None:
                args.passlist = input(colors.lightblue + 'Enter a passlist you would like to use: ' + colors.red)
                if args.passlist == "":
                    args.passlist = None

        if args.username is None or args.password is None: 
            singluser = input(colors.lightblue + 'Would to specify a single username or password (y/n): ' + colors.red)
        if singluser == "y":
            if args.username is None and args.userlist is None:
                args.username = input(colors.lightblue + 'Enter a username: ' + colors.red)
                if args.username == "":
                    args.username = None
            if args.password is None and args.passlist is None:
                args.password = input(colors.lightblue + 'Enter a password: ' + colors.red)
                if args.password == "":
                    args.password = None

        if args.service == "":
            args.service = "all"
        if args.threads == "":
            args.threads = "2"
        if args.hosts == "":
            args.hosts = "1"

    print(colors.normal)

NAME_MAP = {"ms-sql-s": "mssql",
            "microsoft-ds": "smbnt",
            "pcanywheredata": "pcanywhere",
            "postgresql": "postgres",
            "shell": "rsh",
            "exec": "rexec",
            "login": "rlogin",
            "smtps": "smtp",
            "submission": "smtp",
            "imaps": "imap",
            "pop3s": "pop3",
            "iss-realsecure": "vmauthd",
            "snmptrap": "snmp"}

def make_dic_gnmap():
    global loading
    global services

    supported = ['ssh','ftp','postgres','telnet','mysql','ms-sql-s','shell',
                 'vnc','imap','imaps','nntp','pcanywheredata','pop3','pop3s',
                 'exec','login','microsoft-ds','smtp', 'smtps','submission',
                 'svn','iss-realsecure','snmptrap','snmp']


    port = None
    with open(args.file, 'r') as nmap_file:
        for line in nmap_file:
            for name in supported:
                matches = re.compile(r'([0-9][0-9]*)/open/[a-z][a-z]*//' + name)
                try:
                    port =  matches.findall(line)[0]
                except:
                    continue

                ip = re.findall( r'[0-9]+(?:\.[0-9]+){3}', line)
                tmp_ports = matches.findall(line)

                for tmp_port in tmp_ports:

                    name = NAME_MAP.get(name, name)

                    if name in services:
                        if tmp_port in services[name]:
                            services[name][tmp_port] += ip
                        else:
                            services[name][tmp_port] = ip
                    else:
                        services[name] = {tmp_port:ip}

    loading = True

def make_dic_xml():
    global loading
    global services
    supported = ['ssh','ftp','postgresql','telnet','mysql','ms-sql-s','rsh',
                 'vnc','imap','imaps','nntp','pcanywheredata','pop3','pop3s',
                 'exec','login','microsoft-ds','smtp','smtps','submission',
                 'svn','iss-realsecure','snmptrap','snmp']
    tree = ET.parse(args.file)
    root = tree.getroot()
    for host in root.iter('host'):
        ipaddr = host.find('address').attrib['addr']
        for port in host.iter('port'):
            cstate = port.find('state').attrib['state']
            if cstate == "open":
                try:
                    name = port.find('service').attrib['name']
                    tmp_port = port.attrib['portid']
                    iplist = ipaddr.split(',')
                except:
                    continue
                if name in supported:
                    name = NAME_MAP.get(name, name)
                    if name in services:
                        if tmp_port in services[name]:
                            services[name][tmp_port] += iplist
                        else:
                            services[name][tmp_port] = iplist
                    else:
                        services[name] = {tmp_port:iplist}

    loading = True

def make_dic_json():
    global loading
    global services

    supported = ['ssh','ftp','postgres','telnet','mysql','ms-sql-s','shell',
                 'vnc','imap','imaps','nntp','pcanywheredata','pop3','pop3s',
                 'exec','login','microsoft-ds','smtp', 'smtps','submission',
                 'svn','iss-realsecure','snmptrap','snmp']

    with open(args.file, "r") as jsonlines_file:
        for line in jsonlines_file:
            data = json.loads(line)
            try:
                host, port, name = data["host"], data["port"], data["service"]
                
                if name in supported:
                    name = NAME_MAP.get(name, name) 
                    if name not in services:
                        services[name] = {}
                    if port not in services[name]:
                        services[name][port] = []
                    if host not in services[name][port]:
                        services[name][port].append(host)
            except KeyError as e:
                sys.stderr.write("\n[!] Field: " + str(e) + "is missing")
                sys.stderr.write("\n[!] Please provide the json fields. ")               
                continue
    loading = True 

def brute(service,port,fname,output):
    if args.userlist is None and args.username is None:
        userlist = '/usr/share/brutespray/wordlist/'+service+'/user'
        if not os.path.exists(userlist):
            userlist = 'wordlist/'+service+'/user'
        uarg = '-U'
    elif args.userlist:
        userlist = args.userlist
        uarg = '-U'
    elif args.username:
        userlist = args.username
        uarg = '-u'

    if args.passlist is None and args.password is None:
        passlist = '/usr/share/brutespray/wordlist/'+service+'/password'
        if not os.path.exists(passlist):
            passlist = 'wordlist/'+service+'/password'
        parg = '-P'
    elif args.passlist:
        passlist = args.passlist
        parg = '-P'
    elif args.password:
        passlist = args.password
        parg = '-p'

    if args.continuous:
        cont = ''
    else:
        cont = '-F'
    if service == "smtp":
        aarg = "-m"
        auth = "AUTH:LOGIN"
    else:
        aarg = ''
        auth = ''

    p = subprocess.Popen(['medusa', '-b', '-H', fname, uarg, userlist, parg, passlist, '-M', service, '-t', args.threads, '-n', port, '-T', args.hosts, cont, aarg, auth], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=-1)

    out = "[" + colors.green + "+" + colors.normal + "] "
    output_file = output + '/' + port + '-' + service + '-success.txt'
    
 
    for line in iter(p.stdout.readline, b''):
        print(line.decode('utf-8').strip('\n'))
        sys.stdout.flush()
        time.sleep(0.0001)
        if 'SUCCESS' in line.decode('utf-8'):
            f = open(output_file, 'a')
            f.write(out + line.decode('utf-8'))
            f.close()

def animate():
    sys.stdout.write('\rStarting to brute, please make sure to use the right amount of ' + colors.green + 'threads(-t)' + colors.normal + ' and ' + colors.green + 'parallel hosts(-T)' + colors.normal + '...  \n')
    t_end = time.time() + 2
    for c in itertools.cycle(['|', '/', '-', '\\']):
        if not time.time() < t_end:
            break
        sys.stdout.write('\rOutput will be written to the folder: ./' + colors.green + args.output + colors.normal + "/ "+ c)
        sys.stdout.flush()
        time.sleep(0.1)
    sys.stdout.write('\n\nBrute-Forcing...     \n')
    time.sleep(1)

def loading():
    for c in itertools.cycle(['|', '/', '-', '\\']):
        if loading == True:
            break
        sys.stdout.write('\rLoading File: ' + c)
        sys.stdout.flush()
        time.sleep(0.01)

def getInput(filename):
    in_format = None
    with open(filename) as f:
        line = f.readlines()
        if filename.endswith("gnmap"):
            in_format = "gnmap"
        if filename.endswith("json"):
            in_format = "json"
        if filename.endswith("xml"):
            in_format = "xml"
        if '{' in line[0]:
            in_format = "json"
        if '# Nmap' in line[0] and not 'Nmap' in line[1]:
            in_format = "gnmap"
        if '<?xml ' in line[0]:
            in_format = "xml"
        if in_format is None:
            print('File is not correct format!\n')
            sys.exit(0)

    return in_format 

def parse_args():

    parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description=\

    "Usage: python brutespray.py <OPTIONS> \n")

    menu_group = parser.add_argument_group(colors.lightblue + 'Menu Options' + colors.normal)

    #menu_group.add_argument('-f', '--file', help="GNMAP or XML file to parse", required=False, default=None)
    menu_group.add_argument('-f', '--file', help="GNMAP, JSON or XML file to parse", required=False, default=None)
    menu_group.add_argument('-o', '--output', help="Directory containing successful attempts", default="brutespray-output")
    menu_group.add_argument('-s', '--service', help="specify service to attack", default="all")
    menu_group.add_argument('-t', '--threads', help="number of medusa threads", default="2")
    menu_group.add_argument('-T', '--hosts', help="number of hosts to test concurrently", default="1")
    menu_group.add_argument('-U', '--userlist', help="reference a custom username file", default=None)
    menu_group.add_argument('-P', '--passlist', help="reference a custom password file", default=None)
    menu_group.add_argument('-u', '--username', help="specify a single username", default=None)
    menu_group.add_argument('-p', '--password', help="specify a single password", default=None)
    menu_group.add_argument('-c', '--continuous', help="keep brute-forcing after success", default=False, action='store_true')
    menu_group.add_argument('-i', '--interactive', help="interactive mode", default=False, action='store_true')    
    menu_group.add_argument('-m', '--modules', help="dump a list of available modules to brute", default=False, action='store_true')    
    menu_group.add_argument('-q', '--quiet', help="supress banner", default=False, action='store_true')   

    args = parser.parse_args()

    if args.file is None and args.modules is False:
        parser.error("argument -f/--file is required")
    return args

if __name__ == "__main__":
    args = parse_args()
    
    if args.quiet == False:
        print(banner)
    else:
        print(quiet_banner)

    supported = ['ssh','ftp','telnet','vnc','mssql','mysql','postgresql','rsh',
                'imap','nntp','pcanywhere','pop3',
                'rexec','rlogin','smbnt','smtp',
                'svn','vmauthd','snmp']
    #temporary directory for ip addresses

    if args.modules is True:
        print(colors.lightblue + "Supported Services:\n" + colors.green)
        print(('\n'.join(supported)))
        print(colors.normal + "\n") 
    try:
        tmppath = tempfile.mkdtemp(prefix="brutespray-tmp")
    except:
        sys.stderr.write("\nError while creating brutespray temp directory.")
        exit(4)

    if not os.path.exists(args.output):
        os.mkdir(args.output)

    if os.system("command -v medusa > /dev/null") != 0:
        sys.stderr.write("Command medusa not found. Please install medusa before using brutespray")
        exit(3)

    if args.file is None:
        sys.exit(0)

    if args.passlist and not os.path.isfile(args.passlist):
        sys.stderr.write("Passlist given does not exist. Please check your file or path\n")
        exit(3)

    if args.userlist and not os.path.isfile(args.userlist):
        sys.stderr.write("Userlist given does not exist. Please check your file or path\n")
        exit(3)

    if os.path.isfile(args.file):        
        try:
            t = threading.Thread(target=loading)
            t.start()
            in_format = getInput(args.file)
            {
                "gnmap": make_dic_gnmap,
                "xml":   make_dic_xml,
                "json":  make_dic_json
            }[in_format]()
        except:
            print("\nFormat failed!\n")
            loading = True
            sys.exit(0)

        if args.interactive is True:
            interactive()

        animate()

        if services == {}:
            print("\nNo brutable services found.\n Please check your Nmap file.")
    else:
        print("\nError loading file, please check your filename.")

    to_scan = args.service.split(',')
    for service in services:
        if service in to_scan or to_scan == ['all']:
            for port in services[service]:
                fname = tmppath + '/' +service + '-' + port
                iplist = services[service][port]
                f = open(fname, 'w+')
                for ip in iplist:
                    f.write(ip + '\n')
                f.close()
                brute_process = Process(target=brute, args=(service,port,fname,args.output))
                brute_process.start()

    #need to wait for all of the processes to run...
    #shutil.rmtree(tmppath, ignore_errors=False, onerror=None)