#!/usr/bin/ruby -w # # Examine an HTTP server for HTTP/1.1 Host header behavior & stuff # # January 2009, Jon Hart require 'socket' require 'openssl' require 'uri' require 'optparse' require 'ostruct' require 'resolv.rb' require 'digest/md5' class Options# {{{ def self.parse(name, args) options = OpenStruct.new options.verbose = false options.request_methods = %w[GET HEAD] options.host = nil options.headers = {'User-Agent' => 'vhinfo', 'Connection' => 'close'} opts = OptionParser.new do |opts| opts.banner = "#{File.basename(name)} ( http://spoofed.org )" opts.banner += "\nUsage: #{name} [IP] [options]" opts.on("--get", "Use GETs only") do |o| options.request_methods = %w[GET] end opts.on("--head", "Use HEADs only") do |o| options.request_methods = %w[HEAD] end opts.on_tail("-h", "--help", "Show help") do puts opts exit end opts.on("-H [HEADER]", "Append this header to all requests. May be called multiple times") do |o| if (o =~ /([^:]*):\s+?(.*)/) options.headers[$1] = $2 else options.headers[o] = "" end end opts.on("-v", "--verbose", "Be verbose") do |o| options.verbose = o end end options.help = opts.help begin opts.parse!(args) rescue OptionParser::ParseError => e puts "#{e}\n\n#{opts}" exit(1) end options end end# }}} class URICheck# {{{ attr_accessor :ip, :host, :port, :ssl, :checked, :status, :message def initialize(ip, port, ssl, host) @ip = ip @port = port @ssl = ssl @host = host end def checked? @checked end end# }}} def print_results(ip, port, ssl, method, version, host, response)# {{{ uri = "#{ssl ? 'https' : 'http'}://#{ip}" uri += (((ssl && (port == 443)) || (!ssl && (port == 80))) ? "" : ":#{port}") + "/" host_header = "(" + (host.nil? ? "No Host header" : "Host: #{host}") + ")" puts "HTTP/#{version} #{method.upcase} #{host_header} on #{uri}: #{response}" end# }}} def make_socket(ip, port, ssl)# {{{ begin s = TCPSocket.new(ip, port) if (ssl) begin ssl = OpenSSL::SSL::SSLSocket.new(s, OpenSSL::SSL::SSLContext.new) ssl.sync = true ssl.connect yield(ssl) rescue OpenSSL::SSL::SSLError => e puts "SSL setup on #{ip}:#{port} failed: #{e}" ensure ssl.close end else yield(s) end rescue Exception => e puts "Connection to #{ip}:#{port} failed: #{e}" end s.close end# }}} # This was written because Net::HTTP has a number of things # that make doing this work impossible or not as fun: # # * Can't set arbitrary HTTP versions. 1.1 only # * Header values are explicitly "capitalized" # * A "Host" header is set regardless of whether or not # you want one. # # Reopening Net::HTTP is an exercise in futility. Once you figure # out a way around one feature that it breaks, another crops up. # F' that noise. def rawrequest(ip, port, ssl, method, version, headers)# {{{ hstring = "" headers.each_pair do |k,v| hstring += "#{k}: #{v}\r\n" end res = {} res['body'] = "" res['headers'] = {} res['code'] = nil res['message'] = "" req = "#{method} / HTTP/#{version}\r\n#{hstring}\r\n\r\n" make_socket(ip, port, ssl) do |s| begin s.print(req) inBody = false res['message'] = s.readline.strip res['code'] = res['message'].split(/\s+/)[1] s.readlines.each do |l| l.strip! inBody = true if l =~ /^$/ if (inBody) res['body'] += l unless l.empty? else header = l.split(/:\s+?/) res['headers'][header[0]] = (header[1].nil? ? nil : header[1].strip) end end rescue Exception => e res['message'] = e end end res end# }}} # For a given +ip+, perform a battery of tests using +headers+# {{{ # this +host+ header using +uri+ as the URI to pull port and scheme from# }}} def check(ip, port, ssl, host, headers)# {{{ return if @checked["#{host}|#{ip}"] @checked["#{host}|#{ip}"] = 1 if (host.nil?) headers.delete('Host') else headers['Host'] = host end uri = URI.parse("#{ssl ? 'https' : 'http'}://#{host.nil? ? ip : host}:#{port}/") @options.request_methods.each do |method| ["1.1", "1.0"].each do |version| host_header = "(" + (host.nil? ? "No Host header" : "Host: #{host}") + ")" verbose_print("Checking HTTP/#{version} #{method} #{host_header} on #{ip}") res = rawrequest(ip, port, ssl, method, version, headers) other = [] ['Location', 'Content-Location'].each do |u| if (res['headers'][u]) other_uri = uri.merge(res['headers'][u]) other 0) other = " (#{other.join(', ')})" end print_results(ip, port, ssl, method, version, host, "#{res['message']}#{other}") end end end# }}} def getCNAMEs(host)# {{{ if (@CNAMEs[host]) verbose_print("Using cached CNAME records for #{host}") else verbose_print("Getting CNAME records for #{host}") @CNAMEs[host] = [] Resolv::DNS.new.each_resource(host, Resolv::DNS::Resource::IN::CNAME) do |c| @CNAMEs[host]