/
shodan_search.rb
195 lines (169 loc) · 6.1 KB
/
shodan_search.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'rex'
require 'net/https'
require 'uri'
class MetasploitModule < 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 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
},
'Author' =>
[
'John H Sawyer <john[at]sploitlab.com>', # InGuardians, Inc.
'sinn3r' # Metasploit-fu plus other features
],
'License' => MSF_LICENSE
)
)
deregister_options('RHOST', 'DOMAIN', 'DigestAuthIIS', 'NTLM::SendLM',
'NTLM::SendNTLM', 'VHOST', 'RPORT', 'NTLM::SendSPN', 'NTLM::UseLMKey',
'NTLM::UseNTLM2_session', 'NTLM::UseNTLMv2')
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]),
OptRegexp.new('REGEX', [true, 'Regex search for a specific IP/City/Country/Hostname', '.*'])
], 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 = 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
# Check if we can resolve host, got a response,
# then parse the JSON, and return it
if res
results = ActiveSupport::JSON.decode(res.body)
return results
else
return 'server_response_error'
end
end
# save output to file
def save_output(data)
::File.open(datastore['OUTFILE'], 'wb') do |f|
f.write(data)
print_status("Saved results in #{datastore['OUTFILE']}")
end
end
# 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
end
true
end
def run
# check to ensure api.shodan.io is resolvable
unless shodan_resolvable?
print_error("Unable to resolve api.shodan.io")
return
end
# create our Shodan request parameters
query = datastore['QUERY']
apikey = datastore['SHODAN_APIKEY']
page = 1
maxpage = datastore['MAXPAGE']
# results gets our results from shodan_query
results = []
results[page] = shodan_query(query, apikey, page)
if results[page]['total'].nil? || results[page]['total'] == 0
msg = "No results."
if results[page]['error'].to_s.length > 0
msg << " Error: #{results[page]['error']}"
end
print_error(msg)
return
end
# Determine page count based on total results
if results[page]['total'] % 100 == 0
tpages = results[page]['total'] / 100
else
tpages = results[page]['total'] / 100 + 1
maxpage = tpages if datastore['MAXPAGE'] > tpages
end
# start printing out our query statistics
print_status("Total: #{results[page]['total']} on #{tpages} " +
"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
page += 1
while page <= maxpage
break if page > datastore['MAXPAGE']
results[page] = shodan_query(query, apikey, page)
page += 1
end
end
# Save the results to this table
tbl = Rex::Ui::Text::Table.new(
'Header' => 'Search Results',
'Indent' => 1,
'Columns' => ['IP:Port', 'City', 'Country', 'Hostname']
)
# 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|
city = host['location']['city'] || 'N/A'
ip = host['ip_str'] || 'N/A'
port = host['port'] || ''
country = host['location']['country_name'] || 'N/A'
hostname = host['hostnames'][0]
data = host['data']
report_host(:host => ip,
:name => hostname,
:comments => 'Added from Shodan',
:info => host['info']
) if datastore['DATABASE']
report_service(:host => ip,
:port => port,
:info => 'Added from Shodan'
) if datastore['DATABASE']
if ip =~ regex ||
city =~ regex ||
country =~ regex ||
hostname =~ regex ||
data =~ regex
# Unfortunately we cannot display the banner properly,
# because it messes with our output format
tbl << ["#{ip}:#{port}", city, country, hostname]
end
end
p += 1
end
# Show data and maybe save it if needed
print_line
print_line("#{tbl}")
save_output(tbl) if datastore['OUTFILE']
end
end