Skip to content

Commit 79b5a36

Browse files
authored
Merge branch 'master' into master
2 parents 8f8a1eb + 3ec18a4 commit 79b5a36

File tree

5 files changed

+94
-48
lines changed

5 files changed

+94
-48
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ $ pip install -r requirements.txt
3333
| -h, --help | Display help message and exit |
3434
| -t TARGET_HOSTS | Set the target host. |
3535
| -b BASE_HOST | Set host to be used during substitution in wordlist (default to TARGET).|
36-
| -w WORDLIST | Set the wordlist to use (default ./wordlists/virtual-host-scanning.txt) |
36+
| -w WORDLISTS | Set the wordlist(s) to use. You may specify multiple wordlists in comma delimited format (e.g. -w "./wordlists/simple.txt, ./wordlists/hackthebox.txt" (default ./wordlists/virtual-host-scanning.txt). |
3737
| -p PORT | Set the port to use (default 80). |
3838
| -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). |
3939
| --ignore-http-codes IGNORE_HTTP_CODES | Comma separated list of http codes to ignore with virtual host scans (default 404). |
@@ -43,6 +43,8 @@ $ pip install -r requirements.txt
4343
| --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). |
4444
| --no-lookups | Disbale reverse lookups (identifies new targets and append to wordlist, on by default). |
4545
| --rate-limit | Amount of time in seconds to delay between each scan (default 0). |
46+
| --random-agent | If set, each scan will use a random user-agent from a predefined list. |
47+
| --user-agent | Specify a user agent to use for scans. |
4648
| --waf | If set then simple WAF bypass headers will be sent. |
4749
| -oN OUTPUT_NORMAL | Normal output printed to a file when the -oN option is specified with a filename argument. |
4850
| -oJ OUTPUT_JSON | JSON output printed to a file when the -oJ option is specified with a filename argument. |

VHostScan.py

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from socket import gethostbyaddr
88
from lib.core.virtual_host_scanner import *
99
from lib.helpers.output_helper import *
10+
from lib.helpers.file_helper import get_combined_word_lists, load_random_user_agents
1011
from lib.core.__version__ import __version__
1112

1213

@@ -20,7 +21,7 @@ def main():
2021
print_banner()
2122
parser = ArgumentParser()
2223
parser.add_argument("-t", dest="target_hosts", required=True, help="Set a target range of addresses to target. Ex 10.11.1.1-255" )
23-
parser.add_argument("-w", dest="wordlist", required=False, type=str, help="Set the wordlist to use (default ./wordlists/virtual-host-scanning.txt)", default=False)
24+
parser.add_argument("-w", dest="wordlists", required=False, type=str, help="Set the wordlists to use (default ./wordlists/virtual-host-scanning.txt)", default=False)
2425
parser.add_argument("-b", dest="base_host", required=False, help="Set host to be used during substitution in wordlist (default to TARGET).", default=False)
2526
parser.add_argument("-p", dest="port", required=False, help="Set the port to use (default 80).", default=80)
2627
parser.add_argument("-r", dest="real_port", required=False, help="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).", default=False)
@@ -32,45 +33,48 @@ def main():
3233
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)
3334
parser.add_argument("--no-lookups", dest="no_lookup", action="store_true", help="Disable reverse lookups (identifies new targets and appends to wordlist, on by default).", default=False)
3435
parser.add_argument("--rate-limit", dest="rate_limit", type=int, help='Amount of time in seconds to delay between each scan (default 0).', default=0)
36+
parser.add_argument('--random-agent', dest='random_agent', action='store_true', help='If set, then each scan will use random user-agent from predefined list.', default=False)
37+
parser.add_argument('--user-agent', dest='user_agent', type=str, help='Specify a user-agent to use for scans')
3538
parser.add_argument("--waf", dest="add_waf_bypass_headers", action="store_true", help="If set then simple WAF bypass headers will be sent.", default=False)
3639
parser.add_argument("-oN", dest="output_normal", help="Normal output printed to a file when the -oN option is specified with a filename argument." )
3740
parser.add_argument("-oJ", dest="output_json", help="JSON output printed to a file when the -oJ option is specified with a filename argument." )
3841
parser.add_argument("-", dest="stdin", action="store_true", help="By passing a blank '-' you tell VHostScan to expect input from stdin (pipe).", default=False)
3942

4043
arguments = parser.parse_args()
41-
wordlist = list()
42-
43-
if(arguments.stdin and not arguments.wordlist):
44+
wordlist = []
45+
46+
word_list_types = []
47+
48+
default_wordlist = "./wordlists/virtual-host-scanning.txt" if not arguments.stdin else None
49+
50+
if arguments.stdin:
51+
word_list_types.append('stdin')
4452
wordlist.extend(list(line for line in sys.stdin.read().splitlines()))
45-
print("[+] Starting virtual host scan for %s using port %s and stdin data" % (arguments.target_hosts,
46-
str(arguments.port)))
47-
elif(arguments.stdin and arguments.wordlist):
48-
if not os.path.exists(arguments.wordlist):
49-
wordlist.extend(list(line for line in sys.stdin.read().splitlines()))
50-
print("[!] Wordlist %s doesn't exist and can't be appended to stdin." % arguments.wordlist)
51-
print("[+] Starting virtual host scan for %s using port %s and stdin data" % (arguments.target_hosts,
52-
str(arguments.port)))
53-
else:
54-
wordlist.extend(list(line for line in open(arguments.wordlist).read().splitlines()))
55-
print("[+] Starting virtual host scan for %s using port %s, stdin data, and wordlist %s" % (arguments.target_hosts,
56-
str(arguments.port),
57-
arguments.wordlist))
58-
else:
59-
if not arguments.wordlist:
60-
wordlist.extend(list(line for line in open("./wordlists/virtual-host-scanning.txt").read().splitlines()))
61-
print("[+] Starting virtual host scan for %s using port %s and wordlist %s" % ( arguments.target_hosts,
62-
str(arguments.port),
63-
"./wordlists/virtual-host-scanning.txt"))
64-
else:
65-
if not os.path.exists(arguments.wordlist):
66-
print("[!] Wordlist %s doesn't exist, unable to scan." % arguments.wordlist)
67-
sys.exit()
68-
else:
69-
wordlist.extend(list(line for line in open(arguments.wordlist).read().splitlines()))
70-
print("[+] Starting virtual host scan for %s using port %s and wordlist %s" % ( arguments.target_hosts,
71-
str(arguments.port),
72-
str(arguments.wordlist)))
73-
53+
54+
combined = get_combined_word_lists(arguments.wordlists or default_wordlist)
55+
word_list_types.append('wordlists: {}'.format(
56+
', '.join(combined['file_paths']),
57+
))
58+
wordlist.extend(combined['words'])
59+
60+
if len(wordlist) == 0:
61+
print("[!] No words found in provided wordlists, unable to scan.")
62+
sys.exit(1)
63+
64+
print("[+] Starting virtual host scan for {host} using port {port} and {inputs}".format(
65+
host=arguments.target_hosts,
66+
port=arguments.port,
67+
inputs=', '.join(word_list_types),
68+
))
69+
70+
user_agents = []
71+
if arguments.user_agent:
72+
print('[>] User-Agent specified, using it')
73+
user_agents = [arguments.user_agent]
74+
elif arguments.random_agent:
75+
print('[>] Random User-Agent flag set')
76+
user_agents = load_random_user_agents()
77+
7478
if(arguments.ssl):
7579
print("[>] SSL flag set, sending all results over HTTPS")
7680

@@ -90,7 +94,7 @@ def main():
9094
wordlist.extend(aliases)
9195

9296
scanner_args = vars(arguments)
93-
scanner_args.update({'target': arguments.target_hosts, 'wordlist': wordlist})
97+
scanner_args.update({'target': arguments.target_hosts, 'wordlist': wordlist, 'user_agents': user_agents})
9498
scanner = virtual_host_scanner(**scanner_args)
9599

96100
scanner.scan()

lib/core/virtual_host_scanner.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import os
2+
import random
3+
24
import requests
35
import hashlib
46
import pandas as pd
57
import time
68
from lib.core.discovered_host import *
79

10+
DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
11+
812

913
class virtual_host_scanner(object):
1014
"""Virtual host scanning class
@@ -44,6 +48,9 @@ def __init__(self, target, wordlist, **kwargs):
4448
# store associated data for discovered hosts in array for oN, oJ, etc'
4549
self.hosts = []
4650

51+
# available user-agents
52+
self.user_agents = list(kwargs.get('user_agents')) or [DEFAULT_USER_AGENT]
53+
4754
@property
4855
def ignore_http_codes(self):
4956
return self._ignore_http_codes
@@ -63,22 +70,19 @@ def scan(self):
6370
for virtual_host in self.wordlist:
6471
hostname = virtual_host.replace('%s', self.base_host)
6572

73+
headers = {
74+
'User-Agent': random.choice(self.user_agents),
75+
'Host': hostname if self.real_port == 80 else '{}:{}'.format(hostname, self.real_port),
76+
'Accept': '*/*'
77+
}
78+
6679
if self.add_waf_bypass_headers:
67-
headers = {
68-
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
69-
'Host': hostname if self.real_port == 80 else '{}:{}'.format(hostname, self.real_port),
70-
'Accept': '*/*',
80+
headers.update({
7181
'X-Originating-IP': '127.0.0.1',
7282
'X-Forwarded-For': '127.0.0.1',
7383
'X-Remote-IP': '127.0.0.1',
7484
'X-Remote-Addr': '127.0.0.1'
75-
}
76-
else:
77-
headers = {
78-
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36',
79-
'Host': hostname if self.real_port == 80 else '{}:{}'.format(hostname, self.real_port),
80-
'Accept': '*/*'
81-
}
85+
})
8286

8387
dest_url = '{}://{}:{}/'.format('https' if self.ssl else 'http', self.target, self.port)
8488

@@ -115,7 +119,7 @@ def scan(self):
115119
# add url and hash into array for likely matches
116120
self.results.append(hostname + ',' + page_hash)
117121

118-
#rate limit the connection, if the int is 0 it is ignored
122+
#rate limit the connection, if the int is 0 it is ignored
119123
time.sleep(self.rate_limit)
120124

121125
self.completed_scan=True

lib/helpers/file_helper.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,38 @@ def is_json(json_file):
2525

2626
def write_file(self, contents):
2727
with open(self.output_file, "w") as o:
28-
o.write(contents)
28+
o.write(contents)
29+
30+
31+
def parse_word_list_argument(argument):
32+
if not argument:
33+
return []
34+
35+
if ',' in argument:
36+
files = [arg.strip() for arg in argument.split(',')]
37+
else:
38+
files = [argument.strip()]
39+
40+
return [
41+
path for path in files
42+
if os.path.exists(path)
43+
]
44+
45+
46+
def get_combined_word_lists(argument):
47+
files = parse_word_list_argument(argument)
48+
words = []
49+
50+
for path in files:
51+
with open(path) as f:
52+
words.extend(f.read().splitlines())
53+
54+
return {
55+
'file_paths': files,
56+
'words': words,
57+
}
58+
59+
60+
def load_random_user_agents():
61+
with open('./lib/ua-random-list.txt') as f:
62+
return f.readlines()

lib/ua-random-list.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
2+
Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0

0 commit comments

Comments
 (0)