diff --git a/networks/networkConfigurationCredentialsExtract.py b/networks/networkConfigurationCredentialsExtract.py index a70c448..ff52452 100644 --- a/networks/networkConfigurationCredentialsExtract.py +++ b/networks/networkConfigurationCredentialsExtract.py @@ -3,8 +3,9 @@ # # Script intendend to sweep Cisco, Huawei and possibly other network devices # configuration files in order to extract plain and cipher passwords out of them. +# Equipped with functionality to decrypt Cisco Type 7 passwords. # -# Mariusz B., mgeeky '18 +# Mariusz B., mgeeky '18-20 # import re @@ -45,6 +46,10 @@ regexes = { 'ISAKMP Pre-Shared Key' : r'crypto isakmp key \password(?: address \ip)?', 'SNMP-Server User Auth & Encr keys' : r'snmp-server user \name .* encrypted auth md5 ([0-9a-f\:]+) priv aes \d+ ([0-9a-f\:]+)', 'PPP PAP Sent Username & Password' : r'ppp pap sent-username \name password \password', + 'AAA TACACS+/RADIUS Server Private' : r'server-private \ip key \password', + 'AAA TACACS+ Server Private' : r'tacacs-server key \password', + 'SNMP Server Community string' : r'snmp-server community \password', + 'IPSec VPN ISAKMP Pre-Shared Key' : r'pre-shared-key address \ip key \password' }, 'Cisco ASA' : { @@ -88,6 +93,7 @@ regexes = { 'Other uncategorized XML password' : r'password>([^<]+)<', 'Other uncategorized authentication string' : r'.* authentication \password.*', 'Other hash-key related' : r'.* key \hash', + 'Cisco 7 Password' : r'\cisco7', }, } @@ -95,9 +101,12 @@ config = { 'verbose' : False, 'debug' : False, 'lines' : 0, - 'output' : 'normal', + 'format' : 'normal', 'csv_delimiter' : ';', 'no_others' : False, + 'filename' : False, + 'nonunique' : False, + 'output' : '' } markers = { @@ -106,7 +115,8 @@ markers = { 'domain' : r'(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\.[a-zA-Z]{2,3})', 'hash' : r'([a-fA-F0-9]{20,})', 'bcrypt' : r'([\$\w\.\/]+)', - 'password': r'(?:\d\s+)?([^\s]+)', + 'password': r'(?:(?:\d\s+)?([^\s]+))', + 'cisco7' : r'\b(?:7 ([0-9a-f]{4,}))|(?:([0-9a-f]{4,}) 7)\b', 'keystring': r'([a-f0-9]+)', } @@ -115,7 +125,7 @@ foundCreds = set() maxTechnologyWidth = 0 maxRegexpWidth = 0 -results = set() +results = [] class Logger: @staticmethod @@ -156,7 +166,53 @@ def processRegex(inputRegex): inputRegex = '^\\s*{}\\s*.*$'.format(inputRegex) return inputRegex -def matchLines(lines, technology): +def cisco7Decrypt(data): + # source: https://github.com/theevilbit/ciscot7 + xlat = [ + 0x64, 0x73, 0x66, 0x64, 0x3b, 0x6b, 0x66, 0x6f, 0x41, 0x2c, 0x2e, + 0x69, 0x79, 0x65, 0x77, 0x72, 0x6b, 0x6c, 0x64, 0x4a, 0x4b, 0x44, + 0x48, 0x53, 0x55, 0x42, 0x73, 0x67, 0x76, 0x63, 0x61, 0x36, 0x39, + 0x38, 0x33, 0x34, 0x6e, 0x63, 0x78, 0x76, 0x39, 0x38, 0x37, 0x33, + 0x32, 0x35, 0x34, 0x6b, 0x3b, 0x66, 0x67, 0x38, 0x37 + ] + + dp = '' + regex = re.compile(r'(^[0-9A-Fa-f]{2})([0-9A-Fa-f]+)') + result = regex.search(data) + try: + if result: + s, e = int(result.group(1)), result.group(2) + for pos in range(0, len(e), 2): + magic = int(e[pos] + e[pos+1], 16) + newchar = '' + if s <= 50: + # xlat length is 51 + newchar = '%c' % (magic ^ xlat[s]) + s += 1 + if s == 51: s = 0 + dp += newchar + return dp + return '' + except: + return '' + +def tryToCisco7Decrypt(creds): + if not len(creds): + return '' + + decrypted = [] + for m in re.finditer(markers['cisco7'], creds, re.I): + f = m.group(2) if m.group(2) != None else m.group(1) + out = cisco7Decrypt(f) + if out: + decrypted.append(out) + + if len(decrypted): + return " (decrypted cisco 7: '" + "', '".join(decrypted) + "')" + + return '' + +def matchLines(file, lines, technology): global foundCreds global results @@ -166,7 +222,7 @@ def matchLines(lines, technology): for idx in range(len(lines)): line = lines[idx].strip() - if line in foundCreds: + if not config['nonunique'] and line in foundCreds: continue processedRex = processRegex(regexes[technology][rex]) @@ -175,28 +231,28 @@ def matchLines(lines, technology): num += 1 foundCreds.add(line) - creds = '", "'.join(matched.groups(1)) + f = [x for x in matched.groups(1) if type(x) == str] + creds = '", "'.join(f) + creds += tryToCisco7Decrypt(line) - results.add(( - technology, rex, creds + results.append(( + file, technology, rex, creds )) - Logger._out('[+] {}: {}: {}'.format( - technology, rex, creds - )) - - if idx - config['lines'] >= 0: - for i in range(idx - config['lines'], idx): - Logger._out('[{:04}]\t\t{}'.format(i, lines[i])) - if config['lines'] != 0: - Logger._out('[{:04}]==>\t{}'.format(idx, line)) - else: - Logger._out('[{:04}]\t\t{}'.format(idx, line)) + Logger._out('\n[+] {}: {}: {}'.format( + technology, rex, creds + )) - if idx + 1 + config['lines'] < len(lines): - for i in range(idx + 1, idx + config['lines'] + 1): - Logger._out('[{:04}]\t\t{}'.format(i, lines[i])) + if idx - config['lines'] >= 0: + for i in range(idx - config['lines'], idx): + Logger._out('[{:04}]\t\t{}'.format(i, lines[i])) + + Logger._out('[{:04}]==>\t{}'.format(idx, line)) + + if idx + 1 + config['lines'] < len(lines): + for i in range(idx + 1, idx + config['lines'] + 1): + Logger._out('[{:04}]\t\t{}'.format(i, lines[i])) Logger.dbg('\tRegex used: [ {} ]'.format(processedRex)) return num @@ -205,21 +261,23 @@ def processFile(file): lines = [] Logger.info('Processing file: "{}"'.format(file)) - with open(file, 'r') as f: - lines = [ line.strip() for line in f.readlines()] + try: + with open(file, 'r') as f: + lines = [ line.strip() for line in f.readlines()] + except Exception as e: + Logger.err("Parsing file '{}' failed: {}.".format(file, str(e))) + return 0 num = 0 for technology in regexes: if technology == 'Others': continue - num0 = matchLines(lines, technology) + num0 = matchLines(file, lines, technology) num += num0 if not config['no_others']: - num0 = matchLines(lines, 'Others') - if num0 == 0: - print('') + num0 = matchLines(file, lines, 'Others') num += num0 return num @@ -237,11 +295,14 @@ def processDir(dirname): def parseOptions(argv): parser = argparse.ArgumentParser(prog = argv[0], usage='%(prog)s [options] ') parser.add_argument('file', metavar='', type=str, help='Config file or directory to process.') + parser.add_argument('-o', '--output', help = 'Output file.') + parser.add_argument('-H', '--with-filename', action='store_true', help = 'Print file name next to the results') + parser.add_argument('-R', '--show-nonunique', action='store_true', help = 'Print repeated, non unique credentials found. By default only unique references are returned.') parser.add_argument('-C', '--lines', metavar='N', type=int, default=0, help='Display N lines around matched credential if verbose output is enabled.') parser.add_argument('-f', '--format', choices=['raw', 'normal', 'tabular', 'csv'], default='normal', help="Specifies output format: 'raw' (only hashes), 'tabular', 'normal', 'csv'. Default: 'normal'") parser.add_argument('-N', '--no-others', dest='no_others', action='store_true', help='Don\'t match "Others" category which is false-positives prone.') - parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose output.') - parser.add_argument('-d', '--debug', action='store_true', help='Display debug output.') + parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Display verbose output.') + parser.add_argument('-d', '--debug', action='store_true', default=False, help='Display debug output.') if len(argv) < 2: parser.print_help() @@ -253,32 +314,40 @@ def parseOptions(argv): config['debug'] = args.debug config['lines'] = args.lines config['no_others'] = args.no_others + config['filename'] = args.with_filename + config['nonunique'] = args.show_nonunique + config['output'] = args.output if args.format == 'raw': - config['output'] = 'raw' + config['format'] = 'raw' elif args.format == 'tabular': - config['output'] = 'tabular' + config['format'] = 'tabular' elif args.format == 'csv': - config['output'] = 'csv' + config['format'] = 'csv' else: - config['output'] == 'normal' + config['format'] == 'normal' return args def printResults(): global maxTechnologyWidth global maxRegexpWidth + global results # CSV Columns - cols = ['technology', 'name', 'hashes'] + cols = ['file', 'technology', 'name', 'hashes'] - def _print(technology, rex, creds): - if config['output'] == 'tabular': - print('[+] {0: <{width1}} {1:^{width2}}: "{2:}"'.format( + if not config['nonunique']: + results = set(results) + + def _print(file, technology, rex, creds): + out = '' + if config['format'] == 'tabular': + out += '[+] {0: <{width1}} {1:^{width2}}: "{2:}"\n'.format( technology, rex, creds, width1 = maxTechnologyWidth, width2 = maxRegexpWidth - )) - elif config['output'] == 'raw': + ) + elif config['format'] == 'raw': credstab = creds.split('", "') longest = '' @@ -286,24 +355,26 @@ def printResults(): if len(passwd) > len(longest): longest = passwd - print('{}'.format( + out += '{}\n'.format( passwd - )) - elif config['output'] == 'csv': + ) + elif config['format'] == 'csv': creds = '"{}"'.format(creds) rex = rex.replace(config['csv_delimiter'], ' ') - #creds = creds.replace(config['csv_delimiter'], ' ') - print(config['csv_delimiter'].join([technology, rex, creds])) + out += config['csv_delimiter'].join([file, technology, rex, creds]) + out += '\n' else: - print('[+] {}: {}: "{}"'.format( + out += '[+] {}: {}: "{}"\n'.format( technology, rex, creds - )) + ) + + return out maxTechnologyWidth = 0 maxRegexpWidth = 0 for result in results: - technology, rex, creds = result + file, technology, rex, creds = result if len(technology) > maxTechnologyWidth: maxTechnologyWidth = len(technology) @@ -313,23 +384,45 @@ def printResults(): maxTechnologyWidth = maxTechnologyWidth + 3 maxRegexpWidth = maxRegexpWidth + 3 - if config['output'] == 'normal' or config['output'] == 'tabular': - print('\n=== CREDENTIALS FOUND:') - elif config['output'] == 'csv': - print(config['csv_delimiter'].join(cols)) + outputToPrint = '' + if config['format'] == 'normal' or config['format'] == 'tabular': + outputToPrint += '\n=== CREDENTIALS FOUND:\n' + elif config['format'] == 'csv': + outputToPrint += config['csv_delimiter'].join(cols) + outputToPrint += '\n' + + resultsPerFile = {} + otherResultsPerFile = {} for result in results: - technology, rex, creds = result - if technology == 'Others': continue - _print(technology, rex, creds) + file, technology, rex, creds = result + if technology == 'Others': + if file not in otherResultsPerFile.keys(): + otherResultsPerFile[file] = [] + otherResultsPerFile[file].append((technology, rex, creds)) + else: + if file not in resultsPerFile.keys(): + resultsPerFile[file] = [] + resultsPerFile[file].append((technology, rex, creds)) - if not config['no_others'] and (config['output'] == 'normal' or config['output'] == 'tabular'): - print('\n=== BELOW LINES MAY BE FALSE POSITIVES:') + for file, _results in resultsPerFile.items(): + if config['filename'] and config['format'] in ['raw', 'normal', 'tabular']: + outputToPrint += '\nResults from file: "{}"\n'.format(file) + for result in _results: + technology, rex, creds = result + outputToPrint += _print(file, technology, rex, creds) - for result in results: - technology, rex, creds = result - if technology != 'Others': continue - _print(technology, rex, creds) + if not config['no_others'] and (config['format'] == 'normal' or config['format'] == 'tabular'): + outputToPrint += '\n\n=== BELOW LINES MAY BE FALSE POSITIVES:\n' + + for file, _results in otherResultsPerFile.items(): + if config['filename'] and config['format'] in ['raw', 'normal', 'tabular']: + outputToPrint += '\nResults from file: "{}"\n'.format(file) + for result in _results: + technology, rex, creds = result + outputToPrint += _print(file, technology, rex, creds) + + return outputToPrint def main(argv): Logger._out(''' @@ -356,9 +449,16 @@ def main(argv): Logger.err('Please provide either file or directory on input.') return False - printResults() + out = printResults() - if config['output'] == 'normal' or config['output'] == 'tabular': + if config['output']: + Logger.info("Dumping credentials to the output file: '{}'".format(config['output'])) + with open(config['output'], 'w') as f: + f.write(out) + else: + print(out) + + if config['format'] == 'normal' or config['format'] == 'tabular': print('\n[>] Found: {} credentials.'.format(num)) if __name__ == '__main__':