CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/gather/elasticsearch_enum.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
include Msf::Exploit::Remote::HttpClient
8
include Msf::Auxiliary::Report
9
include Msf::Module::Deprecated
10
11
moved_from 'auxiliary/scanner/elasticsearch/indices_enum'
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Elasticsearch Enumeration Utility',
18
'Description' => %q{
19
This module enumerates Elasticsearch instances. It uses the REST API
20
in order to gather information about the server, the cluster, nodes,
21
in the cluster, indices, and pull data from those indices.
22
},
23
'Author' => [
24
'Silas Cutler <Silas.Cutler[at]BlackListThisDomain.com>', # original indices enum module
25
'h00die' # generic enum module
26
],
27
'References' => [
28
['URL', 'https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html']
29
],
30
'License' => MSF_LICENSE,
31
'DefaultOptions' => {
32
'SSL' => true
33
},
34
'Notes' => {
35
'Stability' => [CRASH_SAFE],
36
'Reliability' => [],
37
'SideEffects' => [IOC_IN_LOGS]
38
}
39
)
40
)
41
42
register_options(
43
[
44
Opt::RPORT(9200),
45
OptString.new('USERNAME', [false, 'A specific username to authenticate as', '']),
46
OptString.new('PASSWORD', [false, 'A specific password to authenticate as', '']),
47
OptInt.new('DOWNLOADROWS', [true, 'Number of beginning and ending rows to download per index', 5])
48
]
49
)
50
end
51
52
def get_results(index)
53
vprint_status("Downloading #{datastore['DOWNLOADROWS']} rows from index #{index}")
54
body = { 'query' => { 'query_string' => { 'query' => '*' } }, 'size' => datastore['DOWNLOADROWS'], 'from' => 0, 'sort' => [] }
55
request = {
56
'uri' => normalize_uri(target_uri.path, index, '_search/'),
57
'method' => 'POST',
58
'headers' => {
59
'Accept' => 'application/json'
60
},
61
'ctype' => 'application/json',
62
'data' => body.to_json
63
}
64
request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']
65
66
res = send_request_cgi(request)
67
vprint_error('Unable to establish connection') if res.nil?
68
69
if res && res.code == 200 && !res.body.empty?
70
json_body = res.get_json_document
71
if json_body.empty?
72
vprint_error('Unable to parse JSON')
73
return
74
end
75
else
76
vprint_error('Timeout or unexpected response...')
77
return
78
end
79
80
columns = json_body.dig('hits', 'hits')[0]['_source'].keys
81
elastic_table = Rex::Text::Table.new(
82
'Header' => "#{index} Data",
83
'Indent' => 2,
84
# we know at least 1 row since we wouldn't query an index w/o a row
85
'Columns' => columns
86
)
87
json_body.dig('hits', 'hits').each do |hash|
88
elastic_table << columns.map { |column| hash['_source'][column] }
89
end
90
91
l = store_loot('elasticserch.index.data', 'application/csv', rhost, elastic_table.to_csv, "#{index}_data.csv", nil, @service)
92
print_good("#{index} data stored to #{l}")
93
end
94
95
def get_indices
96
vprint_status('Querying indices...')
97
request = {
98
'uri' => normalize_uri(target_uri.path, '_cat', 'indices/'),
99
'method' => 'GET',
100
'headers' => {
101
'Accept' => 'application/json'
102
},
103
'vars_get' => {
104
# this is the query https://github.com/cars10/elasticvue uses for the chrome browser extension
105
'h' => 'index,health,status,uuid,docs.count,store.size',
106
'bytes' => 'mb'
107
}
108
}
109
request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']
110
111
res = send_request_cgi(request)
112
vprint_error('Unable to establish connection') if res.nil?
113
114
if res && res.code == 200 && !res.body.empty?
115
json_body = res.get_json_document
116
if json_body.empty?
117
vprint_error('Unable to parse JSON')
118
return
119
end
120
else
121
vprint_error('Timeout or unexpected response...')
122
return
123
end
124
125
elastic_table = Rex::Text::Table.new(
126
'Header' => 'Indicies Information',
127
'Indent' => 2,
128
'Columns' =>
129
[
130
'Name',
131
'Health',
132
'Status',
133
'UUID',
134
'Documents',
135
'Storage Usage (MB)'
136
]
137
)
138
139
indices = []
140
141
json_body.each do |index|
142
next if datastore['VERBOSE'] == false && index['index'].starts_with?('.fleet')
143
144
indices << index['index'] if index['docs.count'].to_i > 0 # avoid querying something with no data
145
elastic_table << [
146
index['index'],
147
index['health'],
148
index['status'],
149
index['uuid'],
150
index['docs.count'],
151
"#{index['store.size']}MB"
152
]
153
report_note(
154
host: rhost,
155
port: rport,
156
proto: 'tcp',
157
type: 'elasticsearch.index',
158
data: index[0],
159
update: :unique_data
160
)
161
end
162
163
print_good(elastic_table.to_s)
164
indices.each do |index|
165
get_results(index)
166
end
167
end
168
169
def get_cluster_info
170
vprint_status('Querying cluster information...')
171
request = {
172
'uri' => normalize_uri(target_uri.path, '_cluster', 'health'),
173
'method' => 'GET'
174
}
175
request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']
176
177
res = send_request_cgi(request)
178
179
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
180
fail_with(Failure::NoAccess, 'Credentials required, or incorrect') if res.code == 401
181
182
if res.code == 200 && !res.body.empty?
183
json_body = res.get_json_document
184
if json_body.empty?
185
vprint_error('Unable to parse JSON')
186
return
187
end
188
end
189
190
elastic_table = Rex::Text::Table.new(
191
'Header' => 'Cluster Information',
192
'Indent' => 2,
193
'Columns' =>
194
[
195
'Cluster Name',
196
'Status',
197
'Number of Nodes'
198
]
199
)
200
201
elastic_table << [
202
json_body['cluster_name'],
203
json_body['status'],
204
json_body['number_of_nodes']
205
]
206
print_good(elastic_table.to_s)
207
end
208
209
def get_node_info
210
vprint_status('Querying node information...')
211
request = {
212
'uri' => normalize_uri(target_uri.path, '_cat', 'nodes'),
213
'method' => 'GET',
214
'headers' => {
215
'Accept' => 'application/json'
216
},
217
'vars_get' => {
218
'h' => 'ip,port,version,http,uptime,name,heap.current,heap.max,ram.current,ram.max,node.role,master,cpu,disk.used,disk.total'
219
}
220
}
221
request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']
222
223
res = send_request_cgi(request)
224
225
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
226
fail_with(Failure::NoAccess, 'Credentials required, or incorrect') if res.code == 401
227
228
if res.code == 200 && !res.body.empty?
229
json_body = res.get_json_document
230
if json_body.empty?
231
vprint_error('Unable to parse JSON')
232
return
233
end
234
end
235
236
elastic_table = Rex::Text::Table.new(
237
'Header' => 'Node Information',
238
'Indent' => 2,
239
'Columns' =>
240
[
241
'IP',
242
'Transport Port',
243
'HTTP Port',
244
'Version',
245
'Name',
246
'Uptime',
247
'Ram Usage',
248
'Node Role',
249
'Master',
250
'CPU Load',
251
'Disk Usage'
252
]
253
)
254
json_body.each do |node|
255
report_service(
256
host: node['ip'],
257
port: node['port'],
258
proto: 'tcp',
259
name: 'elasticsearch'
260
)
261
report_service(
262
host: node['ip'],
263
port: node['http'].split(':')[1],
264
proto: 'tcp',
265
name: 'elasticsearch'
266
)
267
elastic_table << [
268
node['ip'],
269
node['port'],
270
node['http'],
271
node['version'],
272
node['name'],
273
node['uptime'],
274
"#{node['ram.current']}/#{node['ram.max']}",
275
node['node.role'],
276
node['master'],
277
"#{node['cpu']}%",
278
"#{node['disk.used']}/#{node['disk.total']}"
279
]
280
end
281
print_good(elastic_table.to_s)
282
end
283
284
def get_version_info
285
vprint_status('Querying version information...')
286
request = {
287
'uri' => normalize_uri(target_uri.path),
288
'method' => 'GET'
289
}
290
request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']
291
292
res = send_request_cgi(request)
293
294
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
295
fail_with(Failure::NoAccess, 'Credentials required, or incorrect') if res.code == 401
296
297
# leaving this here for future travelers, this header was added in 7.14.0 https://www.elastic.co/guide/en/elasticsearch/reference/7.17/release-notes-7.14.0.html
298
# so it isn't too reliable to check for
299
# fail_with(Failure::Unreachable, "#{peer} - Elasticsearch not detected in X-elastic-product header") unless res.headers['X-elastic-product'] == 'Elasticsearch'
300
301
if res.code == 200 && !res.body.empty?
302
json_body = res.get_json_document
303
if json_body.empty?
304
vprint_error('Unable to parse JSON')
305
return
306
end
307
end
308
309
fail_with(Failure::Unreachable, "#{peer} - Elasticsearch cluster name not found, likely not Elasticsearch server") unless json_body['cluster_name']
310
311
elastic_table = Rex::Text::Table.new(
312
'Header' => 'Elastic Information',
313
'Indent' => 2,
314
'Columns' =>
315
[
316
'Name',
317
'Cluster Name',
318
'Version',
319
'Build Type',
320
'Lucene Version'
321
]
322
)
323
324
elastic_table << [
325
json_body['name'],
326
json_body['cluster_name'],
327
json_body.dig('version', 'number'),
328
json_body.dig('version', 'build_type'),
329
json_body.dig('version', 'lucene_version'),
330
]
331
print_good(elastic_table.to_s)
332
333
@service = report_service(
334
host: rhost,
335
port: rport,
336
proto: 'tcp',
337
name: 'elasticsearch'
338
)
339
end
340
341
def get_users
342
vprint_status('Querying user information...')
343
request = {
344
'uri' => normalize_uri(target_uri.path, '_security', 'user/'),
345
'method' => 'GET'
346
}
347
request['authorization'] = basic_auth(datastore['USERNAME'], datastore['PASSWORD']) if datastore['USERNAME'] || datastore['PASSWORD']
348
349
res = send_request_cgi(request)
350
351
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
352
fail_with(Failure::NoAccess, 'Credentials required, or incorrect') if res.code == 401
353
354
if res.code == 200 && !res.body.empty?
355
json_body = res.get_json_document
356
if json_body.empty?
357
vprint_error('Unable to parse JSON')
358
return
359
end
360
end
361
362
if json_body.nil?
363
print_bad('Unable to pull user data')
364
return
365
end
366
367
elastic_table = Rex::Text::Table.new(
368
'Header' => 'User Information',
369
'Indent' => 2,
370
'Columns' =>
371
[
372
'Name',
373
'Roles',
374
'Email',
375
'Metadata',
376
'Enabled'
377
]
378
)
379
380
json_body.each do |username, attributes|
381
elastic_table << [
382
username,
383
attributes['roles'],
384
attributes['email'],
385
attributes['metadata'],
386
attributes['enabled'],
387
]
388
end
389
print_good(elastic_table.to_s)
390
end
391
392
def run
393
get_version_info
394
get_node_info
395
get_cluster_info
396
get_indices
397
get_users
398
end
399
end
400
401