Skip to content

Session 5: OS and Application Security — chroot Sandbox

What is chroot?

chroot (change root) is a Linux system call that changes what a process sees as the root (/) of the filesystem. A process running inside a chroot jail cannot navigate above its designated root — it believes that directory is the entire filesystem. This was historically used to isolate processes and limit what they could access.

chroot is the historical foundation of modern containerisation tools like Docker. However, it was never designed to be a true security sandbox, and this lab demonstrates exactly why.

Environment: Ubuntu 24.04 VM (ff-cs-sedat-yalcin, 192.168.80.104), accessed over WireGuard VPN via SSH.


Part 1: Building and Testing the Jail

Step 1 — Create the host secret and set up the jail

First, a sensitive file was created on the host OS to simulate confidential data:

sudo sh -c 'echo "FLAG{CHROOT_IS_NOT_A_SECURE_SANDBOX_1206}" > /host_secret.txt'

BusyBox Static was used as the shell inside the jail — it is a single statically compiled binary that does not depend on external libraries, avoiding dependency issues:

sudo apt install -y busybox-static
mkdir -p ~/chroot_jail/bin
cp $(which busybox) ~/chroot_jail/bin/sh

Step 2 — Enter the jail

sudo chroot ~/chroot_jail /bin/sh

The prompt changed to / # — the jail is now active.

Step 3 — Test containment

List the root directory:

/ # ls -la /
total 12
drwxrwxr-x    3 1000     1000          4096 Jul 13 13:26 .
drwxrwxr-x    3 1000     1000          4096 Jul 13 13:26 ..
drwxrwxr-x    2 1000     1000          4096 Jul 13 13:27 bin

Only the bin directory exists. The rest of the host filesystem is invisible.

Attempt directory traversal:

/ # cd ../../..
/ # ls -la
(same output — still at jail root)

No matter how many .. steps are attempted, the process stays at the jail root.

Attempt to read the host secret:

/ # /bin/cat /host_secret.txt
cat: can't open '/host_secret.txt': No such file or directory

The host file is not visible inside the jail.


Part 2: Questions

Question 1 — Why can’t you exit with cd ../../..?

The Linux kernel intercepts every path resolution for a chroot-ed process. When the process tries to navigate above its designated root, the kernel silently clamps the traversal back to the jail root. The process has no way to reference directories that exist above its root — those paths simply do not exist from its perspective. The kernel rewrites .. at the jail root to point back to the jail root itself.

Question 2 — What happens with nano, python3, or grep?

These commands fail because the jail only contains the BusyBox binary. nano, python3, and grep are installed on the host OS, but their executables are not inside the jail’s filesystem. More importantly, even if the executables were copied in, they would fail — they depend on shared libraries (.so files) that are also not present in the jail. A program cannot run if the dynamic linker cannot find the libraries it needs. BusyBox avoids this problem because it is statically compiled — everything it needs is bundled into a single binary.


Part 3: Expanding the Jail and Escaping It

Adding /bin and /lib to the jail

To give the sandboxed user access to standard tools including a compiler:

sudo cp /bin/* ~/chroot_jail/bin/ 2>/dev/null
mkdir -p ~/chroot_jail/lib/x86_64-linux-gnu ~/chroot_jail/lib64
sudo cp -r /lib/x86_64-linux-gnu ~/chroot_jail/lib/
sudo cp /lib64/ld-linux-x86-64.so.2 ~/chroot_jail/lib64/

Re-entering the jail with bash:

sudo chroot ~/chroot_jail /bin/bash

Now standard tools work:

bash-5.2# python3 --version
Python 3.12.3

bash-5.2# ls /
bin  lib  lib64

The host secret is still not visible — /host_secret.txt returns “No such file or directory”.

Escaping the jail with mount

chroot modifies path resolution but does not restrict what can be mounted. A root process inside the jail can use mount --bind to graft the host’s real root filesystem into a directory inside the jail.

Step 1 — From the host, create a mount point inside the jail and bind-mount the host root:

mkdir -p ~/chroot_jail/escape
sudo mount --bind / ~/chroot_jail/escape

Step 2 — Enter the jail:

sudo chroot ~/chroot_jail /bin/bash

Step 3 — Access the host filesystem through the mount point:

ls /escape/

Output:

bin  boot  dev  escape  etc  home  host_secret.txt  lib  lib64
lost+found  media  mnt  opt  proc  root  run  sbin  srv  swap.img  sys  tmp  usr  var

The entire host filesystem is now visible inside the jail.

Step 4 — Read the secret:

cat /escape/host_secret.txt

Output:

FLAG{CHROOT_IS_NOT_A_SECURE_SANDBOX_1206}

The secret was successfully stolen from inside the jail.

Why the escape works

chroot only changes path resolution — it does not create any other form of isolation. It does not restrict:

  • Filesystem mounts — a root process can still call mount() to attach any filesystem anywhere
  • Process namespaces — the jailed process shares the same PID namespace as the host
  • Capabilities — a process that enters the jail as root keeps all root capabilities

The mount --bind trick works because the jailed process retains the CAP_SYS_ADMIN capability, which allows it to mount filesystems. By binding the host root to a directory inside the jail, the process creates a path that the kernel resolves normally — bypassing the chroot path restriction entirely.


Key Takeaways

  • chroot changes what a process sees as /, but does not restrict capabilities, mounts, or system calls. It is path isolation only.
  • A statically compiled binary (BusyBox) can run in a minimal jail with no libraries. A dynamically linked binary needs its library dependencies copied into the jail too.
  • A root process inside a chroot jail can escape by mounting the real host root into a directory inside the jail and then reading files through that path.
  • Modern containers (Docker, LXC) use a combination of namespaces, cgroups, and seccomp filters — not just chroot — to achieve real isolation. chroot alone is not a security boundary.
  • The rule: never give an untrusted user root access inside a chroot jail. The moment they have root, the jail offers no real protection.

What Was Difficult

Three things took extra troubleshooting during this lab:

  1. BusyBox location — the homework says to copy from /bin/busybox-static, but on this Ubuntu version the binary is installed at /usr/bin/busybox. The fix was to use which busybox to find the actual path before copying.

  2. mount –bind from inside the jail — the first attempt was to run mount --bind / /escape from inside the jail. This mounted the jail’s own root back onto itself, not the host root. The fix was to exit the jail, run the mount command from the host (where / is the real root), and then re-enter the jail.

  3. Missing escape directory — even after mounting correctly from the host, the /escape directory did not exist inside the jail yet. The mount silently failed the first time because the target directory had not been created with mkdir -p ~/chroot_jail/escape before mounting.


AI Assistance

I used Claude AI (claude-sonnet-4-6) as a learning assistant throughout this session. I ran all commands myself on my Ubuntu VM (ff-cs-sedat-yalcin, 192.168.80.104) over SSH via WireGuard VPN. Key prompts I wrote: provided the homework instructions, ran each step in sequence, and observed all outputs directly in the terminal.

Claude helped me set up the chroot jail with BusyBox, understand why directory traversal fails inside the jail, add /bin and /lib to enable standard tools, troubleshoot the mount-based escape (including the correct order of steps), and read the host secret file from inside the jail. All outputs shown are real results from my VM.