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

196 lines
6.1 KiB
Ruby
Raw Normal View History

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'rex'
2014-08-20 04:48:13 +00:00
require 'net/https'
require 'uri'
class Metasploit4 < Msf::Auxiliary
2013-08-30 21:28:54 +00:00
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'Shodan Search',
'Description' => %q{
2014-08-20 04:48:13 +00:00
This module uses the Shodan API to search Shodan. Accounts are free
and an API key is required to used this module. Output from the module
is displayed to the screen and can be saved to a file or the MSF database.
NOTE: SHODAN filters (i.e. port, hostname, os, geo, city) can be used in
queries, but there are limitations when used with a free API key. Please
see the Shodan site for more information.
Shodan website: https://www.shodan.io/
API: https://developer.shodan.io/api
2013-08-30 21:28:54 +00:00
},
'Author' =>
[
2014-08-20 04:48:13 +00:00
'John H Sawyer <john[at]sploitlab.com>', # InGuardians, Inc.
'sinn3r' # Metasploit-fu plus other features
2013-08-30 21:28:54 +00:00
],
'License' => MSF_LICENSE
2014-08-20 04:48:13 +00:00
)
)
2013-08-30 21:28:54 +00:00
2014-08-20 04:48:13 +00:00
deregister_options('RHOST', 'DOMAIN', 'DigestAuthIIS', 'NTLM::SendLM',
'NTLM::SendNTLM', 'VHOST', 'RPORT', 'NTLM::SendSPN', 'NTLM::UseLMKey',
'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2')
2013-08-30 21:28:54 +00:00
register_options(
[
2014-08-20 04:48:13 +00:00
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]),
OptRegexp.new('REGEX', [true, 'Regex search for a specific IP/City/Country/Hostname', '.*'])
2013-08-30 21:28:54 +00:00
], 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
2014-08-20 04:48:13 +00:00
uri = URI.parse('https://api.shodan.io/shodan/host/search?query=' +
Rex::Text.uri_encode(query) + '&key=' + apikey + '&page=' + page.to_s)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri.request_uri)
res = http.request(request)
if res and res.body =~ /<title>401 Unauthorized<\/title>/
fail_with(Failure::BadConfig, '401 Unauthorized. Your SHODAN_APIKEY is invalid')
end
2014-08-20 04:48:13 +00:00
# Check if we can resolve host, got a response,
# then parse the JSON, and return it
if res
2013-08-30 21:28:54 +00:00
results = ActiveSupport::JSON.decode(res.body)
return results
else
2014-08-20 04:48:13 +00:00
return 'server_response_error'
2013-08-30 21:28:54 +00:00
end
end
2014-08-20 04:48:13 +00:00
# save output to file
2013-08-30 21:28:54 +00:00
def save_output(data)
2014-08-23 04:08:27 +00:00
::File.open(datastore['OUTFILE'], 'wb') do |f|
f.write(data)
print_status("Saved results in #{datastore['OUTFILE']}")
end
2013-08-30 21:28:54 +00:00
end
2014-08-20 04:48:13 +00:00
# Check to see if api.shodan.io resolves properly
def shodan_resolvable?
begin
Rex::Socket.resolv_to_dotted("api.shodan.io")
rescue RuntimeError, SocketError
return false
2014-04-15 02:16:40 +00:00
end
true
2014-04-15 02:16:40 +00:00
end
2013-08-30 21:28:54 +00:00
def run
2014-08-20 04:48:13 +00:00
# check to ensure api.shodan.io is resolvable
unless shodan_resolvable?
print_error("Unable to resolve api.shodan.io")
return
end
2014-04-15 02:16:40 +00:00
2013-08-30 21:28:54 +00:00
# create our Shodan request parameters
query = datastore['QUERY']
apikey = datastore['SHODAN_APIKEY']
page = 1
2014-08-20 04:48:13 +00:00
maxpage = datastore['MAXPAGE']
2013-08-30 21:28:54 +00:00
# results gets our results from shodan_query
results = []
results[page] = shodan_query(query, apikey, page)
if results[page]['total'].nil? || results[page]['total'] == 0
2015-09-10 21:05:40 +00:00
msg = "No results."
if results[page]['error'].to_s.length > 0
msg << " Error: #{results[page]['error']}"
end
print_error(msg)
return
2013-08-30 21:28:54 +00:00
end
# Determine page count based on total results
2014-08-20 04:48:13 +00:00
if results[page]['total'] % 100 == 0
tpages = results[page]['total'] / 100
2013-08-30 21:28:54 +00:00
else
2014-08-20 04:48:13 +00:00
tpages = results[page]['total'] / 100 + 1
maxpage = tpages if datastore['MAXPAGE'] > tpages
2013-08-30 21:28:54 +00:00
end
# start printing out our query statistics
print_status("Total: #{results[page]['total']} on #{tpages} " +
2014-08-20 04:48:13 +00:00
"pages. Showing: #{maxpage} page(s)")
# If search results greater than 100, loop & get all results
print_status('Collecting data, please wait...')
if results[page]['total'] > 100
2013-08-30 21:28:54 +00:00
page += 1
while page <= maxpage
2013-08-30 21:28:54 +00:00
break if page > datastore['MAXPAGE']
2014-08-20 04:48:13 +00:00
results[page] = shodan_query(query, apikey, page)
page += 1
2013-08-30 21:28:54 +00:00
end
end
# Save the results to this table
tbl = Rex::Ui::Text::Table.new(
2014-08-20 04:48:13 +00:00
'Header' => 'Search Results',
2013-08-30 21:28:54 +00:00
'Indent' => 1,
2014-08-20 04:48:13 +00:00
'Columns' => ['IP:Port', 'City', 'Country', 'Hostname']
2013-08-30 21:28:54 +00:00
)
2014-08-20 04:48:13 +00:00
# Organize results and put them into the table and database
p = 1
regex = datastore['REGEX'] if datastore['REGEX']
while p <= maxpage
break if p > maxpage
results[p]['matches'].each do |host|
2014-08-20 04:48:13 +00:00
city = host['location']['city'] || 'N/A'
ip = host['ip_str'] || 'N/A'
2013-08-30 21:28:54 +00:00
port = host['port'] || ''
2014-08-20 04:48:13 +00:00
country = host['location']['country_name'] || 'N/A'
2013-08-30 21:28:54 +00:00
hostname = host['hostnames'][0]
data = host['data']
report_host(:host => ip,
:name => hostname,
2014-08-20 04:48:13 +00:00
:comments => 'Added from Shodan',
:info => host['info']
2014-08-20 04:48:13 +00:00
) if datastore['DATABASE']
report_service(:host => ip,
2014-08-20 04:48:13 +00:00
:port => port,
:info => 'Added from Shodan'
) if datastore['DATABASE']
if ip =~ regex ||
city =~ regex ||
country =~ regex ||
hostname =~ regex ||
data =~ regex
2014-08-23 04:08:27 +00:00
# Unfortunately we cannot display the banner properly,
# because it messes with our output format
tbl << ["#{ip}:#{port}", city, country, hostname]
2013-08-30 21:28:54 +00:00
end
2014-08-20 04:48:13 +00:00
end
p += 1
2013-08-30 21:28:54 +00:00
end
2014-08-20 04:48:13 +00:00
# Show data and maybe save it if needed
2014-08-23 04:08:27 +00:00
print_line
print_line("#{tbl}")
save_output(tbl) if datastore['OUTFILE']
2013-08-30 21:28:54 +00:00
end
end