diff --git a/README.md b/README.md index 73c3cd2..b370744 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ $ pip install -r requirements.txt | -r REAL_PORT | The real port of the webserver to use in headers when not 80 (see RFC2616 14.23), useful when pivoting through ssh/nc etc (default to PORT). | | --ignore-http-codes IGNORE_HTTP_CODES | Comma separated list of http codes to ignore with virtual host scans (default 404). | | --ignore-content-length IGNORE_CONTENT_LENGTH | Ignore content lengths of specificed amount. | +| --first-hit | Return first successful result. Only use in scenarios where you are sure no catch-all is configured (such as a CTF). | | --unique-depth UNIQUE_DEPTH | Show likely matches of page content that is found x times (default 1). | | --ssl | If set then connections will be made over HTTPS instead of HTTP. | | --fuzzy-logic | If set then all unique content replies are compared and a similarity ratio is given for each pair. This helps to isolate vhosts in situations where a default page isn't static (such as having the time on it). | @@ -51,6 +52,7 @@ $ pip install -r requirements.txt | -oJ OUTPUT_JSON | JSON output printed to a file when the -oJ option is specified with a filename argument. | | - | By passing a blank '-' you tell VHostScan to expect input from stdin (pipe). | + ## Usage Examples _Note that a number of these examples reference 10.10.10.29. This IP refers to BANK.HTB, a retired target machine from HackTheBox (https://www.hackthebox.eu/)._ @@ -98,4 +100,4 @@ pip install -r test-requirements.txt pytest ``` -If you're thinking of adding a new feature to the project, consider also contributing with a couple of tests. A well-tested codebase is a sane codebase. :) \ No newline at end of file +If you're thinking of adding a new feature to the project, consider also contributing with a couple of tests. A well-tested codebase is a sane codebase. :) diff --git a/VHostScan.py b/VHostScan.py index 41bc3c5..c429e0d 100644 --- a/VHostScan.py +++ b/VHostScan.py @@ -28,6 +28,7 @@ def main(): parser.add_argument('--ignore-http-codes', dest='ignore_http_codes', type=str, help='Comma separated list of http codes to ignore with virtual host scans (default 404).', default='404') parser.add_argument('--ignore-content-length', dest='ignore_content_length', type=int, help='Ignore content lengths of specificed amount (default 0).', default=0) + parser.add_argument('--first-hit', dest='first_hit', action='store_true', help='Return first successful result. Only use in scenarios where you are sure no catch-all is configured (such as a CTF).', default=False) parser.add_argument('--unique-depth', dest='unique_depth', type=int, help='Show likely matches of page content that is found x times (default 1).', default=1) parser.add_argument("--ssl", dest="ssl", action="store_true", help="If set then connections will be made over HTTPS instead of HTTP (default http).", default=False) parser.add_argument("--fuzzy-logic", dest="fuzzy_logic", action="store_true", help="If set then fuzzy match will be performed against unique hosts (default off).", default=False) @@ -40,7 +41,7 @@ def main(): parser.add_argument("-oJ", dest="output_json", help="JSON output printed to a file when the -oJ option is specified with a filename argument." ) parser.add_argument("-", dest="stdin", action="store_true", help="By passing a blank '-' you tell VHostScan to expect input from stdin (pipe).", default=False) - arguments = parser.parse_args() + arguments = parser.parse_args() wordlist = [] word_list_types = [] @@ -69,23 +70,26 @@ def main(): user_agents = [] if arguments.user_agent: - print('[>] User-Agent specified, using it') + print('[>] User-Agent specified, using it.') user_agents = [arguments.user_agent] elif arguments.random_agent: - print('[>] Random User-Agent flag set') + print('[>] Random User-Agent flag set.') user_agents = load_random_user_agents() if(arguments.ssl): - print("[>] SSL flag set, sending all results over HTTPS") + print("[>] SSL flag set, sending all results over HTTPS.") if(arguments.add_waf_bypass_headers): - print("[>] WAF flag set, sending simple WAF bypass headers") + print("[>] WAF flag set, sending simple WAF bypass headers.") print("[>] Ignoring HTTP codes: %s" % (arguments.ignore_http_codes)) - + if(arguments.ignore_content_length > 0): print("[>] Ignoring Content length: %s" % (arguments.ignore_content_length)) + if arguments.first_hit: + print("[>] First hit is set.") + if not arguments.no_lookup: for ip in Resolver().query(arguments.target_hosts, 'A'): host, aliases, ips = gethostbyaddr(str(ip)) diff --git a/lib/core/__version__.py b/lib/core/__version__.py index 3a82f59..84807bc 100644 --- a/lib/core/__version__.py +++ b/lib/core/__version__.py @@ -2,4 +2,4 @@ # |V|H|o|s|t|S|c|a|n| Developed by @codingo_ & @__timk # +-+-+-+-+-+-+-+-+-+ https://github.com/codingo/VHostScan -__version__ = '1.5.4' +__version__ = '1.6' diff --git a/lib/core/virtual_host_scanner.py b/lib/core/virtual_host_scanner.py index 3c7971d..947d112 100644 --- a/lib/core/virtual_host_scanner.py +++ b/lib/core/virtual_host_scanner.py @@ -24,9 +24,9 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, class virtual_host_scanner(object): """Virtual host scanning class - + Virtual host scanner has the following properties: - + Attributes: wordlist: location to a wordlist file to use with scans target: the target for scanning @@ -50,6 +50,7 @@ def __init__(self, target, wordlist, **kwargs): self.add_waf_bypass_headers = kwargs.get('add_waf_bypass_headers', False) self.unique_depth = int(kwargs.get('unique_depth', 1)) self.ignore_http_codes = kwargs.get('ignore_http_codes', '404') + self.first_hit = kwargs.get('first_hit') # this can be made redundant in future with better exceptions self.completed_scan=False @@ -112,26 +113,15 @@ def scan(self): # hash the page results to aid in identifing unique content page_hash = hashlib.sha256(res.text.encode('utf-8')).hexdigest() - output = '[#] Found: {} (code: {}, length: {}, hash: {})\n'.format(hostname, res.status_code, - res.headers.get('content-length'), page_hash) - host = discovered_host() - host.hostname = hostname - host.response_code = res.status_code - host.hash = page_hash - host.content = res.content - - for key, val in res.headers.items(): - output += ' {}: {}\n'.format(key, val) - host.keys.append('{}: {}'.format(key, val)) - - self.hosts.append(host) - - # print current results so feedback remains in "realtime" - print(output) + + self.hosts.append(self.create_host(res, hostname, page_hash)) # add url and hash into array for likely matches self.results.append(hostname + ',' + page_hash) - + + if len(self.hosts) == 2 and self.first_hit: + break + #rate limit the connection, if the int is 0 it is ignored time.sleep(self.rate_limit) @@ -154,3 +144,24 @@ def likely_matches(self): matches = ((segmented_data["key_col"].values).tolist()) return matches + + def create_host(self, response, hostname, page_hash): + """ + Creates a host using the responce and the hash. + Prints current result in real time. + """ + output = '[#] Found: {} (code: {}, length: {}, hash: {})\n'.format(hostname, response.status_code, + response.headers.get('content-length'), page_hash) + host = discovered_host() + host.hostname = hostname + host.response_code = response.status_code + host.hash = page_hash + host.content = response.content + + for key, val in response.headers.items(): + output += ' {}: {}\n'.format(key, val) + host.keys.append('{}: {}'.format(key, val)) + + print(output) + + return host \ No newline at end of file diff --git a/tests/helpers/test_file_helper.py b/tests/helpers/test_file_helper.py index ea700f9..39112a8 100644 --- a/tests/helpers/test_file_helper.py +++ b/tests/helpers/test_file_helper.py @@ -39,4 +39,3 @@ def test_get_combined_word_lists(wordlist): assert wordlist.files == result['file_paths'] assert wordlist.words == result['words'] - \ No newline at end of file