lib/msf/core/auxiliary/ubiquiti.rb
# -*- coding: binary -*-
require 'bson'
require 'zip'
module Msf
###
#
# This module provides methods for working with Ubiquiti equipment
#
###
module Auxiliary::Ubiquiti
include Msf::Auxiliary::Report
def decrypt_unf(contents)
aes = OpenSSL::Cipher.new('aes-128-cbc')
aes.decrypt
aes.key = 'bcyangkmluohmars' # https://github.com/zhangyoufu/unifi-backup-decrypt/blob/master/E>
aes.padding = 0
aes.iv = 'ubntenterpriseap'
aes.update(contents)
end
def repair_zip(fname)
zip_exe = Msf::Util::Helper.which('zip')
if zip_exe.nil?
print_error('Zip utility not found.')
return nil
end
print_status('Attempting to repair zip file (this is normal and takes some time)')
temp_file = Rex::Quickfile.new('fixed_zip')
system("yes | #{zip_exe} -FF #{fname} --out #{temp_file.path}.zip > /dev/null")
return File.read("#{temp_file.path}.zip", mode: 'rb')
end
def extract_and_process_db(db_path)
f = nil
Zip::File.open(db_path) do |zip_file|
# Handle entries one by one
zip_file.each do |entry|
# Extract to file
next unless entry.name == 'db.gz'
print_status('extracting db.gz')
gz = Zlib::GzipReader.new(entry.get_input_stream)
f = gz.read
gz.close
break
end
end
f
end
def bson_to_json(byte_buffer)
# This function takes a byte buffer (db file from Unifi read in), which is a bson string
# it then converts it to JSON, where it uses the 'select collection' documents
# as keys. For instance a bson that contained the follow (displayed in json
# for ease):
# {"__cmd"=>"select", "collection"=>"heatmap"}
# {'example'=>'example'}
# {'example2'=>'example2'}
# would become:
# {'heatmap'=>[{'example'=>'example'}, {'example2'=>'example2'}]}
# this is mainly done to ease the grouping of items for easy navigation later.
buf = BSON::ByteBuffer.new(byte_buffer)
output = {}
key = ''
while buf
begin
# read the document from the buffer
bson = BSON::Document.from_bson(buf)
if bson.has_key?('__cmd')
key = bson['collection']
output[key] = []
next
end
output[key] << bson
rescue RangeError
break
end
end
output
end
def unifi_config_eater(thost, tport, config)
# This is for the Ubiquiti Unifi files. These are typically in the backup download zip file
# then in the db.gz file as db. It is a MongoDB BSON file, which can be difficult to read.
# https://stackoverflow.com/questions/51242412/undefined-method-read-bson-document-for-bsonmodule
# The BSON file is a bunch of BSON Documents chained together. There doesn't seem to be a good
# way to read these files directly, so looping through loading the content seems to work with
# minimal repercussions.
# The file format is broken into sections by __cmd select documents as such:
# {"__cmd"=>"select", "collection"=>"heatmap"}
# we can pull the relevant section name via the collection value.
if framework.db.active
creds_template = {
address: thost,
port: tport,
protocol: 'tcp',
workspace_id: myworkspace_id,
origin_type: :service,
private_type: :password,
service_name: '',
module_fullname: fullname,
status: Metasploit::Model::Login::Status::UNTRIED
}
end
report_host({
host: thost,
info: 'Ubiquiti Unifi Controller'
})
store_loot('unifi.json', 'application/json', thost, config.to_s.strip, 'unifi.json', 'Ubiquiti Unifi Configuration')
# Example BSON lines
# {"__cmd"=>"select", "collection"=>"admin"}
# {"_id"=>BSON::ObjectId('5c7f23af3825ce2067a1d9ce'), "name"=>"adminuser", "email"=>"admin@admin.com", "x_shadow"=>"$6$R4qnAaaF$AAAlL2t.fXu0aaa9z3uvcIm3ujbtJLhIO.lN1xZqHZPQoUAXs2BUTmI5UbuBo2/8t3epzbVLib17Ls7GCVx7V.", "time_created"=>1551825823, "last_site_name"=>"default", "ubic_name"=>"admin@admin.com", "ubic_uuid"=>"c23da064-3f4d-282f-1dc9-7e25f9c6812c", "ui_settings"=>{"dashboardConfig"=>{"lastActiveDashboardId"=>"2c7f2d213813ce2487d1ac38", "dashboards"=>{"3c7f678a3815ce2021d1d9c7"=>{"order"=>1}, "5b4f2d269115ce2087d1abb9"=>{}}}}}
def process_admin(lines, credential_data)
lines.each do |line|
admin_name = line['name']
admin_email = line['email']
admin_password_hash = line['x_shadow']
print_good("Admin user #{admin_name} with email #{admin_email} found with password hash #{admin_password_hash}")
next unless framework.db.active
cred = credential_data.dup
cred[:username] = admin_name
cred[:private_data] = admin_password_hash
cred[:private_type] = :nonreplayable_hash
create_credential_and_login(cred)
end
end
# Example BSON lines
# {"__cmd"=>"select", "collection"=>"firewallrule"}
# {"_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"}
def process_firewallrule(lines, _)
lines.each do |line|
rule = (line['action']).to_s
unless line['dst_address'].empty?
rule << " dst addresses: #{line['dst_address']}"
end
unless line['dst_firewallgroup_ids'].empty?
rule << " dst group: #{line['dst_firewallgroup_ids'].join(', ')}"
end
unless line['src_address'].empty?
rule << " src addresses: #{line['src_address']}"
end
unless line['src_firewallgroup_ids'].empty?
rule << " src group: #{line['src_firewallgroup_ids'].join(', ')}"
end
rule << " protocol: #{line['protocol']}"
print_status("#{line['enabled'] ? 'Enabled' : 'Disabled'} Firewall Rule '#{line['name']}': #{rule}")
end
end
# Example BSON lines
# {"__cmd"=>"select", "collection"=>"radiusprofile"}
# {"_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"=>[]}
def process_radiusprofile(lines, credential_data)
lines.each do |line|
line['auth_servers'].each do |server|
report_service(
host: server['ip'],
port: server['port'],
name: 'radius',
proto: 'udp'
)
next unless server['x_secret'] # no need to output if the secret is blank, therefore its not configured
print_good("Radius server: #{server['ip']}:#{server['port']} with secret '#{server['x_secret']}'")
next unless framework.db.active
cred = credential_data.dup
cred[:username] = ''
cred[:private_data] = server['x_secret']
cred[:address] = server['ip']
cred[:port] = server['port']
create_credential_and_login(cred)
end
end
end
# settings has multiple items we care about:
# x_mesh_essid/x_mesh_psk -> should contain the mesh network wifi name and password
# ntp -> ntp servers
# x_ssh_username/x_ssh_password/x_ssh_keys/x_ssh_sha512passwd
# Example lines
# {"__cmd"=>"select", "collection"=>"setting"}
# {"_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"}
# {"_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."}
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9bc'), "site_id"=>"3c2f215b3825ca2087c1dfb6", "key"=>"connectivity", "enabled"=>true, "uplink_type"=>"gateway", "x_mesh_essid"=>"vwire-851237d214c8c6ba", "x_mesh_psk"=>"523a9b872b4624c7894f96c3ae22cdfa"}
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9bd'), "site_id"=>"3c2f215b3825ca2087c1dfb6", "key"=>"snmp", "community": "public", "enabled": true, "enabledV3": true, "username": "usernamesnmpv3", "x_password": "passwordsnmpv3"}
def process_setting(lines, credential_data)
lines.each do |line|
case line['key']
when 'snmp'
if framework.db.active
cred = credential_data.dup
cred[:protocol] = 'udp'
cred[:port] = 161
cred[:service_name] = 'snmp'
else
cred = {} # throw away
end
unless line['community'].blank?
print_good("SNMP v2 #{line['enabled'] ? 'enabled' : 'disabled'} with password #{line['community']}")
cred[:private_data] = line['community']
create_credential_and_login(cred) if framework.db.active
end
unless line['x_password'].blank? || line['username'].blank?
print_good("SNMP v3 #{line['enabledV3'] ? 'enabled' : 'disabled'} with username #{line['username']} password #{line['x_password']}")
cred[:username] = line['username']
cred[:private_data] = line['x_password']
create_credential_and_login(cred) if framework.db.active
end
when 'connectivity'
print_good("Mesh Wifi Network #{line['x_mesh_essid']} password #{line['x_mesh_psk']}")
next unless framework.db.active
cred = credential_data.dup
cred[:username] = line['x_mesh_essid']
cred[:private_data] = line['x_mesh_psk']
create_credential_and_login(cred)
when 'ntp'
['ntp_server_1', 'ntp_server_2', 'ntp_server_3', 'ntp_server_4'].each do |ntp|
next if line[ntp].empty? || line[ntp].ends_with?('ubnt.pool.ntp.org')
report_service(
host: line[ntp],
port: '123',
name: 'ntp',
proto: 'udp'
)
print_good("NTP Server: #{line[ntp]}")
end
when 'mgmt'
admin_name = line['x_ssh_username']
admin_password_hash = line['x_ssh_sha512passwd']
admin_password = line['x_ssh_password']
print_good("SSH user #{admin_name} found with password #{admin_password} and hash #{admin_password_hash}")
line['x_ssh_keys'].each do |key|
print_good("SSH user #{admin_name} found with SSH key: #{key}")
end
next unless framework.db.active
cred = credential_data.dup
cred[:username] = admin_name
cred[:private_data] = admin_password_hash
cred[:private_type] = :nonreplayable_hash
login = create_credential_and_login(cred)
if login.present? && admin_password.present?
create_cracked_credential(username: admin_name, password: admin_password, core_id: login.core.id)
end
end
end
end
# Example lines
# {"__cmd"=>"select", "collection"=>"wlanconf"}
# {"_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}
def process_wlanconf(lines, credential_data)
lines.each do |line|
ssid = line['name']
mode = line['security']
password = line['x_passphrase']
print_good("#{line['enabled'] ? 'Enabled' : 'Disabled'} wifi #{ssid} on #{mode}(#{line['wpa_mode']},#{line['wpa_enc']}) has password #{password}")
next unless framework.db.active
cred = credential_data.dup
cred[:username] = ssid
cred[:private_data] = password
create_credential_and_login(cred)
end
end
# Example lines
# {"__cmd"=>"select", "collection"=>"firewallgroup"}
# {"_id"=>BSON::ObjectId('3c3e21ac3715ce20a721d9ba'), "name" => "Cameras", "group_type" => "address-group", "group_members" => ["1.1.1.1"], "site_id" => "5c7f111b3815ce208aaa111a"}
def process_firewallgroup(lines, _)
lines.each do |line|
print_status("Firewall Group: #{line['name']}, group type: #{line['group_type']}, members: #{line['group_members'].join(', ')}")
end
end
# Example lines
# {"__cmd"=>"select", "collection"=>"device"}
# {"_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}
def process_device(lines, _)
lines.each do |line|
report_host({
host: line['ip'],
name: line['name'],
mac: line['mac'],
os_name: 'Ubiquiti Unifi'
})
print_good("Unifi Device #{line['name']} of model #{line['model']} on #{line['ip']}")
end
end
# Example lines
# {"__cmd"=>"select", "collection"=>"user"}
# {"_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"}
def process_user(lines, _)
lines.each do |line|
host_hash = {
name: line['hostname'],
mac: line['mac']
}
desc = "#{line['hostname']} (#{line['mac']})"
if line['fixed_ip']
host_hash[:host] = line['fixed_ip']
desc << " on IP #{line['fixed_ip']}"
end
if line['name']
host_hash[:info] = line['name']
desc << " with name #{line['name']}"
end
report_host(host_hash)
print_good("Network Device #{desc} found")
end
end
# here is where we actually process the file
config.each do |key, value|
next unless respond_to?("process_#{key}")
credential_data = creds_template.dup
send("process_#{key}", value, credential_data)
end
end
end
end