##
# $Id: vhost_scanner.rb 8423 2010-02-09 01:10:29Z et $
##

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##
################################################################################
##
## Enhancement of /auxiliary/scanner/http/vhost_scanner.rb (Original Author - et)
##
## Enhanced by Aung Khant
## YGN Ethical Hacker Group, Yangon, Myanmar
## http://yehg.net/
##
## Change Logs:
## added support wordlist to load from file
## added more built-in words commonly used in subdomain naming
## added TLD support
## added title/header display
##
###
## We've made some(maybe weird) enhancement over it; thus
## We don't want to mess up your usual habit of using tool
## Enjoy!
##
## Background Info
## when dns is not available in controlled env, vhost gives us probably exploitatable entries and recon points
##  - development host name (test app)
##  - virtual hosts that point/belong to other IPs
##


require 'rex/proto/http'
require 'msf/core'
require 'cgi'



	class Metasploit3 < Msf::Auxiliary

		include Msf::Exploit::Remote::HttpClient
		include Msf::Auxiliary::WMAPScanServer
		include Msf::Auxiliary::Scanner
		include Msf::Auxiliary::Report


		def initialize(info = {})
			super(update_info(info,
				'Name'   		=> 'HTTP Virtual Host Brute Force Scanner',
				'Description'	=> %q{
					This module tries to identify unique virtual hosts
				hosted by the target web server  using defined wordlist file or built-in vhost list.

					},
				'Author' 		=> [ 'et [at] metasploit.com', # original author
					'aungkhant [at] yehg.net' ], # modified and improved				
				'License'		=> BSD_LICENSE,
				'Version'		=> '$Revision: N/A $'))

						
			register_options(
			[
				OptString.new('PATH', [ true,  "The PATH to use while testing", '/']),
				OptString.new('QUERY', [ false,  "HTTP URI Query", '']),
				OptString.new('DOMAIN', [ false,  "Domain name (like google/google.com)  used as prefix to bruteforce.", '']),
				OptString.new('TLD', [ false,  "Domain TLD (.net,.com,..etc) used as surfix to bruteforce.", '']),
				OptString.new('HEADERS', [ false,  "HTTP Headers", '']),
				OptString.new('SHOW_RESPONSE_HEADER', [ false,  "Show Response HTTP Headers? (true/false)", 'false']),				
				OptString.new('FILE', [ false,  "Use this wordlist file for DOMAIN instead of built-in wordlists", File.join(Msf::Config.install_root, "data", "wordlists", "namelist.txt")])
			], self.class)
			
			deregister_options('VHOST') # misleading, so removed

		end
	
		def run_host(ip)

			if (File.exist?(datastore['FILE']))
			    valstr = []
				File.open(datastore['FILE'], 'rb').each_line do |d|
					valstr << d.strip 
				end
				valstr.uniq!
				print_status("loaded wordlist from : #{datastore['FILE']} |  #{valstr.length} words")
			else
				valstr = [
					"a",
					"academy",
					"account",
					"accounts",
					"admin",
					"administrator",
					"ads",
					"ap",
					"app",
					"apps",
					"ask",
					"asset",
					"assets",
					"b",
					"base",
					"billing",
					"blog",
					"blogs",
					"book",
					"books",
					"business",
					"c",
					"calendar",
					"cam",
					"career",
					"cdn",
					"checkout",
					"clients",
					"clients1",
					"clients2",
					"clients3",
					"code",
					"console",
					"consult",
					"content",
					"contents",
					"corporate",
					"customer",
					"customer-login",
					"customers",
					"customers-login",
					"d",
					"db1",
					"db2",
					"db3",
					"demo",
					"demos",
					"desktop",
					"dev",
					"develop",
					"developer",
					"development",
					"directory",
					"discussion",
					"discussions",
					"dns",
					"doc",
					"docs",
					"domain",
					"domains",					
					"download",
					"downloads",
					"e",
					"external",
					"extranet",
					"f",
					"feed",
					"feeds",
					"file",
					"fileman",
					"filemanager",
					"file-manager",
					"files",
					"finance",
					"forum",
					"forums",
					"ftp",
					"g",
					"gprs",
					"gps",
					"h",
					"home",
					"i",
					"image",
					"images",
					"info",
					"internal",
					"intra",
					"intranet",
					"investor",
					"ipv6",
					"j",
					"job",
					"jobs",
					"js",
					"k",
					"l",
					"lab",
					"loadbalancer",
					"loadbalancers",
					"local",
					"localhost",
					"log",
					"login",
					"logs",
					"m",
					"mail",
					"manager",
					"managers",
					"map",
					"maps",
					"me",
					"member",
					"members",
					"members-login",
					"monitor",
					"mt0",
					"mt1",
					"myaccounts",
					"mybills",
					"n",
					"net2ftp",
					"news",
					"ns",
					"o",
					"online",
					"oss",
					"p",
					"pack",
					"page",
					"pages",
					"partner",
					"partner-login",
					"partnernet",
					"partnernetwork",
					"partners",
					"people",
					"photo",
					"photos",
					"phpmyadmin",
					"print",
					"product",
					"products",
					"proxy",
					"q",
					"r",
					"research",
					"restricted",
					"restricted-zone",
					"s",
					"sales",
					"search",
					"searchcgi",
					"secure",
					"seminars",
					"service",
					"services",
					"services",
					"site",
					"sites",
					"sms",
					"solutions",
					"spool",
					"spreadsheet",
					"spreadsheets",
					"ssl",
					"ssl-vpn",
					"staff",
					"staff-login",
					"staffs",
					"staffs-login",
					"stat",
					"static",
					"stats",
					"store",
					"support",
					"t",
					"test",
					"test-apps",
					"training",
					"u",
					"unit",
					"v",
					"videos",
					"visitors-stat",
					"vpn",
					"w",
					"web",
					"webadmin",
					"web-admin",
					"webcam",
					"webftp",
					"web-ftp",
					"weblog",
					"weblogs",
					"webmail",
					"webmin",
					"www",
					"www1",
					"www2",
					"www3",
					"wwwwww",
					"x",
					"xx",
					"xxx",
					"y",
					"yy",
					"yyy",
					"z",
					"zz",
					"zzz",
				]
				print_status("using built-in wordlist: #{valstr.length} words")
			end
			
			datastore['QUERY'] ? tquery = queryparse(datastore['QUERY']): nil
			datastore['HEADERS'] ? thead = headersparse(datastore['HEADERS']) : nil

			noexistsres = nil
			resparr = []

			if (datastore['TLD'].to_s.length > 0)
				print_status("matchig with tld domain: #{datastore['TLD']}")
			end
			
			2.times do |n|

				if (datastore['DOMAIN'].to_s.length > 0 && datastore['TLD'].to_s.length > 0)
					randhost = Rex::Text.rand_text_alpha(5)+"."+datastore['DOMAIN'] + datastore['TLD']				
				elsif (datastore['DOMAIN'].to_s.length > 0)
					randhost = Rex::Text.rand_text_alpha(5)+"."+datastore['DOMAIN']				
				elsif (datastore['TLD'].to_s.length > 0)
					randhost = Rex::Text.rand_text_alpha(5)+datastore['TLD']
				else
					randhost = Rex::Text.rand_text_alpha(5)
				end

				begin
					noexistsres = send_request_cgi({
						'uri'  		=>  datastore['PATH'],
						'vars_get' 	=>  tquery,
						'headers' 	=>  thead,
						'vhost'		=>  randhost,
						'method'   	=> 'GET',
						'ctype'		=> 'text/plain'
					}, 20)

					print_status("[#{ip}] Testing with random domain #{randhost} ")

					if noexistsres
						resparr[n] = noexistsres.body
					end
					
				rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
				rescue ::Timeout::Error, ::Errno::EPIPE
				end
			end

			if resparr[0] != resparr[1]
				print_error("[#{ip}] Unable to identify error response")
				return
			end

			valstr.each do |astr|
				
				if (datastore['DOMAIN'].to_s.length > 0 && datastore['TLD'].to_s.length > 0)
					thost = astr+"."+datastore['DOMAIN'] + datastore['TLD']				
				elsif (datastore['DOMAIN'].to_s.length > 0)
					thost = astr+"."+datastore['DOMAIN']	
				elsif (datastore['TLD'].to_s.length > 0)
					thost = astr+datastore['TLD']	
				else
					thost = astr
				end

				begin
					res = send_request_cgi({
						'uri'  		=>  datastore['PATH'],
						'vars_get' 	=>  tquery,
						'headers' 	=>  thead,
						'vhost'		=>  thost,
						'method'   	=> 'GET',
						'ctype'		=> 'text/plain'
					}, 20)


					if res and noexistsres

						if res.body !=  noexistsres.body
							# get title 
							restitle = 'N/A'

							if (res.body =~ /<title>(.*?)<\/title>/)
								restitle = $1
							end
							print_status("[#{ip}] Vhost found  #{thost} | response code: #{res.code} | title: #{restitle}")
							
							if  /true/i.match(datastore['SHOW_RESPONSE_HEADER'])
								print_status("[#{ip}] Response Header:\n#{res.headers}")
							end
							
							report_note(
								:host	=> ip,
								:proto	=> 'HTTP',
								:port	=> rport,
								:type	=> 'VHOST',
								:data	=> "#{thost}"
							)

						else
							#print_status("NOT Found #{thost}") # hide because it's killing us to see what is up
						end
					else
						print_status("[#{ip}] NO Response")
					end

				rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
				rescue ::Timeout::Error, ::Errno::EPIPE
				end

			end

		end
	end

