def _fill_template(self, scan_name, cmd) -> List[str]: """Replace template parameters with values.""" cmd = (cmd.replace('<target>', self.target).replace( '<wordlist>', get_db_value('web-word-list')).replace( '<userlist>', get_db_value('brute-user-list')).replace( '<passlist>', get_db_value('brute-pass-list'))) if '<ports>' in cmd: fout = get_scan_file( self.target, self.name + '.' + '.'.join([str(p) for p in self.ports]) + '.' + scan_name) return [ cmd.replace('<ports>', self.port_str()).replace('<fout>', fout) ] elif '<port>' in cmd: cmds = [] for port in self.ports: fout = get_scan_file( self.target, self.name + '.' + str(port) + '.' + scan_name) cmds.append( cmd.replace('<port>', str(port)).replace('<fout>', fout)) return cmds else: fout = get_scan_file(self.target, self.name + '.' + scan_name) # handling edge-case where a qs-spawned non-port scan could be # overwritten by a ts-spawned non-port scan of the same service i = 0 while file_exists(fout): fout = get_scan_file( self.target, self.name + '.' + str(i) + '.' + scan_name) i += 1 cmd = cmd.replace('<fout>', fout) return [cmd]
def join_services(target: str, services: Set[ParsedService]) ->\ Tuple[Set[ParsedService], List[DetectedService]]: """Join services on multiple ports into a consolidated set. Returns: A set of unmatched services and a list of consolidated service matches. """ defined_services = get_db_value('services') unmatched_services = services.copy() joined_services = [] for protocol, config in defined_services.items(): matches = [s for s in services if s.name in config['nmap-service-names']] if matches: ds = DetectedService( protocol, target, tuple(sorted(s.port for s in matches)), config['scans'], tuple(config['recommendations'])) joined_services.append(ds) unmatched_services -= set(matches) return unmatched_services, joined_services
async def run_udp_s(target: str) -> Set[ParsedService]: """Run a UDP scan on a target.""" print_i_d2(target, ': beginning UDP scan') udp_config = get_db_value('udp-scan') cmd = udp_config.scan.format( target=target, fout=get_scan_file(target, 'udp.' + udp_config.name)) services = await _parse_port_scan(target, cmd, udp_config.pattern) print_i_d2(target, ': finished UDP scan') return services
async def run_ts(target: str) -> Set[ParsedService]: """Run a thorough TCP scan on a target using the configured method.""" print_i_d2(target, ': beginning TCP thorough scan') ts_config = get_db_value('thorough-scan') cmd = ts_config.scan.format( target=target, fout=get_scan_file(target, 'tcp.thorough.' + ts_config.name)) services = await _parse_port_scan(target, cmd, ts_config.pattern) print_i_d2(target, ': finished TCP thorough scan') return services
async def run_qs(target: str) -> Set[ParsedService]: """Run a quick scan on a target via the configured method.""" print_i_d2(target, ': beginning TCP quick scan') qs_config = get_db_value('quick-scan') cmd = qs_config.scan.format( target=target, fout=get_scan_file(target, 'tcp.quickscan.' + qs_config.name)) services = await _parse_port_scan(target, cmd, qs_config.pattern) print_i_d2(target, ': finished TCP quick scan') return services
def highlight_patterns(target: str, line: str) -> None: """Print a string with configured pattern matches highlighted in purple.""" patterns = get_db_value('patterns') pos = 0 highlighted_line = '' did_match = False for match in re.finditer(patterns, line): did_match = True highlighted_line += line[pos:match.start()] highlighted_line += purple(match.group(0)) pos = match.end() if did_match: highlighted_line += line[pos:] print_i_d3( target, ': matched pattern in line `', highlighted_line, '`')
def create_dir_skeleton(target: str) -> None: """Create the directory skeleton for a target-based scan. Args: target: The singular target of the scan. """ print_i_d1(target, ': beginning creation of directory structure') base_dir = get_base_dir(target) if path_exists(base_dir): if not get_db_value('hard'): raise BscanForceSkipTarget('Base directory ' + base_dir + ' already exists, use ' '`--hard` option to force overwrite') print_w_d2(target, ': removing existing base directory ', base_dir) remove_dir(base_dir) create_dir(base_dir) notes_file = get_notes_txt_file(target) touch_file(notes_file) recommendations_file = get_recommendations_txt_file(target) touch_file(recommendations_file) loot_dir = get_loot_dir(target) create_dir(loot_dir) proof_file = get_proof_txt_file(target) touch_file(proof_file) local_file = get_local_txt_file(target) touch_file(local_file) services_dir = get_services_dir(target) create_dir(services_dir) sploits_dir = get_sploits_dir(target) create_dir(sploits_dir) print_i_d1(target, ': successfully completed directory skeleton setup')
async def scan_target(target: str) -> None: """Run quick, thorough, and service scans on a target.""" do_ts = not get_db_value('quick-only') do_s_scans = not get_db_value('no-service-scans') await add_active_target(target) # block on the initial quick scan qs_parsed_services = await run_qs(target) qs_unmatched_services, qs_joined_services = \ join_services(target, qs_parsed_services) _print_matched_services(target, qs_joined_services) _print_unmatched_services(target, qs_unmatched_services) # schedule service scans based on qs-found ports if do_s_scans: qs_s_scan_cmds: List[List[str]] = \ [js.build_scans() for js in qs_joined_services] qs_s_scans: List[Coroutine[Any, Any, Any]] = \ [run_service_s(target, cmd) for cmd in chain(*qs_s_scan_cmds)] qs_s_scan_tasks = [ensure_future(scan) for scan in qs_s_scans] else: qs_s_scan_tasks = [] # block on the thorough scan, if enabled if do_ts: ts_parsed_services: Set[ParsedService] = await run_ts(target) else: ts_parsed_services = set() print_i_d2(target, ': skipping thorough scan') # diff open ports between quick and thorough scans new_services: Set[ParsedService] = ts_parsed_services - qs_parsed_services ts_joined_services: List[DetectedService] = [] if new_services and do_s_scans: ts_unmatched_services, ts_joined_services = \ join_services(target, new_services) _print_matched_services(target, ts_joined_services) _print_unmatched_services(target, ts_unmatched_services) ts_s_scan_cmds = [js.build_scans() for js in ts_joined_services] ts_s_scans = [run_service_s(target, cmd) for cmd in chain(*ts_s_scan_cmds)] ts_s_scan_tasks = [ensure_future(scan) for scan in ts_s_scans] elif do_ts: print_i_d2(target, ': thorough scan discovered no additional ' 'services') ts_s_scan_tasks = [] else: ts_s_scan_tasks = [] # write recommendations file for further manual TCP commands for js in chain(qs_joined_services, ts_joined_services): if not js.recommendations: continue with open(get_recommendations_txt_file(js.target), 'a') as f: fprint = partial(print, file=f, sep='') section_header = ( 'The following commands are recommended for service ' + js.name + ' running on port(s) ' + js.port_str() + ':') fprint(section_header) fprint('-'*len(section_header)) for rec in js.build_recommendations(): fprint(rec) fprint() # run UDP scan if get_db_value('udp'): udp_services = await run_udp_s(target) for service in udp_services: print_i_d3( target, ': detected service ', blue(service.name), ' on UDP port ', blue(str(service.port))) # block on any pending service scan tasks await gather(*chain(qs_s_scan_tasks, ts_s_scan_tasks)) await remove_active_target(target)
def get_base_dir(target: str) -> str: """Get the path of the base directory for a scan.""" return os.path.join(get_db_value('output-dir'), f'{target}.bscan.d')
async def main(args: Optional[List[str]] = None) -> int: """Main entry point for `bscan`'s command-line interface. Args: args: Custom arguments to override ``sys.argv``. Returns: The exit code of the program. """ try: init_colorama() if not good_py_version(): print_w_d1('Running with Python version ', py_version_str(), 'but this program is only tested with Python 3.6') opts = get_parsed_args(args) print_i_d1('Initializing configuration from command-line arguments') mc = opts.max_concurrency try: mc = (20 if mc is None else int(mc)) if mc < 1: raise ValueError except ValueError: raise BscanConfigError( 'Invalid `--max-concurrency` positive integer value ' 'received: ' + str(mc)) async with Sublemon(max_concurrency=mc) as subl: await write_db_value('sublemon', subl) await init_config(opts) print_color_info() if not opts.targets: print_e_d1('No targets specified; use `--help` to figure ' 'out what you\'re doing') return 1 # TODO: create a full list of targets from network address and # --ping-sweep filtering targets = [] _target_set: Set[str] = set() for candidate in opts.targets: if candidate in _target_set: print_w_d1( 'Target ', candidate, ' has already been added as a ' 'target; skipping another attempted addition') continue elif is_valid_ip_host_addr(candidate): pass elif is_valid_hostname(candidate): pass elif is_valid_ip_net_addr(candidate): print_w_d1( 'Network scanning not yet supported; ' 'skipping network: ', candidate) continue else: print_e_d1('Unable to parse target ', candidate, ', skipping it') continue try: create_dir_skeleton(candidate) except BscanForceSkipTarget as e: print_e_d1(e.message) print_e_d1(candidate, ': skipping this target') continue targets.append(candidate) _target_set.add(candidate) if not targets: print_e_d1('No valid targets specified') return 1 print_i_d1('Kicking off scans of ', len(targets), ' targets') tasks = [scan_target(target) for target in targets] if get_db_value('status-interval') > 0: tasks.append(status_update_poller()) await asyncio.gather(*tasks) print_i_d1('Completed execution') return 0 except BscanConfigError as e: print_e_d1('Configuration error: ', e.message) return 1 except BscanForceSilentExit as e: return 1 except BscanInternalError as e: print_e_d1('Internal error: ', e.message) return 1 except BscanSubprocessError as e: print_e_d1('Error handling subprocess: ', e.message) return 1 except BscanError as e: print_e_d1('This should not be reached!') return 1 except Exception as e: print_e_d1('Received unexpected exception; re-raising it.', file=sys.stderr) raise e