Lab: Path Traversal Attack

This lab will show you a common security flaw called “Path Traversal.” We’ll start by exploiting a vulnerable program (readmail) to read another user’s private mail. Then, we’ll examine a secure version (readmail-safe) to understand how to fix this vulnerability using the “Principle of Least Privilege.”

Setup

open in GitHub Codespaces.

  1. Go to the path-traversal-attack directory.
  2. Initialize the Environment: The setup.sh script creates two users (user1, user2) and sets up their password and mail files for our experiment. It also compiles the readmail and readmail-safe executables and install them to /usr/local/bin and setuid on them.
# Run the setup script with root privileges
cd path-traversal-attack
sudo bash setup.sh

Experiment 1: Path Traversal

The readmail program is a “SUID root” executable. This means that even though we’ll run it as a regular user, the operating system will grant it the permissions of its owner, which is root. The program needs this temporary power to read the system-wide password file for authentication.

You can verify the SUID executable readmail with ls -l:

$ ls -l $(which readmail)
-rwsr-xr-x 1 root root 84224 Sep 16 08:31 /usr/local/bin/readmail

This file is owned by the root user. The s in the permission string is the SUID bit. It tells the kernel: “When anyone executes this file, run the process as the file’s owner (root), not as the person who typed the command.”

Normal Usage

We’ll run the program to read mail1 for user1. Since setup.sh installed it in /usr/local/bin, (try: echo $PATH and see if /usr/local/bin is in it), you can call it directly without typing the full path: /usr/local/bin/readmail.

# We're running as the regular 'vscode' user, with no 'sudo'
$ readmail user1 mail1
Password: pass1
# Expected output: The content of user1's mail1.

The readmail program runs with the permissions of its owner (root), not the user who executes it, in order to read the system password file.

First, let’s see how the program is supposed to work. We’ll run it to read mail1 for user1.

# The first argument is the username, the second is the mail file.
$ readmail user1 mail1
Password: pass1
# Expected output: The content of user1's mail1.

Malicious usage

Now, let’s try something malicious. We will authenticate as user1, but ask the program to read a file at the path ../user2/mail1. The .. tells the system to go up one directory level.

$ readmail user1 ../user2/mail1
Password: pass1
# Success! We can now read the contents of user2's private mail...

Why did the attack succeed? The program made a critical mistake:

  1. It starts running with an Effective User ID (EUID) of root.
  2. It correctly checks user1’s password.
  3. The Flaw: After authentication, the program never drops its root privileges.
  4. It blindly takes the user’s input (../user2/mail1) and tries to open that file.
  5. When the program asks the OS to open the file, the OS sees that the request is coming from root. Since root can read anything, the OS allows it.

Experiment 2: The Principle of Least Privilege

Now, let’s test readmail-safe, the fixed version of the program.

Attempt the Same Attack: We’ll repeat the exact same attack, this time targeting readmail-safe.

$ readmail-safe user1 ../user2/mail1
Password: pass1
# Expected output: "Permission denied" or a similar error. The attack fails!

How It Works

The attack failed because readmail-safe follows the Principle of Least Privilege. Here’s its improved logic:

  1. It starts running with an EUID of root.
  2. It correctly checks user1’s password.
  3. The Fix: Immediately after authentication, it uses the seteuid() system call to drop its privileges, changing its EUID from root to the authenticated user (user1).
  4. Now, when the program tries to open ../user2/mail1, it’s no longer acting as root. It’s acting as user1.
  5. The operating system sees that user1 is trying to access user2’s private files and correctly denies permission.

This principle is simple but powerful: only use elevated privileges for the shortest time necessary, and reduce them as soon as the high-privilege task is done.

Food for Thought

  1. Besides dropping privileges, what are some other ways to prevent path traversal attack?
  2. In our safe version, we used seteuid() to drop privileges. What’s the difference between seteuid() and setuid() in Linux?
  3. What are some other malicious paths you can think of to feed into readmail to exploit it?

Bonus

Let’s explore PTT’s main daemon program: mbbsd.c and check if it’s secure.
PTT daemon starts as root (so it can run privileged operations like opening network ports or creating shared memory, etc.). Then, before accepting any real user sessions, it call setuid(BBSUID) to drop to the unprivileged bbs user. Then, every new logged-in user runs in separate process but all with the same UID. Unlike our readmail-safe, PTT does not rely on the OS to ensure one user cannot read another user’s private file. PTT uses its built-in application enforcement. Similar to us, user files are also constructed using snprintf in common/bbs/path.c, but it is validated with is_validuserid in common/bbs/names.c.

In the following code, userid is validated everytime PTT constructs the filepath.

void sethomepath(char *buf, const char *userid) {
    assert(is_validuserid(userid));
    snprintf(buf, PATHLEN, "home/%c/%s", userid[0], userid);
}

Even if the attacker finds a vulnerability that leads to memory corruption bug (e.g. buffer overflow) and can overwrite userid into something like ../../other_user/other_user, the path will be rejected by the assert(is_validuserid(userid));. It is quite difficult to construct /home/bbs/home/my_user/my_user/../../other_user/other_user (attacker tries to read /home/bbs/home/other_user/other_user) without being caught.

Back to top