metasploit-framework/modules/auxiliary/gather/shodan_search.rb

186 lines
5.8 KiB
Ruby

##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'rex'
require 'net/dns'
class Metasploit4 < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'Shodan Search',
'Description' => %q{
This module uses the SHODAN API to query the database and
returns the first 50 IPs. SHODAN accounts are free & output
can be sent to a file for use by another program. Results
can also populated into the services table in the database.
NOTE: SHODAN filters (port, hostname, os, geo, city) can be
used in queries, but the free API does not allow net, country,
before, and after filters. An unlimited API key can be
purchased from the Shodan site to use those queries. The 50
result limit can also be raised to 10,000 for a small fee.
API: http://www.shodanhq.com/api_doc
FILTERS: http://www.shodanhq.com/help/filters
},
'Author' =>
[
'John Sawyer <johnhsawyer[at]gmail.com>', #sploitlab.com
'sinn3r' #Metasploit-fu plus other features
],
'License' => MSF_LICENSE
))
# disabling all the unnecessary options that someone might set to break our query
deregister_options('RPORT','RHOST', 'DOMAIN',
'DigestAuthIIS', 'SSLVersion', 'NTLM::SendLM', 'NTLM::SendNTLM',
'NTLM::SendSPN', 'NTLM::UseLMKey', 'NTLM::UseNTLM2_session',
'NTLM::UseNTLMv2','SSL')
register_options(
[
OptString.new('SHODAN_APIKEY', [true, "The SHODAN API key"]),
OptString.new('QUERY', [true, "Keywords you want to search for"]),
OptString.new('OUTFILE', [false, "A filename to store the list of IPs"]),
OptBool.new('DATABASE', [false, "Add search results to the database", false]),
OptInt.new('MAXPAGE', [true, "Max amount of pages to collect", 1]),
OptString.new('FILTER', [false, 'Search for a specific IP/City/Country/Hostname']),
OptString.new('VHOST', [true, 'The virtual host name to use in requests', 'www.shodanhq.com']),
], self.class)
end
# create our Shodan query function that performs the actual web request
def shodan_query(query, apikey, page)
# send our query to Shodan
uri = "/api/search?&q=" + Rex::Text.uri_encode(query) + "&key=" + apikey + "&page=" + page.to_s
res = send_request_raw(
{
'rhost' => shodan_rhost,
'rport' => shodan_rport,
'vhost' => vhost,
'method' => 'GET',
'uri' => uri
}, 25)
# Check if we got a response, parse the JSON, and return it
if (res)
results = ActiveSupport::JSON.decode(res.body)
return results
else
return 'server_error'
end
end
def save_output(data)
f = ::File.open(datastore['OUTFILE'], "wb")
f.write(data)
f.close
print_status("Save results in #{datastore['OUTFILE']}")
end
def shodan_rhost
@res = Net::DNS::Resolver.new()
dns_query = @res.query("#{datastore['VHOST']}", "A")
if dns_query.answer.length == 0
print_error("Could not resolve #{datastore['VHOST']}")
raise ::Rex::ConnectError(vhost, shodan_port)
end
dns_query.answer[0].to_s.split(/[\s,]+/)[4]
end
def shodan_rport
80
end
def run
# create our Shodan request parameters
query = datastore['QUERY']
apikey = datastore['SHODAN_APIKEY']
page = 1
# results gets our results from shodan_query
results = []
results[page] = shodan_query(query, apikey, page)
if results[page].empty?
print_error("No Results Found!")
return
end
# Determine page count based on total results
if results[page]['total']%50 == 0
tpages = results[page]['total']/50
else
tpages = results[page]['total']/50 + 1
end
# start printing out our query statistics
print_status("Total: #{results[page]['total']} on #{tpages} pages. Showing: #{datastore['MAXPAGE']}")
print_status("Country Statistics:")
results[page]['countries'].each { |ctry|
print_status "\t#{ctry['name']} (#{ctry['code']}): #{ctry['count']}"
}
# If search results greater than 50, loop & get all results
print_status("Collecting data, please wait...")
if (results[page]['total'] > 50)
page += 1
while page <= tpages
results[page] = shodan_query(query, apikey, page)
page +=1
break if page > datastore['MAXPAGE']
end
end
# Save the results to this table
tbl = Rex::Ui::Text::Table.new(
'Header' => 'IP Results',
'Indent' => 1,
'Columns' => ['IP', 'City', 'Country', 'Hostname']
)
# Organize results and put them into the table
page = 1
my_filter = datastore['FILTER']
for i in page..tpages
next if results[i].nil? or results[i]['matches'].nil?
results[i]['matches'].each { |host|
city = host['city'] || 'N/A'
ip = host['ip'] || 'N/A'
port = host['port'] || ''
country = host['country_name'] || 'N/A'
hostname = host['hostnames'][0]
data = host['data']
if ip =~ /#{my_filter}/ or
city =~ /#{my_filter}/i or
country =~ /#{my_filter}/i or
hostname =~ /#{my_filter}/i or
data =~ /#{my_filter}/i
# Unfortunately we cannot display the banner properly,
# because it messes with our output format
tbl << ["#{ip}:#{port}", city, country, hostname]
end
}
end
#Show data and maybe save it if needed
print_line("\n#{tbl.to_s}")
report_note(
:type => 'shodan',
:data => tbl.to_csv
) if datastore['DATABASE']
save_output(tbl.to_s) if not datastore['OUTFILE'].nil?
end
end