Skip to content

Commit 1959f7a

Browse files
committed
Updated shodan_search for new API
1 parent 0c9daff commit 1959f7a

File tree

1 file changed

+84
-86
lines changed

1 file changed

+84
-86
lines changed

modules/auxiliary/gather/shodan_search.rb

Lines changed: 84 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
require 'msf/core'
77
require 'rex'
8-
require 'net/dns'
8+
require 'net/https'
9+
require 'uri'
910

1011
class Metasploit4 < Msf::Auxiliary
1112

@@ -16,170 +17,167 @@ def initialize(info = {})
1617
super(update_info(info,
1718
'Name' => 'Shodan Search',
1819
'Description' => %q{
19-
This module uses the SHODAN API to query the database and
20-
returns the first 50 IPs. SHODAN accounts are free & output
21-
can be sent to a file for use by another program. Results
22-
can also populated into the services table in the database.
23-
NOTE: SHODAN filters (port, hostname, os, geo, city) can be
24-
used in queries, but the free API does not allow net, country,
25-
before, and after filters. An unlimited API key can be
26-
purchased from the Shodan site to use those queries. The 50
27-
result limit can also be raised to 10,000 for a small fee.
28-
API: http://www.shodanhq.com/api_doc
29-
FILTERS: http://www.shodanhq.com/help/filters
20+
This module uses the Shodan API to search Shodan. Accounts are free
21+
and an API key is required to used this module. Output from the module
22+
is displayed to the screen and can be saved to a file or the MSF database.
23+
NOTE: SHODAN filters (i.e. port, hostname, os, geo, city) can be used in
24+
queries, but there are limitations when used with a free API key. Please
25+
see the Shodan site for more information.
26+
Shodan website: https://www.shodan.io/
27+
API: https://developer.shodan.io/api
3028
},
3129
'Author' =>
3230
[
33-
'John Sawyer <johnhsawyer[at]gmail.com>', #sploitlab.com
34-
'sinn3r' #Metasploit-fu plus other features
31+
'John H Sawyer <john[at]sploitlab.com>', # InGuardians, Inc.
32+
'sinn3r' # Metasploit-fu plus other features
3533
],
3634
'License' => MSF_LICENSE
37-
))
35+
)
36+
)
3837

39-
# disabling all the unnecessary options that someone might set to break our query
40-
deregister_options('RPORT','RHOST', 'DOMAIN',
41-
'DigestAuthIIS', 'SSLVersion', 'NTLM::SendLM', 'NTLM::SendNTLM',
42-
'NTLM::SendSPN', 'NTLM::UseLMKey', 'NTLM::UseNTLM2_session',
43-
'NTLM::UseNTLMv2','SSL')
38+
deregister_options('RHOST', 'DOMAIN', 'DigestAuthIIS', 'NTLM::SendLM',
39+
'NTLM::SendNTLM', 'VHOST', 'RPORT', 'NTLM::SendSPN', 'NTLM::UseLMKey',
40+
'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2')
4441

4542
register_options(
4643
[
47-
OptString.new('SHODAN_APIKEY', [true, "The SHODAN API key"]),
48-
OptString.new('QUERY', [true, "Keywords you want to search for"]),
49-
OptString.new('OUTFILE', [false, "A filename to store the list of IPs"]),
50-
OptBool.new('DATABASE', [false, "Add search results to the database", false]),
51-
OptInt.new('MAXPAGE', [true, "Max amount of pages to collect", 1]),
52-
OptString.new('FILTER', [false, 'Search for a specific IP/City/Country/Hostname']),
53-
OptString.new('VHOST', [true, 'The virtual host name to use in requests', 'www.shodanhq.com']),
44+
OptString.new('SHODAN_APIKEY', [true, 'The SHODAN API key']),
45+
OptString.new('QUERY', [true, 'Keywords you want to search for']),
46+
OptString.new('OUTFILE', [false, 'A filename to store the list of IPs']),
47+
OptBool.new('DATABASE', [false, 'Add search results to the database', false]),
48+
OptInt.new('MAXPAGE', [true, 'Max amount of pages to collect', 1]),
49+
OptString.new('FILTER', [false, 'Search for a specific IP/City/Country/Hostname'])
5450
], self.class)
5551
end
5652

5753
# create our Shodan query function that performs the actual web request
5854
def shodan_query(query, apikey, page)
5955
# send our query to Shodan
60-
uri = "/api/search?&q=" + Rex::Text.uri_encode(query) + "&key=" + apikey + "&page=" + page.to_s
61-
res = send_request_raw(
62-
{
63-
'rhost' => shodan_rhost,
64-
'rport' => shodan_rport,
65-
'vhost' => vhost,
66-
'method' => 'GET',
67-
'uri' => uri
68-
}, 25)
69-
70-
# Check if we got a response, parse the JSON, and return it
71-
if (res)
56+
57+
uri = URI.parse('https://api.shodan.io/shodan/host/search?query=' +
58+
Rex::Text.uri_encode(query) + '&key=' + apikey + '&page=' + page.to_s)
59+
http = Net::HTTP.new(uri.host, uri.port)
60+
http.use_ssl = true
61+
request = Net::HTTP::Get.new(uri.request_uri)
62+
res = http.request(request)
63+
64+
# Check if we can resolve host, got a response,
65+
# then parse the JSON, and return it
66+
if res
7267
results = ActiveSupport::JSON.decode(res.body)
7368
return results
7469
else
75-
return 'server_error'
70+
return 'server_response_error'
7671
end
7772
end
7873

74+
# save output to file
7975
def save_output(data)
80-
f = ::File.open(datastore['OUTFILE'], "wb")
76+
f = ::File.open(datastore['OUTFILE'], 'wb')
8177
f.write(data)
8278
f.close
8379
print_status("Save results in #{datastore['OUTFILE']}")
8480
end
8581

82+
# Check to see if api.shodan.io resolves properly
8683
def shodan_rhost
87-
@res = Net::DNS::Resolver.new()
88-
dns_query = @res.query("#{datastore['VHOST']}", "A")
84+
@res = Net::DNS::Resolver.new
85+
dns_query = @res.query('api.shodan.io', 'A')
8986
if dns_query.answer.length == 0
90-
print_error("Could not resolve #{datastore['VHOST']}")
91-
raise ::Rex::ConnectError(vhost, shodan_port)
87+
print_error('Could not resolve api.shodan.io')
88+
raise ::Rex::ConnectError('api.shodan.io', '443')
9289
end
9390
dns_query.answer[0].to_s.split(/[\s,]+/)[4]
9491
end
9592

96-
def shodan_rport
97-
80
98-
end
99-
10093
def run
94+
# check to ensure api.shodan.io is resolvable
95+
shodan_rhost
10196

10297
# create our Shodan request parameters
10398
query = datastore['QUERY']
10499
apikey = datastore['SHODAN_APIKEY']
105-
106100
page = 1
101+
maxpage = datastore['MAXPAGE']
107102

108103
# results gets our results from shodan_query
109104
results = []
110105
results[page] = shodan_query(query, apikey, page)
111106

112-
if results[page].empty?
113-
print_error("No Results Found!")
107+
if results[page]['total'] == 0
108+
print_error('No Results Found!')
114109
return
115110
end
116111

117112
# Determine page count based on total results
118-
if results[page]['total']%50 == 0
119-
tpages = results[page]['total']/50
113+
if results[page]['total'] % 100 == 0
114+
tpages = results[page]['total'] / 100
120115
else
121-
tpages = results[page]['total']/50 + 1
116+
tpages = results[page]['total'] / 100 + 1
117+
maxpage = tpages if datastore['MAXPAGE'] > tpages
122118
end
123119

124120
# start printing out our query statistics
125-
print_status("Total: #{results[page]['total']} on #{tpages} pages. Showing: #{datastore['MAXPAGE']}")
126-
print_status("Country Statistics:")
127-
results[page]['countries'].each { |ctry|
128-
print_status "\t#{ctry['name']} (#{ctry['code']}): #{ctry['count']}"
129-
}
130-
131-
# If search results greater than 50, loop & get all results
132-
print_status("Collecting data, please wait...")
133-
if (results[page]['total'] > 50)
121+
print_status("Total: #{results[page]['total']} on #{tpages} "\
122+
"pages. Showing: #{maxpage} page(s)")
123+
124+
# If search results greater than 100, loop & get all results
125+
print_status('Collecting data, please wait...')
126+
if results[page]['total'] > 100
134127
page += 1
135128
while page <= tpages
136-
results[page] = shodan_query(query, apikey, page)
137-
page +=1
138129
break if page > datastore['MAXPAGE']
130+
results[page] = shodan_query(query, apikey, page)
131+
page += 1
139132
end
140133
end
141134

142135
# Save the results to this table
143136
tbl = Rex::Ui::Text::Table.new(
144-
'Header' => 'IP Results',
137+
'Header' => 'Search Results',
145138
'Indent' => 1,
146-
'Columns' => ['IP', 'City', 'Country', 'Hostname']
139+
'Columns' => ['IP:Port', 'City', 'Country', 'Hostname']
147140
)
148141

149-
# Organize results and put them into the table
142+
# Organize results and put them into the table and database
150143
page = 1
151144
my_filter = datastore['FILTER']
152145
for i in page..tpages
153-
next if results[i].nil? or results[i]['matches'].nil?
154-
results[i]['matches'].each { |host|
146+
next if results[i].nil? || results[i]['matches'].nil?
147+
results[i]['matches'].each do |host|
155148

156-
city = host['city'] || 'N/A'
157-
ip = host['ip'] || 'N/A'
149+
city = host['location']['city'] || 'N/A'
150+
ip = host['ip_str'] || 'N/A'
158151
port = host['port'] || ''
159-
country = host['country_name'] || 'N/A'
152+
country = host['location']['country_name'] || 'N/A'
160153
hostname = host['hostnames'][0]
161154
data = host['data']
162155

163-
if ip =~ /#{my_filter}/ or
164-
city =~ /#{my_filter}/i or
165-
country =~ /#{my_filter}/i or
166-
hostname =~ /#{my_filter}/i or
156+
report_host(:host => ip,
157+
:name => hostname,
158+
:comments => 'Added from Shodan',
159+
:info => host['info']
160+
) if datastore['DATABASE']
161+
162+
report_service(:host => ip,
163+
:port => port,
164+
:info => 'Added from Shodan'
165+
) if datastore['DATABASE']
166+
167+
if ip =~ /#{my_filter}/ ||
168+
city =~ /#{my_filter}/i ||
169+
country =~ /#{my_filter}/i ||
170+
hostname =~ /#{my_filter}/i ||
167171
data =~ /#{my_filter}/i
168172
# Unfortunately we cannot display the banner properly,
169173
# because it messes with our output format
170174
tbl << ["#{ip}:#{port}", city, country, hostname]
171175
end
172-
}
176+
end
173177
end
174178

175-
#Show data and maybe save it if needed
179+
# Show data and maybe save it if needed
176180
print_line("\n#{tbl.to_s}")
177-
178-
report_note(
179-
:type => 'shodan',
180-
:data => tbl.to_csv
181-
) if datastore['DATABASE']
182-
183181
save_output(tbl.to_s) if not datastore['OUTFILE'].nil?
184182
end
185183
end

0 commit comments

Comments
 (0)