diff --git a/.gitignore b/.gitignore index d8454ea6..2f5388b1 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ docs/_build/ # PyBuilder target/ +targets.txt # Jupyter Notebook .ipynb_checkpoints diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3d6417..1bf10821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 3.1.6 +- Added scanning and brute-forcing of multiple targets +- Scanned targets now have a summary output +- Stop scanning if the "skip" parameter is provided + ### 3.1.5 - Fix color bug that resulted in DOM XSS vulnerabilities not being reported on certain systems (Windows, macOS, iOS) diff --git a/core/log.py b/core/log.py index b05068cf..e6195948 100644 --- a/core/log.py +++ b/core/log.py @@ -137,6 +137,12 @@ def log_red_line(self, amount=60, level='INFO'): _switch_to_default_loggers(self) +def log_yellow_summary_line(self, amount=60, level='INFO'): + _switch_to_no_format_loggers(self) + _get_level_and_log(self, f"{yellow}{'=' * amount}{end}", level) + _switch_to_default_loggers(self) + + def log_no_format(self, msg='', level='INFO'): _switch_to_no_format_loggers(self) _get_level_and_log(self, msg, level) @@ -187,6 +193,8 @@ def setup_logger(name='xsstrike'): # Create logger method to only log a red line logger.red_line = MethodType(log_red_line, logger) + # Create logger method to only log a yellow line + logger.yellow_summary_line = MethodType(log_yellow_summary_line, logger) # Create logger method to log without format logger.no_format = MethodType(log_no_format, logger) # Create logger method to convert data to json and log with debug level diff --git a/core/requester.py b/core/requester.py index f99c1e68..dd79bb47 100755 --- a/core/requester.py +++ b/core/requester.py @@ -47,6 +47,6 @@ def requester(url, data, headers, GET, delay, timeout): logger.warning('WAF is dropping suspicious requests.') logger.warning('Scanning will continue after 10 minutes.') time.sleep(600) - except Exception as e: - logger.warning('Unable to connect to the target.') + except Exception: + logger.warning('Unable to connect to the target.\r') return requests.Response() diff --git a/core/utils.py b/core/utils.py index 512fae36..ee6fe2ae 100644 --- a/core/utils.py +++ b/core/utils.py @@ -201,7 +201,7 @@ def writer(obj, path): def reader(path): - with open(path, 'r') as f: + with open(path, 'r', encoding='utf-8', errors='ignore') as f: result = [line.rstrip( '\n').encode('utf-8').decode('utf-8') for line in f] return result diff --git a/core/wafDetector.py b/core/wafDetector.py index a167d132..1919ee69 100644 --- a/core/wafDetector.py +++ b/core/wafDetector.py @@ -17,12 +17,11 @@ def wafDetector(url, params, headers, GET, delay, timeout): # Opens the noise injected payload response = requester(url, params, headers, GET, delay, timeout) page = response.text - code = str(response.status_code) + code = response.status_code headers = str(response.headers) logger.debug('Waf Detector code: {}'.format(code)) logger.debug_json('Waf Detector headers:', response.headers) - - if int(code) >= 400: + if code and code >= 400: bestMatch = [0, None] for wafName, wafSignature in wafSignatures.items(): score = 0 @@ -33,7 +32,7 @@ def wafDetector(url, params, headers, GET, delay, timeout): if re.search(pageSign, page, re.I): score += 1 if codeSign: - if re.search(codeSign, code, re.I): + if re.search(codeSign, str(code), re.I): score += 0.5 # increase the overall score by a smaller amount because http codes aren't strong indicators if headersSign: if re.search(headersSign, headers, re.I): diff --git a/modes/bruteforcer.py b/modes/bruteforcer.py index 5d0a013f..a762990e 100644 --- a/modes/bruteforcer.py +++ b/modes/bruteforcer.py @@ -1,4 +1,6 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed import copy +from threading import Lock from urllib.parse import urlparse, unquote from core.colors import good, green, end @@ -7,9 +9,10 @@ from core.log import setup_logger logger = setup_logger(__name__) +lock = Lock() -def bruteforcer(target, paramData, payloadList, encoding, headers, delay, timeout): +def bruteforcer(target, paramData, payloadList, encoding, headers, delay, timeout, threadCount): GET, POST = (False, True) if paramData else (True, False) host = urlparse(target).netloc # Extracts host out of the url logger.debug('Parsed host to bruteforce: {}'.format(host)) @@ -20,20 +23,30 @@ def bruteforcer(target, paramData, payloadList, encoding, headers, delay, timeou if not params: logger.error('No parameters to test.') quit() - for paramName in params.keys(): - progress = 1 - paramsCopy = copy.deepcopy(params) - for payload in payloadList: - logger.run('Bruteforcing %s[%s%s%s]%s: %i/%i\r' % - (green, end, paramName, green, end, progress, len(payloadList))) - if encoding: - payload = encoding(unquote(payload)) - paramsCopy[paramName] = payload - response = requester(url, paramsCopy, headers, - GET, delay, timeout).text - if encoding: - payload = encoding(payload) - if payload in response: - logger.info('%s %s' % (good, payload)) - progress += 1 + progress = {'lap': 0} + with ThreadPoolExecutor(max_workers=threadCount) as executor: + + for paramName in params.keys(): + paramsCopy = copy.deepcopy(params) + for payload in payloadList: + executor.submit( + make_request, url, payload, paramsCopy, headers, GET, + delay, timeout, paramName, encoding, progress, len(payloadList)) logger.no_format('') + + +def make_request(url, payload, paramsCopy, headers, GET, delay, timeout, paramName, encoding, progress, total): + logger.run('Bruteforcing %s[%s%s%s]%s: %i/%i \r' % + (green, end, paramName, green, end, progress['lap'], total)) + if encoding: + payload = encoding(unquote(payload)) + paramsCopy[paramName] = payload + response = requester(url, paramsCopy, headers, + GET, delay, timeout).text + if encoding: + payload = encoding(payload) + if payload in response: + li = response.split(payload) + if li[0][-1] != '"' and li[1][0] != '"': + logger.good(payload) + progress['lap'] += 1 \ No newline at end of file diff --git a/modes/scan.py b/modes/scan.py index 912d68db..7b864c92 100644 --- a/modes/scan.py +++ b/modes/scan.py @@ -1,5 +1,7 @@ +from concurrent.futures import ThreadPoolExecutor, as_completed import copy import re +from threading import Lock from urllib.parse import urlparse, quote, unquote from core.checker import checker @@ -16,9 +18,10 @@ from core.log import setup_logger logger = setup_logger(__name__) +lock = Lock() -def scan(target, paramData, encoding, headers, delay, timeout, skipDOM, skip): +def scan(target, paramData, encoding, headers, delay, timeout, skipDOM, skip, threadCount): GET, POST = (False, True) if paramData else (True, False) # If the user hasn't supplied the root url with http(s), we will handle it if not target.startswith('http'): @@ -86,23 +89,23 @@ def scan(target, paramData, encoding, headers, delay, timeout, skipDOM, skip): logger.error('No vectors were crafted.') continue logger.info('Payloads generated: %i' % total) - progress = 0 - for confidence, vects in vectors.items(): - for vect in vects: - if core.config.globalVariables['path']: - vect = vect.replace('/', '%2F') - loggerVector = vect - progress += 1 - logger.run('Progress: %i/%i\r' % (progress, total)) - if not GET: - vect = unquote(vect) - efficiencies = checker( - url, paramsCopy, headers, GET, delay, vect, positions, timeout, encoding) - if not efficiencies: - for i in range(len(occurences)): - efficiencies.append(0) - bestEfficiency = max(efficiencies) + progress = {'lap': 0} + running_futures = [] + with ThreadPoolExecutor(max_workers=threadCount) as executor: + + # Add all comb + for confidence, vects in vectors.items(): + for vect in vects: + running_futures.append(executor.submit(checky, target, url, paramsCopy, headers, GET, delay, + vect, positions, timeout, encoding, occurences, confidence, progress, total)) + + for future in as_completed(running_futures): + + # retrieve the result + bestEfficiency, target, loggerVector, confidence = future.result() + if bestEfficiency == 100 or (vect[0] == '\\' and bestEfficiency >= 95): + lock.acquire() logger.red_line() logger.good('Payload: %s' % loggerVector) logger.info('Efficiency: %i' % bestEfficiency) @@ -110,11 +113,37 @@ def scan(target, paramData, encoding, headers, delay, timeout, skipDOM, skip): if not skip: choice = input( '%s Would you like to continue scanning? [y/N] ' % que).lower() - if choice != 'y': - quit() + lock.release() + if skip or choice != 'y': + [f.cancel() for f in running_futures] + return target, loggerVector elif bestEfficiency > minEfficiency: + lock.acquire() logger.red_line() logger.good('Payload: %s' % loggerVector) logger.info('Efficiency: %i' % bestEfficiency) logger.info('Confidence: %i' % confidence) + lock.release() + logger.no_format('') + + +def checky( + target, url, paramsCopy, headers, GET, delay, + vect, positions, timeout, encoding, occurences, confidence, progress, total): + if core.config.globalVariables['path']: + vect = vect.replace('/', '%2F') + loggerVector = vect + progress['lap'] += 1 + lock.acquire() + logger.run(f'Progress: {progress["lap"]}/{total} \r') + lock.release() + if not GET: + vect = unquote(vect) + efficiencies = checker( + url, paramsCopy, headers, GET, delay, vect, positions, timeout, encoding) + if not efficiencies: + for i in range(len(occurences)): + efficiencies.append(0) + bestEfficiency = max(efficiencies) + return bestEfficiency, target, loggerVector, confidence diff --git a/xsstrike.py b/xsstrike.py index 1aab695d..2b6a7a6c 100644 --- a/xsstrike.py +++ b/xsstrike.py @@ -6,7 +6,7 @@ # Just a fancy ass banner print('''%s -\tXSStrike %sv3.1.5 +\tXSStrike %sv3.1.6 %s''' % (red, white, end)) try: @@ -39,6 +39,7 @@ # Processing command line arguments, where dest var names will be mapped to local vars with the same name parser = argparse.ArgumentParser() parser.add_argument('-u', '--url', help='url', dest='target') +parser.add_argument('-ul', '--url_list', help='list of urls', dest='targets') parser.add_argument('--data', help='post data', dest='paramData') parser.add_argument('-e', '--encode', help='encode payloads', dest='encode') parser.add_argument('--fuzzer', help='fuzzer', @@ -85,6 +86,7 @@ # Pull all parameter values of dict from argparse namespace into local variables of name == key # The following works, but the static checkers are too static ;-) locals().update(vars(args)) target = args.target +targets = args.targets path = args.path jsonData = args.jsonData paramData = args.paramData @@ -142,6 +144,12 @@ headers['Content-type'] = 'application/json' paramData = converter(paramData) +target_list = [] +if targets: + target_list = list(filter(None, reader(targets))) +elif target: + target_list.append(target) + if args_file: if args_file == 'default': payloadList = core.config.payloads @@ -161,17 +169,30 @@ updater() quit() # quitting because files have been changed -if not target and not args_seeds: # if the user hasn't supplied a url +if not target_list and not args_seeds: # if the user hasn't supplied a url logger.no_format('\n' + parser.format_help().lower()) quit() if fuzz: singleFuzz(target, paramData, encoding, headers, delay, timeout) elif not recursive and not args_seeds: - if args_file: - bruteforcer(target, paramData, payloadList, encoding, headers, delay, timeout) - else: - scan(target, paramData, encoding, headers, delay, timeout, skipDOM, skip) + results = [] + for i, target in enumerate(target_list): + logger.red_line() + logger.info(f'Target: {target} ({i + 1}/{len(target_list)})') + if args_file: + bruteforcer(target, paramData, payloadList, encoding, headers, delay, timeout, threadCount) + else: + result = scan(target, paramData, encoding, headers, delay, timeout, skipDOM, skip, threadCount) + results.append(result) if result else 'The target is not vulnerable!' + + if results: + logger.yellow_summary_line() + logger.run('SUMMARY') + logger.info(f'Total {len(target_list)} target{"s"[:len(target_list)^1]}') + logger.info(f'Vulnerable {len(results)} target{"s"[:len(results)^1]}') + for i, result in enumerate(results): + logger.good(f'Pwned {result[0]} ({result[1]})') else: if target: seedList.append(target)