Leviathan — OverTheWire Wargame — Writeup

hrbrtschmu1l
8 min readFeb 19, 2022

--

Leviathan is a wargame offered by OverTheWire. It ‘doesn’t require any knowledge about programming — just a bit of common sense and some knowledge about basic *nix commands’. I enjoyed this wargame, but did find it a little odd — there is only one level that is significantly more difficult than the others, and many of the levels use very similar tricks.

I am aware that the game asks players to refrain from sharing writeups. However, given the large number of writeups already on the web, I am sharing this because I feel that I explain some sections (particularly level 2 → 3, the ‘hard’ level) in a way that is slightly easier to understand than I have seen in other writeups.

Please give the game a good go yourself before reading the writeup.

Leviathan — dare you face the lord of the oceans?

Without further delay, let’s begin!

We begin by SSHing into the first level, using the provided username and password, ‘leviathan0’. While SSH is typically found running on port 22, the creator has used port 2223 in this instance, so we use the -pflag to specify this:

ssh leviathan0@leviathan.labs.overthewire.org -p 2223

We’re in. Let’s begin by listing all contents of the current directory (/home/leviathan0, as shown by the output of the pwd command), as well as getting information about permissions with the ls -la command :

Finding out which directory we are in (pwd) and looking at what’s in the directory (ls -la)

The only nonstandard thing here is the hidden directory ‘backup’. We can tell that it is a hidden directory because of the preceding full stop/period (.). If you simply used the ls command without the -aflag, the output would not contain this directory. In fact, it would look like the directory is completely empty since all of the files are hidden.

Finding directories and files that are related to backups have the potential to have ‘juicy’ data in, such as usernames or passwords. Let’s go and see what is in there with the cd command:

Exploring the hidden directory called ‘backup’

Using the cat command to print the contents of ‘bookmarks.html’ shows us a file with 1399 lines of text. Since we don’t want to go through this file manually, let’s grepthe file and search for a word like ‘password’:

The ‘-i’ flag tells grep to perform a case-insensitive search

A line is returned with the password for leviathan1. With that, let’s SSH into leviathan1:

ssh leviathan1@leviathan.labs.overthewire.org -p 2223

Once logged in, let’s look at directory contents, similar to how we did for leviathan0:

Contents of leviathan1’s home directory

We can execute the ‘check’ file. Let’s do that…

Running ‘check’, being asked for a password and trying ‘password_attempt’

It appears that we need a password. If we run strings on ‘check’, then we see some well-known C functions, such as printf and strcmp. Given that strcmp is used to compare two strings, this may be how the password is being checked.

Snippet of the output after running ‘strings check’ — notice the C functions

strcmp is in a shared library. This means that we can ltrace (library trace) to see what functions are being called by the program, and gather additional information about what they’re being called to do:

The important output from ‘ltrace ./check’

We can see that strcmp is being called to compare the first three characters of our password attempt. Indeed, our attempt, effectively ‘pas’, is being compared to ‘sex’. Let’s try ‘sex’ as the password…

We get dropped into a shell with the user ID of leviathan2, allowing us to read leviathan2’s password

Leviathan2 is, in my opinion, the hardest level (and it’s not even close). However, it is also the most interesting. Let’s get started!

As usual, listing the contents of the directory and running the executable (printfile) to see its behaviour

Running the executable, printfile, in our home directory, we are told that we need to provide the program with a file to print, suggesting that the program may have similar functionality to cat. Let’s create a directory, ‘hs’, in the world-writeable ‘/tmp’ folder and then make a simple text file in that directory. We’ll then run printfile with the path to that file (we’ll call it foo.txt, as is standard in the literature) as the argument:

As expected, printfile does appear to be similar to ‘cat’

The obvious thing to try is using printfile to read the password for leviathan3:

Why does this not work? Intuitively, it must be an issue with our permissions, but why? Let’s look at the line of output detailing the permissions and owner of the printfile executable:

-r-sr-x---  1 leviathan3 leviathan2 7436 Aug 26  2019 printfile

Note the ‘s’, rather than the typical ‘x’. This means that the program takes the ‘effective user ID’ to be the owner’s ID (in this case, leviathan3). This kind of file/program is commonly called a ‘SUID’ file — read more about them here.

So, why is cat not allowing us to print the contents of /etc/leviathan_pass/leviathan3, if we are effectively leviathan3? Let’s look at the output from running ltrace on printfile, on a file that we weren’t authorised to view (/etc/leviathan_pass/leviathan3) and one that we could view (/tmp/hs/foo.txt) to see how they differ:

Output of ltrace with files that we can and cannot read with printfile

We notice that the first thing that the program does is call a function called access(). Looking at an online man page for this function, we get some interesting information:

access() man page

It is interesting how access() uses the real user ID, rather than the effective one. When we (leviathan2) run printfile, our effective user ID is leviathan3 (due to the ‘sticky bit’, represented by the ‘x’ as previously discussed), but our real user ID is still leviathan2. Since leviathan2 does not have permission to read /etc/leviathan_pass/leviathan3, the access check fails and will return a value of -1, before the program terminates. You can imagine that pseudocode for this check would look something like:

file_to_check = arg[1]
if access(file_to_check): # if access() returns 0, continue
continue
else: # if access() returns anything other than 0, exit
exit

Refer back to the above screenshot of the ltraces. We do see /bin/cat in there. cat, like most programs, will use the effective user ID (leviathan3) when trying to read files. This means that if we can somehow bypass the access() check and then execute cat /etc/leviathan_pass_leviathan3 later in the program, we should be able to read the password.

So, how can we bypass access() and then read leviathan3’s password? The trick lies in knowing that the cat function, if passed multiple arguments, separated by spaces, will print each file’s contents, one after another (i.e., concatenating them). However, the access() function does not care about spaces — it treats whatever you pass it as the singular file to be checked. Essentially, this exploit comes down to the fact that access() will only be used to check the permissions of one file, however cat can be used to print the contents of multiple files.

We make a file in /tmp/hs called ‘foo.txt bar.txt’. Since we, leviathan2, have created this, if we pass this file to access(), it will be successful. After that, cat will attempt to print the contents of ‘foo.txt’, followed by ‘bar.txt’. Right now, we don’t have a bar.txt file. We can create one, but we don’t want a random text file with content like ‘bar’ — we want it to print the password for leviathan3. This is why we use a symbolic link to /etc/leviathan_pass/leviathan3. cat will try to print the password, and since cat only considers effective user ID, we should be able to view the password!

Done!

Given the relative difficulty of the last level, I was very surprised by how easy level 3 → 4 is. We look at what’s in our home directory and run the executable. We’re prompted for a password, similar to how we were in level 1. We run ltrace again and find that strcmp is being used to compare our attempt and ‘snlprintf’. Simply using that as the password works…

An easier road to level 4

This next level is also fairly easy. We repeat the usual steps, noticing the hidden ‘trash’ directory. In here, there’s an executable called bin. Running that gives us a list of numbers, quite clearly in binary. Converting these numbers to ASCII can be done in whichever way you prefer and will give you the password for leviathan6. It’s probably easiest to use an online converter, but you can write a short script like so (I used Python in the hackiest manner possible…):

nums = open('nums.txt','r').read().split()
for i in range(len(nums)):
nums[i] = chr(int(nums[i],2))
password = ""
print(password.join(nums))

We’re now onto leviathan6. Again, a strange level, simply because it seems like an easier version of 2 → 3…

Getting the password for leviathan6 using a symbolic link

It’s not even necessary to use ltrace here. We observe the behaviour of the leviathan5 executable and try to make /tmp/file.log a symbolic link to the leviathan5 password file. This works without any issues.

We’re now at the last level! We’ll repeat the standard steps:

The fact that there are only 10000 possibilities for this code mean that this level is begging to be brute forced (although note that the ‘sleep’ is necessary in the code shown below, otherwise the resource becomes unavailable). Bash scripting is an easy way of doing this:

for i in {0..10000}; do echo "trying $i"; ./leviathan6 $i | grep -v "Wrong"; sleep 0.005; done
The script stops at 7123, which is the passcode. Using it drops us into a shell as leviathan7
Leviathan? Completed it mate…

I hope you enjoyed this writeup!

--

--