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/lib/msf/core/auxiliary/ubiquiti.rb
Views: 1904
1
# -*- coding: binary -*-
2
3
require 'bson'
4
require 'zip'
5
6
module Msf
7
###
8
#
9
# This module provides methods for working with Ubiquiti equipment
10
#
11
###
12
module Auxiliary::Ubiquiti
13
include Msf::Auxiliary::Report
14
15
def decrypt_unf(contents)
16
aes = OpenSSL::Cipher.new('aes-128-cbc')
17
aes.decrypt
18
aes.key = 'bcyangkmluohmars' # https://github.com/zhangyoufu/unifi-backup-decrypt/blob/master/E>
19
aes.padding = 0
20
aes.iv = 'ubntenterpriseap'
21
aes.update(contents)
22
end
23
24
def repair_zip(fname)
25
zip_exe = Msf::Util::Helper.which('zip')
26
if zip_exe.nil?
27
print_error('Zip utility not found.')
28
return nil
29
end
30
print_status('Attempting to repair zip file (this is normal and takes some time)')
31
temp_file = Rex::Quickfile.new('fixed_zip')
32
system("yes | #{zip_exe} -FF #{fname} --out #{temp_file.path}.zip > /dev/null")
33
return File.read("#{temp_file.path}.zip", mode: 'rb')
34
end
35
36
def extract_and_process_db(db_path)
37
f = nil
38
Zip::File.open(db_path) do |zip_file|
39
# Handle entries one by one
40
zip_file.each do |entry|
41
# Extract to file
42
next unless entry.name == 'db.gz'
43
44
print_status('extracting db.gz')
45
gz = Zlib::GzipReader.new(entry.get_input_stream)
46
f = gz.read
47
gz.close
48
break
49
end
50
end
51
f
52
end
53
54
def bson_to_json(byte_buffer)
55
# This function takes a byte buffer (db file from Unifi read in), which is a bson string
56
# it then converts it to JSON, where it uses the 'select collection' documents
57
# as keys. For instance a bson that contained the follow (displayed in json
58
# for ease):
59
# {"__cmd"=>"select", "collection"=>"heatmap"}
60
# {'example'=>'example'}
61
# {'example2'=>'example2'}
62
# would become:
63
# {'heatmap'=>[{'example'=>'example'}, {'example2'=>'example2'}]}
64
# this is mainly done to ease the grouping of items for easy navigation later.
65
66
buf = BSON::ByteBuffer.new(byte_buffer)
67
output = {}
68
key = ''
69
70
while buf
71
begin
72
# read the document from the buffer
73
bson = BSON::Document.from_bson(buf)
74
if bson.has_key?('__cmd')
75
key = bson['collection']
76
output[key] = []
77
next
78
end
79
output[key] << bson
80
rescue RangeError
81
break
82
end
83
end
84
output
85
end
86
87
def unifi_config_eater(thost, tport, config)
88
# This is for the Ubiquiti Unifi files. These are typically in the backup download zip file
89
# then in the db.gz file as db. It is a MongoDB BSON file, which can be difficult to read.
90
# https://stackoverflow.com/questions/51242412/undefined-method-read-bson-document-for-bsonmodule
91
# The BSON file is a bunch of BSON Documents chained together. There doesn't seem to be a good
92
# way to read these files directly, so looping through loading the content seems to work with
93
# minimal repercussions.
94
95
# The file format is broken into sections by __cmd select documents as such:
96
# {"__cmd"=>"select", "collection"=>"heatmap"}
97
# we can pull the relevant section name via the collection value.
98
99
if framework.db.active
100
creds_template = {
101
address: thost,
102
port: tport,
103
protocol: 'tcp',
104
workspace_id: myworkspace_id,
105
origin_type: :service,
106
private_type: :password,
107
service_name: '',
108
module_fullname: fullname,
109
status: Metasploit::Model::Login::Status::UNTRIED
110
}
111
end
112
113
report_host({
114
host: thost,
115
info: 'Ubiquiti Unifi Controller'
116
})
117
118
store_loot('unifi.json', 'application/json', thost, config.to_s.strip, 'unifi.json', 'Ubiquiti Unifi Configuration')
119
120
# Example BSON lines
121
# {"__cmd"=>"select", "collection"=>"admin"}
122
# {"_id"=>BSON::ObjectId('5c7f23af3825ce2067a1d9ce'), "name"=>"adminuser", "email"=>"[email protected]", "x_shadow"=>"$6$R4qnAaaF$AAAlL2t.fXu0aaa9z3uvcIm3ujbtJLhIO.lN1xZqHZPQoUAXs2BUTmI5UbuBo2/8t3epzbVLib17Ls7GCVx7V.", "time_created"=>1551825823, "last_site_name"=>"default", "ubic_name"=>"[email protected]", "ubic_uuid"=>"c23da064-3f4d-282f-1dc9-7e25f9c6812c", "ui_settings"=>{"dashboardConfig"=>{"lastActiveDashboardId"=>"2c7f2d213813ce2487d1ac38", "dashboards"=>{"3c7f678a3815ce2021d1d9c7"=>{"order"=>1}, "5b4f2d269115ce2087d1abb9"=>{}}}}}
123
def process_admin(lines, credential_data)
124
lines.each do |line|
125
admin_name = line['name']
126
admin_email = line['email']
127
admin_password_hash = line['x_shadow']
128
print_good("Admin user #{admin_name} with email #{admin_email} found with password hash #{admin_password_hash}")
129
next unless framework.db.active
130
131
cred = credential_data.dup
132
cred[:username] = admin_name
133
cred[:private_data] = admin_password_hash
134
cred[:private_type] = :nonreplayable_hash
135
create_credential_and_login(cred)
136
end
137
end
138
139
# Example BSON lines
140
# {"__cmd"=>"select", "collection"=>"firewallrule"}
141
# {"_id"=>BSON::ObjectId('5c7f23af3825ce2067a1d9ce'), "ruleset" => "WAN_OUT", "rule_index" => "2000", "name" => "Block Example", "enabled" => true, "action" => "reject", "protocol_match_excepted" => false, "logging" => false, "state_new" => false, "state_established" => false, "state_invalid" => false, "state_related" => false, "ipsec" => "", "src_firewallgroup_ids" => ["1a1c15a11111ce14b1f1111a"], "src_mac_address" => "", "dst_firewallgroup_ids" => [], "dst_address" => "", "src_address" => "", "protocol" => "all", "icmp_typename" => "", "src_networkconf_id" => "", "src_networkconf_type" => "NETv4", "dst_networkconf_id" => "", "dst_networkconf_type" => "NETv4", "site_id" => "1c1f208b3815ce1111a1a1a1"}
142
def process_firewallrule(lines, _)
143
lines.each do |line|
144
rule = (line['action']).to_s
145
unless line['dst_address'].empty?
146
rule << " dst addresses: #{line['dst_address']}"
147
end
148
unless line['dst_firewallgroup_ids'].empty?
149
rule << " dst group: #{line['dst_firewallgroup_ids'].join(', ')}"
150
end
151
unless line['src_address'].empty?
152
rule << " src addresses: #{line['src_address']}"
153
end
154
unless line['src_firewallgroup_ids'].empty?
155
rule << " src group: #{line['src_firewallgroup_ids'].join(', ')}"
156
end
157
rule << " protocol: #{line['protocol']}"
158
159
print_status("#{line['enabled'] ? 'Enabled' : 'Disabled'} Firewall Rule '#{line['name']}': #{rule}")
160
end
161
end
162
163
# Example BSON lines
164
# {"__cmd"=>"select", "collection"=>"radiusprofile"}
165
# {"_id"=>BSON::ObjectId('2c7a318c38c5ce2f86d179cb'), "attr_no_delete"=>true, "attr_hidden_id"=>"Default", "name"=>"Default", "site_id"=>"3c7f226b2315be2087a1d5b2", "use_usg_auth_server"=>true, "auth_servers"=>[{"ip"=>"192.168.0.1", "port"=>1812, "x_secret"=>""}], "acct_servers"=>[]}
166
def process_radiusprofile(lines, credential_data)
167
lines.each do |line|
168
line['auth_servers'].each do |server|
169
report_service(
170
host: server['ip'],
171
port: server['port'],
172
name: 'radius',
173
proto: 'udp'
174
)
175
next unless server['x_secret'] # no need to output if the secret is blank, therefore its not configured
176
177
print_good("Radius server: #{server['ip']}:#{server['port']} with secret '#{server['x_secret']}'")
178
next unless framework.db.active
179
180
cred = credential_data.dup
181
cred[:username] = ''
182
cred[:private_data] = server['x_secret']
183
cred[:address] = server['ip']
184
cred[:port] = server['port']
185
create_credential_and_login(cred)
186
end
187
end
188
end
189
190
# settings has multiple items we care about:
191
# x_mesh_essid/x_mesh_psk -> should contain the mesh network wifi name and password
192
# ntp -> ntp servers
193
# x_ssh_username/x_ssh_password/x_ssh_keys/x_ssh_sha512passwd
194
195
# Example lines
196
# {"__cmd"=>"select", "collection"=>"setting"}
197
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9ba'), "site_id"=>"3c2f215b3825ca2087c1dfb6", "key"=>"ntp", "ntp_server_1"=>"0.ubnt.pool.ntp.org", "ntp_server_2"=>"1.ubnt.pool.ntp.org", "ntp_server_3"=>"2.ubnt.pool.ntp.org", "ntp_server_4"=>"3.ubnt.pool.ntp.org"}
198
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9bb'), "site_id"=>"3c2f215b3825ca2087c1dfb6", "key"=>"mgmt", "advanced_feature_enabled"=>false, "x_ssh_enabled"=>true, "x_ssh_bind_wildcard"=>false, "x_ssh_auth_password_enabled"=>true, "unifi_idp_enabled"=>true, "x_mgmt_key"=>"ba6cbe170f8276cd86b24ac79ab29afc", "x_ssh_username"=>"admin", "x_ssh_password"=>"16xoB6F2UyAcU6fP", "x_ssh_keys"=>[], "x_ssh_sha512passwd"=>"$6$R4qnAaaF$AAAlL2t.fXu0aaa9z3uvcIm3ujbtJLhIO.lN1xZqHZPQoUAXs2BUTmI5UbuBo2/8t3epzbVLib17Ls7GCVx7V."}
199
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9bc'), "site_id"=>"3c2f215b3825ca2087c1dfb6", "key"=>"connectivity", "enabled"=>true, "uplink_type"=>"gateway", "x_mesh_essid"=>"vwire-851237d214c8c6ba", "x_mesh_psk"=>"523a9b872b4624c7894f96c3ae22cdfa"}
200
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9bd'), "site_id"=>"3c2f215b3825ca2087c1dfb6", "key"=>"snmp", "community": "public", "enabled": true, "enabledV3": true, "username": "usernamesnmpv3", "x_password": "passwordsnmpv3"}
201
def process_setting(lines, credential_data)
202
lines.each do |line|
203
case line['key']
204
when 'snmp'
205
if framework.db.active
206
cred = credential_data.dup
207
cred[:protocol] = 'udp'
208
cred[:port] = 161
209
cred[:service_name] = 'snmp'
210
else
211
cred = {} # throw away
212
end
213
unless line['community'].blank?
214
print_good("SNMP v2 #{line['enabled'] ? 'enabled' : 'disabled'} with password #{line['community']}")
215
cred[:private_data] = line['community']
216
create_credential_and_login(cred) if framework.db.active
217
end
218
unless line['x_password'].blank? || line['username'].blank?
219
print_good("SNMP v3 #{line['enabledV3'] ? 'enabled' : 'disabled'} with username #{line['username']} password #{line['x_password']}")
220
cred[:username] = line['username']
221
cred[:private_data] = line['x_password']
222
create_credential_and_login(cred) if framework.db.active
223
end
224
when 'connectivity'
225
print_good("Mesh Wifi Network #{line['x_mesh_essid']} password #{line['x_mesh_psk']}")
226
next unless framework.db.active
227
228
cred = credential_data.dup
229
cred[:username] = line['x_mesh_essid']
230
cred[:private_data] = line['x_mesh_psk']
231
create_credential_and_login(cred)
232
when 'ntp'
233
['ntp_server_1', 'ntp_server_2', 'ntp_server_3', 'ntp_server_4'].each do |ntp|
234
next if line[ntp].empty? || line[ntp].ends_with?('ubnt.pool.ntp.org')
235
236
report_service(
237
host: line[ntp],
238
port: '123',
239
name: 'ntp',
240
proto: 'udp'
241
)
242
print_good("NTP Server: #{line[ntp]}")
243
end
244
when 'mgmt'
245
admin_name = line['x_ssh_username']
246
admin_password_hash = line['x_ssh_sha512passwd']
247
admin_password = line['x_ssh_password']
248
print_good("SSH user #{admin_name} found with password #{admin_password} and hash #{admin_password_hash}")
249
line['x_ssh_keys'].each do |key|
250
print_good("SSH user #{admin_name} found with SSH key: #{key}")
251
end
252
next unless framework.db.active
253
254
cred = credential_data.dup
255
cred[:username] = admin_name
256
cred[:private_data] = admin_password_hash
257
cred[:private_type] = :nonreplayable_hash
258
login = create_credential_and_login(cred)
259
if login.present? && admin_password.present?
260
create_cracked_credential(username: admin_name, password: admin_password, core_id: login.core.id)
261
end
262
end
263
end
264
end
265
266
# Example lines
267
# {"__cmd"=>"select", "collection"=>"wlanconf"}
268
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9ba'), "enabled" => true, "security" => "wpapsk", "wep_idx" => 1, "wpa_mode" => "wpa2", "wpa_enc" => "ccmp", "usergroup_id" => "5a7f111a3815ce1111a1d1c3", "dtim_mode" => "default", "dtim_ng" => 1, "dtim_na" => 1, "minrate_ng_enabled" => false, "minrate_ng_advertising_rates" => false, "minrate_ng_data_rate_kbps" => 1000, "minrate_ng_cck_rates_enabled" => true, "minrate_na_enabled" => false, "minrate_na_advertising_rates" => false, "minrate_na_data_rate_kbps" => 6000, "mac_filter_enabled" => false, "mac_filter_policy" => "allow", "mac_filter_list" => [], "bc_filter_enabled" => false, "bc_filter_list" => [], "group_rekey" => 3600, "name" => "ssid_name", "x_passphrase" => "supersecret", "wlangroup_id" => "5c7f208c3815ce2087d1d9c4", "schedule" => [], "minrate_ng_mgmt_rate_kbps" => 1000, "minrate_na_mgmt_rate_kbps" => 6000, "minrate_ng_beacon_rate_kbps" => 1000, "minrate_na_beacon_rate_kbps" => 6000, "site_id" => "5c7f208b3815ce2087d1d9b6", "x_iapp_key" => "d11a1c86df1111be86aaa69e8aa1c57f", "no2ghz_oui" => true}
269
def process_wlanconf(lines, credential_data)
270
lines.each do |line|
271
ssid = line['name']
272
mode = line['security']
273
password = line['x_passphrase']
274
print_good("#{line['enabled'] ? 'Enabled' : 'Disabled'} wifi #{ssid} on #{mode}(#{line['wpa_mode']},#{line['wpa_enc']}) has password #{password}")
275
next unless framework.db.active
276
277
cred = credential_data.dup
278
cred[:username] = ssid
279
cred[:private_data] = password
280
create_credential_and_login(cred)
281
end
282
end
283
284
# Example lines
285
# {"__cmd"=>"select", "collection"=>"firewallgroup"}
286
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9ba'), "name" => "Cameras", "group_type" => "address-group", "group_members" => ["1.1.1.1"], "site_id" => "5c7f111b3815ce208aaa111a"}
287
def process_firewallgroup(lines, _)
288
lines.each do |line|
289
print_status("Firewall Group: #{line['name']}, group type: #{line['group_type']}, members: #{line['group_members'].join(', ')}")
290
end
291
end
292
293
# Example lines
294
# {"__cmd"=>"select", "collection"=>"device"}
295
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9ba'), "ip" => "5.5.5.5", "mac" => "cc:cc:cc:cc:cc:cc", "model" => "UGW3", "type" => "ugw", "version" => "4.4.44.5213844", "adopted" => true, "site_id" => "5aaaaaabaaaaae1117d1d1b6", "x_authkey" => "eaaaaaaa63e59ab89c111e11d6e11aa1", "cfgversion" => "aaa4b11b1df1a111", "config_network" => {"type" => "dhcp", "ip" => "1.1.1.1"}, "license_state" => "registered", "two_phase_adopt" => false, "unsupported" => false, "unsupported_reason" => 0, "x_fingerprint" => "aa:aa:11:aa:11:11:11:11:11:11:11:11:11:11:11:11", "x_ssh_hostkey" => "MIIBIjANBgkAhkiG9w0AAQEFAAOCAQ8AMIIBCgKCAQEAAU4S/7r548xvtGuHlgAAAKzkrL+t97ZWAZru8wQFbltEB4111HiIAkzt041td8V+P7c1bQtn3YQdViAuH2h2sgt8feAvMWo56OskAoDvHwAEv5AWqmPKy/xmKbdfgA5wTzvSztPGFA4QuOuA1YxQICf1MgpoOtplAAA31JxAYF/t7n8qgvJlm1JRv2AAAZHHtSiz1IaxzOO9LAAAqCfHvHugPcZYk2yAAAP7JrnnR1fAVj9F4aaYaA0eSjvDTAglykXHCbh1EWAAAecqHZ/SWn9cjmuAAArZxxG6m6Eu/aj9we82/PmtKzQGN0RWUsgrxajQowtNpVsNTnaOglUsfQIDAAAA", "x_ssh_hostkey_fingerprint" => "11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11", "inform_url" => "http://1.1.2.2:8080/inform", "inform_ip" => "1.1.1.1", "serial" => "AAAAAAAAAAAA", "required_version" => "4.0.0", "ethernet_table" => [{ "mac" => "b4:fb:e4:cc:cc:cc", "num_port" => 1, "name" => "eth0"}, {"mac" => "b4:fb:e4:bb:bb:bb", "num_port" => 1, "name" => "eth1"}, {"mac" => "b4:fb:e4:aa:aa:aa", "num_port" => 1, "name" => "eth2"}], "fw_caps" => 184323, "hw_caps" => 0, "usg_caps" => 786431, "board_rev" => 16, "x_aes_gcm" => true, "ethernet_overrides" => [{"ifname" => "eth1", "networkgroup" => "LAN"}, {"ifname" => "eth0", "networkgroup" => "WAN"}], "led_override" => "default", "led_override_color" => "#0000ff", "led_override_color_brightness" => 100, "outdoor_mode_override" => "default", "name" => "USG", "map_id" => "1a111c2e1111ce2087d1e199", "x" => -22.11111198630405, "y" => -41.1111113859866, "heightInMeters" => 2.4}
296
def process_device(lines, _)
297
lines.each do |line|
298
report_host({
299
host: line['ip'],
300
name: line['name'],
301
mac: line['mac'],
302
os_name: 'Ubiquiti Unifi'
303
})
304
print_good("Unifi Device #{line['name']} of model #{line['model']} on #{line['ip']}")
305
end
306
end
307
308
# Example lines
309
# {"__cmd"=>"select", "collection"=>"user"}
310
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9ba'), "mac" => "00:0c:29:11:aa:11", "site_id" => "5c7f111b1111aa2087d11111", "oui" => "Vmware", "is_guest" => false, "first_seen" => 1551111161, "last_seen" => 1561621747, "is_wired" => true, "hostname" => "android", "usergroup_id" => "", "name" => "example device", "noted" => true, "use_fixedip" => true, "network_id" => "1c7f111a1115aa2087aaa9aa", "fixed_ip" => "7.7.7.7"}
311
def process_user(lines, _)
312
lines.each do |line|
313
host_hash = {
314
name: line['hostname'],
315
mac: line['mac']
316
}
317
desc = "#{line['hostname']} (#{line['mac']})"
318
if line['fixed_ip']
319
host_hash[:host] = line['fixed_ip']
320
desc << " on IP #{line['fixed_ip']}"
321
end
322
if line['name']
323
host_hash[:info] = line['name']
324
desc << " with name #{line['name']}"
325
end
326
report_host(host_hash)
327
print_good("Network Device #{desc} found")
328
end
329
end
330
331
# here is where we actually process the file
332
config.each do |key, value|
333
next unless respond_to?("process_#{key}")
334
335
credential_data = creds_template.dup
336
send("process_#{key}", value, credential_data)
337
end
338
end
339
end
340
end
341
342