#!/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 << "#{u}: #{other_uri}" if res['headers'][u] @urisToBeChecked << URICheck.new(ip, port, uri.scheme == 'https', other_uri.host) getCNAMEs(other_uri.host).each do |cname| cname_uri = other_uri.dup cname_uri.host = cname @urisToBeChecked << URICheck.new(ip, port, cname_uri.scheme == 'https', cname) end end end unless res['body'].empty? other << "Body MD5: #{Digest::MD5.hexdigest(res['body'])}" end if (other.size > 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] << c.name.to_s @CNAMEs[host] << getCNAMEs(c.name.to_s) end end @CNAMEs[host].flatten end# }}} def getAs(host)# {{{ if (@As[host]) verbose_print("Using cached A records for #{host}") else verbose_print("Getting A records for #{host}") @As[host] = [] Resolv.each_address(host) do |ip| @As[host] << ip if (@PTRs[ip]) @PTRs[ip] << getPTRs(ip) else @PTRs[ip] = getPTRs(ip) end end end @As[host].flatten end# }}} def getPTRs(ip)# {{{ if (@PTRs[ip]) verbose_print("Using cached PTR records for #{ip}") else verbose_print("Getting PTR records for #{ip}") @PTRs[ip] = [] Resolv.each_name(ip) do |p| @PTRs[ip] << p @PTRs[ip] << getCNAMEs(p) end end @PTRs[ip].flatten end# }}} def verbose_print(str)# {{{ puts str if @options.verbose end# }}} STDOUT.sync = true @options = Options.parse($0, ARGV) @checked = {} @CNAMEs = {} @As = {} @PTRs = {} case ARGV.size when 1, 2 uri = ARGV[0] uri = "http://#{uri}" unless (uri =~ /^https?:\/\//) uri = URI.parse(uri) orig_uri = uri.host if (ARGV.size == 2) orig_ip = ARGV[1] end else puts @options.help exit(1) end @urisToBeChecked = [] # if an IP is provided, hit ONLY that IP ips = [] if (orig_ip) ips << orig_ip else ips << getAs(uri.host) end # a base list of "Host" headers to try baseNames = [] baseNames << uri.host baseNames << 'localhost' << '127.0.0.1' baseNames << getCNAMEs(uri.host) baseNames << "A" + Digest::MD5.hexdigest(Time.now.to_s) baseNames << nil ips.flatten.each do |ip| [baseNames, ip, getPTRs(ip)].flatten.each do |n| new_uri = uri.dup new_uri.host = n @urisToBeChecked << URICheck.new(ip, uri.port, uri.scheme == 'https', n) end end while (!@urisToBeChecked.empty?) u = @urisToBeChecked.shift check(u.ip, u.port, u.ssl, u.host, @options.headers) end # vim: set ts=2 et sw=2: