5
5
6
6
require 'msf/core'
7
7
require 'rex'
8
- require 'net/dns'
8
+ require 'net/https'
9
+ require 'uri'
9
10
10
11
class Metasploit4 < Msf ::Auxiliary
11
12
@@ -16,170 +17,171 @@ def initialize(info = {})
16
17
super ( update_info ( info ,
17
18
'Name' => 'Shodan Search' ,
18
19
'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
30
28
} ,
31
29
'Author' =>
32
30
[
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
35
33
] ,
36
34
'License' => MSF_LICENSE
37
- ) )
35
+ )
36
+ )
38
37
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' )
44
41
45
42
register_options (
46
43
[
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
+ OptRegexp . new ( 'REGEX ' , [ true , 'Regex search for a specific IP/City/Country/Hostname' , '.*' ] )
50
+
54
51
] , self . class )
55
52
end
56
53
57
54
# create our Shodan query function that performs the actual web request
58
55
def shodan_query ( query , apikey , page )
59
56
# 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 )
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
+ if res and res . body =~ /<title>401 Unauthorized<\/ title>/
65
+ fail_with ( Failure ::BadConfig , '401 Unauthorized. Your SHODAN_APIKEY is invalid' )
66
+ end
67
+
68
+ # Check if we can resolve host, got a response,
69
+ # then parse the JSON, and return it
70
+ if res
72
71
results = ActiveSupport ::JSON . decode ( res . body )
73
72
return results
74
73
else
75
- return 'server_error '
74
+ return 'server_response_error '
76
75
end
77
76
end
78
77
78
+ # save output to file
79
79
def save_output ( data )
80
- f = ::File . open ( datastore [ 'OUTFILE' ] , "wb" )
81
- f . write ( data )
82
- f . close
83
- print_status ( "Save results in #{ datastore [ 'OUTFILE' ] } " )
80
+ ::File . open ( datastore [ 'OUTFILE' ] , 'wb' ) do | f |
81
+ f . write ( data )
82
+ print_status ( "Saved results in #{ datastore [ 'OUTFILE' ] } " )
83
+ end
84
84
end
85
85
86
+ # Check to see if api.shodan.io resolves properly
86
87
def shodan_rhost
87
- @res = Net ::DNS ::Resolver . new ( )
88
- dns_query = @res . query ( " #{ datastore [ 'VHOST' ] } " , "A" )
88
+ @res = Net ::DNS ::Resolver . new
89
+ dns_query = @res . query ( 'api.shodan.io' , 'A' )
89
90
if dns_query . answer . length == 0
90
- print_error ( " Could not resolve #{ datastore [ 'VHOST' ] } " )
91
- raise ::Rex ::ConnectError ( vhost , shodan_port )
91
+ print_error ( ' Could not resolve api.shodan.io' )
92
+ raise ::Rex ::ConnectError ( 'api.shodan.io' , '443' )
92
93
end
93
94
dns_query . answer [ 0 ] . to_s . split ( /[\s ,]+/ ) [ 4 ]
94
95
end
95
96
96
- def shodan_rport
97
- 80
98
- end
99
-
100
97
def run
98
+ # check to ensure api.shodan.io is resolvable
99
+ shodan_rhost
101
100
102
101
# create our Shodan request parameters
103
102
query = datastore [ 'QUERY' ]
104
103
apikey = datastore [ 'SHODAN_APIKEY' ]
105
-
106
104
page = 1
105
+ maxpage = datastore [ 'MAXPAGE' ]
107
106
108
107
# results gets our results from shodan_query
109
108
results = [ ]
110
109
results [ page ] = shodan_query ( query , apikey , page )
111
110
112
- if results [ page ] . empty?
113
- print_error ( "No Results Found!" )
114
- return
111
+ if results [ page ] [ 'total' ] . nil? || results [ page ] [ 'total' ] == 0
112
+ print_error ( 'No Results Found!' )
115
113
end
116
114
117
115
# Determine page count based on total results
118
- if results [ page ] [ 'total' ] % 50 == 0
119
- tpages = results [ page ] [ 'total' ] / 50
116
+ if results [ page ] [ 'total' ] % 100 == 0
117
+ tpages = results [ page ] [ 'total' ] / 100
120
118
else
121
- tpages = results [ page ] [ 'total' ] /50 + 1
119
+ tpages = results [ page ] [ 'total' ] / 100 + 1
120
+ maxpage = tpages if datastore [ 'MAXPAGE' ] > tpages
122
121
end
123
122
124
123
# 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 )
124
+ print_status ( "Total: #{ results [ page ] [ 'total' ] } on #{ tpages } " +
125
+ "pages. Showing: #{ maxpage } page(s)" )
126
+
127
+ # If search results greater than 100, loop & get all results
128
+ print_status ( 'Collecting data, please wait...' )
129
+ if results [ page ] [ 'total' ] > 100
134
130
page += 1
135
- while page <= tpages
136
- results [ page ] = shodan_query ( query , apikey , page )
137
- page +=1
131
+ while page <= maxpage
138
132
break if page > datastore [ 'MAXPAGE' ]
133
+ results [ page ] = shodan_query ( query , apikey , page )
134
+ page += 1
139
135
end
140
136
end
141
137
142
138
# Save the results to this table
143
139
tbl = Rex ::Ui ::Text ::Table . new (
144
- 'Header' => 'IP Results' ,
140
+ 'Header' => 'Search Results' ,
145
141
'Indent' => 1 ,
146
- 'Columns' => [ 'IP' , 'City' , 'Country' , 'Hostname' ]
142
+ 'Columns' => [ 'IP:Port ' , 'City' , 'Country' , 'Hostname' ]
147
143
)
148
144
149
- # Organize results and put them into the table
150
- page = 1
151
- my_filter = datastore [ 'FILTER' ]
152
- for i in page ..tpages
153
- next if results [ i ] . nil? or results [ i ] [ 'matches' ] . nil?
154
- results [ i ] [ 'matches' ] . each { |host |
155
-
156
- city = host [ 'city' ] || 'N/A'
157
- ip = host [ 'ip' ] || 'N/A'
145
+ # Organize results and put them into the table and database
146
+ p = 1
147
+ regex = datastore [ 'REGEX' ] if datastore [ 'REGEX' ]
148
+ while p <= maxpage
149
+ break if p > maxpage
150
+ results [ p ] [ 'matches' ] . each do |host |
151
+ city = host [ 'location' ] [ 'city' ] || 'N/A'
152
+ ip = host [ 'ip_str' ] || 'N/A'
158
153
port = host [ 'port' ] || ''
159
- country = host [ 'country_name' ] || 'N/A'
154
+ country = host [ 'location' ] [ ' country_name'] || 'N/A'
160
155
hostname = host [ 'hostnames' ] [ 0 ]
161
156
data = host [ 'data' ]
162
157
163
- if ip =~ /#{ my_filter } / or
164
- city =~ /#{ my_filter } /i or
165
- country =~ /#{ my_filter } /i or
166
- hostname =~ /#{ my_filter } /i or
167
- data =~ /#{ my_filter } /i
168
- # Unfortunately we cannot display the banner properly,
169
- # because it messes with our output format
170
- tbl << [ "#{ ip } :#{ port } " , city , country , hostname ]
158
+ report_host ( :host => ip ,
159
+ :name => hostname ,
160
+ :comments => 'Added from Shodan' ,
161
+ :info => host [ 'info' ]
162
+ ) if datastore [ 'DATABASE' ]
163
+
164
+ report_service ( :host => ip ,
165
+ :port => port ,
166
+ :info => 'Added from Shodan'
167
+ ) if datastore [ 'DATABASE' ]
168
+
169
+ if ip =~ regex ||
170
+ city =~ regex ||
171
+ country =~ regex ||
172
+ hostname =~ regex ||
173
+ data =~ regex
174
+ # Unfortunately we cannot display the banner properly,
175
+ # because it messes with our output format
176
+ tbl << [ "#{ ip } :#{ port } " , city , country , hostname ]
171
177
end
172
- }
178
+ end
179
+ p += 1
173
180
end
174
181
175
- #Show data and maybe save it if needed
176
- print_line ( "\n #{ tbl . to_s } " )
177
-
178
- report_note (
179
- :type => 'shodan' ,
180
- :data => tbl . to_csv
181
- ) if datastore [ 'DATABASE' ]
182
-
183
- save_output ( tbl . to_s ) if not datastore [ 'OUTFILE' ] . nil?
182
+ # Show data and maybe save it if needed
183
+ print_line
184
+ print_line ( "#{ tbl } " )
185
+ save_output ( tbl ) if datastore [ 'OUTFILE' ]
184
186
end
185
187
end
0 commit comments