Assignment Chef icon Assignment Chef
All English tutorials

Programming lesson

Set-UID Program Exploitation: A Hands-On Guide to Privilege Escalation in C

Learn how Set-UID programs work, how the system() function and PATH variable can be exploited, and how to prevent privilege escalation in C. Includes real-world attack scenarios and countermeasures.

Set-UID program exploitation privilege escalation C system() function vulnerability PATH hijacking attack CSCI 180 computer security Set-UID root shell capability leaking least privilege principle C security tutorial setuid() system call buffer overflow alternative Ubuntu Set-UID countermeasure zsh privilege escalation game server privilege escalation AI security vulnerability finance app privilege management

Introduction to Set-UID Programs and Security

In computer security, Set-UID (Set User ID) programs are a powerful mechanism that allow users to run an executable with the file owner's privileges, often root. While useful for tasks like password changes, they also introduce serious risks if not carefully implemented. This tutorial explores the classic Set-UID vulnerability using C programming, focusing on the system() function, PATH manipulation, and privilege escalation. We'll also discuss the principle of least privilege and capability leaking. By understanding these concepts, you'll be better prepared to secure your own programs—whether you're building a game server, a finance app, or an AI tool that requires elevated permissions.

Task 1: Understanding the system() Function and PATH

The system() function in C executes a command by invoking /bin/sh -c command. Unlike execve(), which directly runs an executable, system() relies on the shell and the PATH environment variable to locate the command. If the command doesn't contain a slash, the shell searches the directories listed in PATH in order.

Writing systemtest.c

#include <stdlib.h>

int main() {
    system("ls");
    return 0;
}

Compile and run: gcc systemtest.c -o systemtest. It will execute the standard /bin/ls. Now, create your own ls.c that prints a message instead of listing files.

#include <stdio.h>

int main() {
    printf("Custom ls executed!\n");
    return 0;
}

Compile: gcc ls.c -o ls. To make systemtest use your ls, prepend the current directory to PATH: export PATH=.:$PATH. Now run systemtest again. You'll see "Custom ls executed!" This demonstrates how an attacker can hijack a program by placing a malicious executable earlier in the PATH.

Q1 Part A and B Observations

Initially, systemtest runs /bin/ls. After changing PATH, it runs your custom ls. This is a classic attack vector: if a Set-UID program uses system() and the user can control PATH, they can execute arbitrary code with elevated privileges.

Task 2: Creating a Set-UID Program

Let's make systemtest a Set-UID root program. As root:

sudo chown root systemtest
sudo chmod 4755 systemtest

Now, any user running systemtest will have root privileges. Modify ls.c to print real and effective user IDs:

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("Real UID: %d\n", getuid());
    printf("Effective UID: %d\n", geteuid());
    return 0;
}

Compile and run systemtest as a normal user. You might see both UIDs as the user's ID (e.g., 1000), not root. Why? Because /bin/sh often has a countermeasure: if it detects it's running in a Set-UID process, it drops privileges by setting effective UID to real UID. In Ubuntu, /bin/sh is typically a symlink to dash, which has this protection.

Bypassing the Countermeasure

To see the attack work, link /bin/sh to a shell without this protection, like zsh:

sudo rm /bin/sh
sudo ln -s /bin/zsh /bin/sh

Now re-run systemtest. You'll see Real UID=1000 (your user) and Effective UID=0 (root). This confirms that the custom ls runs with root privileges, allowing an attacker to execute arbitrary commands.

Task 3: Real Attack – Spawning a Root Shell

Instead of a benign ls, an attacker would create a malicious ls that spawns a shell. For example, copy /bin/zsh to a file named ls in the current directory:

cp /bin/zsh ./ls

Now, when systemtest runs ls, it executes the shell. Since the process has root privileges, you'll get a root shell (prompt ending with #). This is a full privilege escalation.

Steps to Exploit

  1. Create a shell executable named ls in a directory you control.
  2. Set PATH to include that directory first: export PATH=.:$PATH.
  3. Run the vulnerable Set-UID program (systemtest).
  4. Enjoy a root shell.

This attack works because system() invokes the shell, which searches PATH for the command. The Set-UID program's elevated privileges are inherited by the spawned shell.

Task 4: Capability Leaking and Least Privilege

The principle of least privilege states that a program should drop unnecessary privileges as soon as possible. The setuid() system call can revoke privileges: if a root process calls setuid(n), all UIDs become n. However, a common mistake is capability leaking—the program may have opened privileged resources (e.g., a root-owned file) before dropping privileges, and the file descriptor remains accessible.

Example: Vulnerable Program

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("/etc/readonlyfile", O_RDWR);
    setuid(getuid()); // drop privileges
    // write to file using fd
    write(fd, "hello", 5);
    close(fd);
    return 0;
}

Compile, set as Set-UID root, and create /etc/readonlyfile with permissions 644 (read-only for others). Run as a normal user. Even though the program dropped privileges, the file descriptor fd was opened while the process still had root access. After setuid(), the process can still write to the file via the leaked descriptor. This is a capability leak.

Expected vs Actual Behavior

Expected: After dropping privileges, the process should not be able to modify a root-owned file. Actual: The write succeeds because the file descriptor retains the original privilege. To prevent this, close file descriptors before dropping privileges, or use fcntl() to set the close-on-exec flag.

Conclusion and Best Practices

Set-UID programs must be designed with security in mind. Avoid using system() with user-controlled input or PATH. Instead, use absolute paths or execve() with explicit arguments. Always drop privileges as soon as possible and clean up resources. This tutorial mirrors real-world vulnerabilities found in system utilities, game servers, and even AI frameworks that run with elevated privileges. By understanding these attacks, you can write more secure code.