diff --git a/bfg b/bfg index d643ae8..0451d40 100755 --- a/bfg +++ b/bfg @@ -1,6 +1,11 @@ #!/usr/bin/env python3 +import pdb +import IPython +from sys import exit + import argparse +from bruteloops.args import timezone_parser from bruteloops.db_manager import * from bruteloops.jitter import Jitter from bruteloops.brute import BruteForcer @@ -37,14 +42,17 @@ AART = \ # GLOBAL VARIABLES # ================ -# Shared logger object -logger=None +def initLoggers(timezone=None): + + db_logger = getLogger('bfg.dbmanager', + log_level=10, + timezone=timezone) -# Database manager object -manager=None + brute_logger = getLogger('bfg', + log_level=10, + timezone=timezone) -# Shared args variables -args=None + return db_logger, brute_logger def ymlBoolToFlag(flag:str, b:bool) -> str: '''Parse a YAML boolean value to it's corresponding --{flag} @@ -109,6 +117,16 @@ def findFile(path) -> Path: raise FileNotFoundError(args.yaml_file) return path +def processYmlArg(param, arg) -> list: + + param = swapScore(param) + if isinstance(arg, bool): + return [ymlBoolToFlag(flag=param, b=arg)] + elif isinstance(arg, list): + return [BFT.format(flag=param), *ymlListToValue(arg)] + else: + return [FT.format(flag=param, value=arg)] + def parseYml(f, key_checks:list=None) -> dict: '''Load an open YAML file into memory as a JSON object, ensure that each high-level key is supplied in key_checks, @@ -132,11 +150,12 @@ def parseYml(f, key_checks:list=None) -> dict: except Exception as e: print( - '\n\n{e}\n\n' + f'\n\n{e}\n\n' 'Failed to parse the YAML file due to the above error.\n' 'Is it properly formatted?\n' "Here's a quick linter: " 'https://codebeautify.org/yaml-validator') + exit() keys = values.keys() @@ -167,8 +186,8 @@ def get_user_input(m:str) -> str: return uinput -def run_db_command(parser:argparse.ArgumentParser, args=None, - manager=None, associate_spray_values=True) -> None: +def run_db_command(parser:argparse.ArgumentParser, logger, + args=None, manager=None, associate_spray_values=True) -> None: '''Run a database management command. Args: @@ -189,13 +208,6 @@ def run_db_command(parser:argparse.ArgumentParser, args=None, parser.print_help() exit() - # ================= - # CONFIGURE LOGGING - # ================= - - logger = getLogger('bfg.dbmanager', log_level=10) - logger.info('Initializing database manager') - # ======================= # HANDLE MISSING DATABASE # ======================= @@ -235,13 +247,11 @@ def run_db_command(parser:argparse.ArgumentParser, args=None, # EXECUTE THE SUBCOMMAND # ====================== - logger.info(f'Executing command') if args.cmd == handle_values: args.cmd(args, logger, manager, associate_spray_values=associate_spray_values) else: args.cmd(args, logger, manager) - logger.info('Execution finished. Exiting.') def handle_keyboard_interrupt(brute,exception): @@ -296,14 +306,14 @@ if __name__ == '__main__': # Database management db_sp = cli_subparsers.add_parser('manage-db', - parents=[db_parser], + parents=[db_parser, timezone_parser], description='Manage the attack database.', help='Manage the attack database.') db_sp.set_defaults(parser=db_sp, mode='db') # Brute force brute_sp = cli_subparsers.add_parser('brute-force', - parents=[modules_parser], + parents=[modules_parser, timezone_parser], description='Perform a brute-force attack.', help='Perform a brute-force attack.') @@ -338,6 +348,20 @@ if __name__ == '__main__': args.parser.print_help() exit() + db_logger, brute_logger = None, None + + # =================== + # HANDLE THE TIMEZONE + # =================== + + timezone = None + if hasattr(args, 'timezone'): + timezone = args.timezone + del(args.timezone) + + if timezone: + db_logger, brute_logger = initLoggers(timezone) + # ===================== # HANDLE YAML ARGUMENTS # ===================== @@ -353,6 +377,13 @@ if __name__ == '__main__': with path.open() as yfile: yargs = parseYml(yfile, key_checks=('database',)) + if 'timezone' in yargs: + timezone = yargs['timezone'] + del(yargs['timezone']) + + if not db_logger or not brute_logger: + db_logger, brute_logger = initLoggers(timezone) + db_arg = '--database=' + yargs['database'] db_args = yargs.get('manage-db', {}) @@ -376,21 +407,10 @@ if __name__ == '__main__': 'arguments.') _args = [cmd, db_arg] - for flag, values in argset.items(): + _args += processYmlArg(param=flag, arg=values) - if isinstance(values, bool): - - _args.append( - ymlBoolToFlag(flag=flag, b=values)) - - else: - - values = ymlListToValue(values) - - _args += [BFT.format(flag=swapScore(flag))]+values - - run_db_command(db_sp, _args, manager=manager, + run_db_command(db_sp, db_logger, _args, manager=manager, associate_spray_values=False) manager.associate_spray_values() @@ -414,56 +434,44 @@ if __name__ == '__main__': for k,v in bf_args.items(): if k != 'module': - - # ============================= - # CAPTURE A NON-MODULE ARGUMENT - # ============================= - - if isinstance(v, bool): - - brute_cli_args.append( - ymlBoolToFlag(flag=k, b=v)) - else: + # ==================== + # NON-MODULE ARGUMENTS + # ==================== - brute_cli_args.append( - FT.format(flag=swapScore(k), value=v)) + # Capture non-module arguments + brute_cli_args += processYmlArg( + param=k, + arg=v) else: - # ========================= - # CAPTURE A MODULE ARGUMENT - # ========================= + # =============== + # MODULE ARGUMENT + # =============== - name, args = v.get('name'), v.get('args') + name, args = v.get('name'), v.get('args', {}) if not name: raise ValueError( f'"name" field must be defined under "module".') - - elif not args: - - raise ValueError( - f'"args" field must be defined under "module".') - + + # Append the module name to the argument list brute_cli_args.append(name) for ik, iv in args.items(): - # Convert list arguments back to a - # space delimited string value - if isinstance(iv, list): - iv = ' '.join(iv) - - brute_cli_args.append( - FT.format( - flag=swapScore(ik), - value=iv)) + brute_cli_args += processYmlArg( + param=ik, + arg=iv) brute_cli_args.append(db_arg) args = brute_sp.parse_args(brute_cli_args) + if not db_logger or not brute_logger: + db_logger, brute_logger = initLoggers(timezone) + if args.mode == 'db': # ======================== @@ -474,7 +482,7 @@ if __name__ == '__main__': db_parser.print_help() exit() - run_db_command(parser) + run_db_command(parser, db_logger) if args.mode == 'brute': @@ -526,25 +534,25 @@ if __name__ == '__main__': # Log Levels config.log_level = args.log_level - config.timezone = args.timezone - config.blackout_start = args.blackout_start - config.blackout_stop = args.blackout_stop + config.timezone = timezone + + if hasattr(args, 'blackout_start') and \ + hasattr(args, 'blackout_stop'): + config.blackout_start = args.blackout_start + config.blackout_stop = args.blackout_stop # Configure an exception handler for keyboard interrupts config.exception_handlers={KeyboardInterrupt:handle_keyboard_interrupt} # Always validate the configuration. config.validate() - - # Configure logging - logger = getLogger('bfg', log_level=10) try: - logger.info('Initializing attack') + brute_logger.info('Initializing attack') bf = BruteForcer(config) bf.launch() - logger.info('Attack complete') + brute_logger.info('Attack complete') except Exception as e: diff --git a/setup.py b/setup.py index 1953ef3..fb24611 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setuptools.setup( name='bl-bfg', - version='0.5.1', + version='0.5.2', author='Justin Angel', author_email='justin@arch4ngel.ninja', description='A simple password guessing framework.', diff --git a/src/bfg/args/http.py b/src/bfg/args/http.py index aa386cb..42d5b87 100644 --- a/src/bfg/args/http.py +++ b/src/bfg/args/http.py @@ -34,6 +34,7 @@ def proxies(name_or_flags=('--proxies',), @argument def headers(name_or_flags=('--headers',), + nargs='+', required=False, help='Space delimited static HTTP headers to pass along to ' 'each request. Note that each header must be formatted ' diff --git a/src/bfg/cli/manage_db.py b/src/bfg/cli/manage_db.py index 2a23331..187d04c 100644 --- a/src/bfg/cli/manage_db.py +++ b/src/bfg/cli/manage_db.py @@ -1,9 +1,6 @@ from bruteloops import args as blargs -from logging import getLogger import argparse -logger = getLogger('bfg.db_cmd') - def dump_valid(args, logger, manager): '''Write valid credentials to stdout ''' @@ -42,11 +39,9 @@ def dump_strict_credentials(args, logger, manager): logger.info('Strict credentials dumped') - def handle_values(args, logger, manager, associate_spray_values=True): '''Insert or delete values from the database. ''' - new_args = dict( as_credentials = args.as_credentials, insert = args.action == 'insert', @@ -57,7 +52,7 @@ def handle_values(args, logger, manager, associate_spray_values=True): 'csv_files']: if hasattr(args,handle): new_args[handle] = getattr(args,handle) - manager.manage_db_values(**new_args) + manager.manage_db_values(**new_args, logger=logger) def prioritize_values(args, logger, manager): '''Prioritize or unprioritize values @@ -117,39 +112,32 @@ def associate_spray_values(args, logger, manager): parser_dump_strict_credentials = subparsers.add_parser( 'dump-credential-values', - description='Dump scrict credentials, regardless of ' \ - 'of status from the database. This is a mean' \ - 's of identifying which static values have b' \ - 'een imported and can be used to obtain a li' \ - 'st of values to be deleted from the attack.' \ - ' Use the dump_valid subcommand to dump vali' \ - 'values, including strict records, from the ' \ - 'database.', + description=('Dump scrict credentials, regardless of ' + 'of status from the database. This is a mean' + 's of identifying which static values have b' + 'een imported and can be used to obtain a li' + 'st of values to be deleted from the attack.' + ' Use the dump_valid subcommand to dump vali' + 'values, including strict records, from the ' + 'database.'), help='Dump all credential values from the database.', parents=[db_flag], add_help=False) -parser_dump_strict_credentials.add_argument( - '--credential-delimiter', - default=':', - help='Character delimiting the username to password valu' \ - 'e. Default: ":"') -parser_dump_strict_credentials.set_defaults( - cmd=dump_strict_credentials) # ======================== # IMPORT VALUES SUBCOMMAND # ======================== parser_import_values = subparsers.add_parser('import-spray-values', - description='Import username and password values ' \ - 'into the target database. NOTE: if crede' \ - 'ntial inputs are provided, they will be ' \ - 'split into individual username and passw' \ - 'ord values and imported for spraying. Us' \ - 'the import-credentials subommand if you ' \ - 'wish to import individual credentials th' \ - 'at will be paired individualy in the dat' \ - 'base.', + description=('Import username and password values ' + 'into the target database. NOTE: if crede' + 'ntial inputs are provided, they will be ' + 'split into individual username and passw' + 'ord values and imported for spraying. Us' + 'the import-credentials subommand if you ' + 'wish to import individual credentials th' + 'at will be paired individualy in the dat' + 'base.'), help='Import values into the target database', parents=[db_flag, blargs.input_parser,blargs.credential_parser], add_help=False) @@ -162,14 +150,14 @@ def associate_spray_values(args, logger, manager): parser_import_credentials = subparsers.add_parser( 'import-credential-values', - description='Import credential values into the target' \ - ' database. The username to password relations' \ - 'hip is maintained in the data meaning t' \ - 'hat individual guesses for the username to p' \ - 'assword combination will be scheduled. No ad' \ - 'ditional guesses will be made for the userna' \ - 'me and the password will not be used for gue' \ - 'sses targeting other usernames.', + description=('Import credential values into the target' + ' database. The username to password relations' + 'hip is maintained in the data meaning t' + 'hat individual guesses for the username to p' + 'assword combination will be scheduled. No ad' + 'ditional guesses will be made for the userna' + 'me and the password will not be used for gue' + 'sses targeting other usernames.'), help='Import credential pairs into the target database', parents=[db_flag, blargs.credential_parser], add_help=False) @@ -182,12 +170,12 @@ def associate_spray_values(args, logger, manager): # TODO: TEST ME; PAY ATTENTION TO CASCADING DELETIONS parser_delete_values = subparsers.add_parser('delete-spray-values', - description='Delete username and password values ' \ - 'from the target database. NOTE: if crede' \ - 'ntials are supplied, then the username a' \ - 'nd password values are removed from the ' \ - 'database entirely. Use the delete-creden' \ - 'tials subcommand if you wish to delete c' \ + description='Delete username and password values ' + 'from the target database. NOTE: if crede' + 'ntials are supplied, then the username a' + 'nd password values are removed from the ' + 'database entirely. Use the delete-creden' + 'tials subcommand if you wish to delete c' 'redential records', help='Delete values from the target database', parents=[db_flag,blargs.input_parser,blargs.credential_parser], @@ -198,16 +186,15 @@ def associate_spray_values(args, logger, manager): # DELETE CREDENTIALS SUBCOMMAND # ============================= # TODO: TEST ME - parser_delete_credentials = subparsers.add_parser( 'delete-credential-values', - description='Delete credential values from the target' \ - 'database. This targets password to username ' \ - 'relationships, but a given username or passw' \ - 'will be removed from the database entirely s' \ - 'hould either no longer be associated with fu' \ - 'ture guesses after the associations have bee' \ - 'n removed', + description=('Delete credential values from the target' + 'database. This targets password to username ' + 'relationships, but a given username or passw' + 'will be removed from the database entirely s' + 'hould either no longer be associated with fu' + 'ture guesses after the associations have bee' + 'n removed'), help='Delete credential pairs from the target database', parents=[db_flag, blargs.credential_parser], add_help=False) diff --git a/src/bfg/data.py b/src/bfg/data.py index 90d06eb..93f0f17 100644 --- a/src/bfg/data.py +++ b/src/bfg/data.py @@ -3,9 +3,6 @@ # made available. Call loader functions to populate the variables. from pathlib import Path -from logging import getLogger - -log = getLogger('bfg.data') DATASETS_PATH = Path(__file__).parent / 'datasets' USER_AGENT_STRINGS = UAS = [] @@ -49,8 +46,6 @@ def loadUserAgents(path:str=None, force=False) -> None: if not path.exists(): - log.general(f'Fatal: User agent string source missing!') - raise FileNotFoundError( 'Source for user agent strings source was not found: ' + str(path)) diff --git a/src/bfg/modules/http/adfs/module.py b/src/bfg/modules/http/adfs/module.py index e9cf709..32fd48d 100644 --- a/src/bfg/modules/http/adfs/module.py +++ b/src/bfg/modules/http/adfs/module.py @@ -4,7 +4,6 @@ from logging import getLogger,INFO from time import sleep -brute_logger = getLogger('BruteLoops.example.modules.http.adfs') getLogger('urllib3.connectionpool').setLevel(INFO) class Module(HTTPModule): diff --git a/src/bfg/shortcuts/azure.py b/src/bfg/shortcuts/azure.py index d1f1069..6aad223 100644 --- a/src/bfg/shortcuts/azure.py +++ b/src/bfg/shortcuts/azure.py @@ -65,6 +65,17 @@ def lookupCode(status_code:int, error_code:str) -> (int, bool, [str]): return CRED_FAILED, USERNAME_INVALID, [message] + elif error_code == 'AADSTS90019': + + # ===================== + # NO TENANT INFORMATION + # ===================== + + message += 'No tenant-identifying information supplied in ' \ + 'the authentication data, e.g. domain name.' + + return CRED_FAILED, USERNAME_INVALID, [message] + elif error_code == 'AADSTS50056': # ======================================= @@ -110,7 +121,10 @@ def lookupCode(status_code:int, error_code:str) -> (int, bool, [str]): # SOMETHING WENT....EVEN WORSE? # ============================= - message += 'Unhandled Azure AD error code!' + message += 'Unhandled Azure AD error code' + if error_code in ERROR_CODES: + message += '-> {}'.format(ERROR_CODES[error_code]) + return CRED_FAILED, True, [message] def getRandomListItem(lst:list): diff --git a/src/bfg/shortcuts/http.py b/src/bfg/shortcuts/http.py index 513b750..b31539a 100644 --- a/src/bfg/shortcuts/http.py +++ b/src/bfg/shortcuts/http.py @@ -8,11 +8,8 @@ from functools import wraps from random import randint from inspect import getargspec -from logging import getLogger,INFO import pdb -brute_logger = getLogger('BruteLoops.example.shortcuts.http') - warnings.filterwarnings('ignore') PROXY_FORMAT=':' PROXY_EXAMPLE='https:http://127.0.0.1:8080' diff --git a/brute_sample.yml b/yaml_examples/attack_full.yml similarity index 61% rename from brute_sample.yml rename to yaml_examples/attack_full.yml index 76c985e..9c638db 100644 --- a/brute_sample.yml +++ b/yaml_examples/attack_full.yml @@ -1,4 +1,4 @@ -database: /tmp/test.db +database: full_attack_example.db manage-db: # =============== @@ -7,34 +7,24 @@ manage-db: import-credential-values: - # credential-delimiter: ":" - credentials: - username1:password1 - username2:password3 - # credential-files: - # - /tmp/credentials.txt - # csv-files: - # - /tmp/credentials.csv - import-spray-values: usernames: - username2 - username3 - username4 - - # username-files: - # - /tmp/usernames.txt passwords: - password2 - password3 - password4 - # password-files: - # - /tmp/passwords.txt + credentials: + - username5:password5 prioritize-values: prioritize: true @@ -50,17 +40,17 @@ brute-force: # ATTACK CONFIGURATIONS # ===================== - log-file: /tmp/test.log + log-file: full_attack_example.log stop-on-valid: false - parallel-guess-count: 4 - auth-threshold: 2 + parallel-guess-count: 1 + auth-threshold: 1 - auth-jitter-min: 1s - auth-jitter-max: 10s + auth-jitter-min: 0.01s + auth-jitter-max: 0.01s - threshold-jitter-min: 1m - threshold-jitter-max: 3m + threshold-jitter-min: 1s + threshold-jitter-max: 3s module: name: testing.fake diff --git a/yaml_examples/attack_ms_graph.yml b/yaml_examples/attack_ms_graph.yml new file mode 100644 index 0000000..757d8be --- /dev/null +++ b/yaml_examples/attack_ms_graph.yml @@ -0,0 +1,48 @@ +database: attack_ms_graph_example.db +brute-force: + + # ===================== + # ATTACK CONFIGURATIONS + # ===================== + # + # This attack configuration is conservative: + # + # - Guesses only 1 user at a time + # - Two passwords for that user + # - 1-5 seconds rest between guesses + # - 1.5-2 hour wait between guess rounds + # - Randomized client ID + # - Randomized resource URL + + log-file: attack_ms_graph_example.log + + stop-on-valid: false + parallel-guess-count: 1 + auth-threshold: 2 + + auth-jitter-min: 1s + auth-jitter-max: 5s + + threshold-jitter-min: 1.5h + threshold-jitter-max: 2h + + module: + name: ms_graph + args: + client-id: RANDOM + resource-url: RANDOM + + # For FireProx URL + #url: https://your/fireprox/url + + # Uncomment to randomize user agent + # Defaults to MS Teams :) + #user-agent: RANDOM + + # For SOCKS/HTTP proxies + #proxies: + ## HTTP (Burp) + #- https:http://127.0.0.1:8080 + # SOCKS + #- https:socks://127.0.0.1:1080 + diff --git a/yaml_examples/db_values.yml b/yaml_examples/db_values.yml new file mode 100644 index 0000000..21435a7 --- /dev/null +++ b/yaml_examples/db_values.yml @@ -0,0 +1,35 @@ +database: db_values_example.db +manage-db: + + # =============== + # DATABASE VALUES + # =============== + + import-credential-values: + + credentials: + - username1:password1 + - username2:password3 + + import-spray-values: + + usernames: + - username2 + - username3 + - username4 + + passwords: + - password2 + - password3 + - password4 + + credentials: + - username5:password5 + + prioritize-values: + prioritize: true + usernames: + - username3 + - username4 + passwords: + - password2 diff --git a/yaml_examples/db_values_from_file.yml b/yaml_examples/db_values_from_file.yml new file mode 100644 index 0000000..9eeaf43 --- /dev/null +++ b/yaml_examples/db_values_from_file.yml @@ -0,0 +1,53 @@ +database: db_values_from_file_example.db +manage-db: + + # =============== + # DATABASE VALUES + # =============== + + import-credential-values: + + credentials: + - username1:password1 + - username2:password3 + + credential-files: + # Must be newline delimited lines like > username:password + - /tmp/credentials.txt + + import-spray-values: + + usernames: + - username2 + - username3 + - username4 + + passwords: + - password2 + - password3 + - password4 + + credentials: + # Will be parsed as individual spray values + - username5:password5 + + username-files: + # Must be newline delimited username values + - /tmp/usernames.txt + + password-files: + # Must be newline delimited password values + - /tmp/passwords.txt + + credential-files: + # Must be newline delimited lines like > username:password + # Will be imported as individual spray values + - /tmp/credentials.txt + + prioritize-values: + prioritize: true + usernames: + - username3 + - username4 + passwords: + - password2