OverTheWire Leviathan


Contents:



The following are (brief) notes/solutions to the OverTheWire Leviathan CTF.

It's mostly about extracting useful information from setuid binaries and, thus, a good introduction to reverse engineering.

0

Login to gameserver:

ssh -o PreferredAuthentications=password leviathan0@leviathan.labs.overthewire.org -p 2223

Password: leviathan0.

0->1

leviathan0@gibson:~$ ls -lha
total 24K
drwxr-xr-x  3 root       root       4.0K Sep 19 07:07 .
drwxr-xr-x 83 root       root       4.0K Sep 19 07:09 ..
drwxr-x---  2 leviathan1 leviathan0 4.0K Sep 19 07:07 .backup
-rw-r--r--  1 root       root        220 Mar 31  2024 .bash_logout
-rw-r--r--  1 root       root       3.7K Mar 31  2024 .bashrc
-rw-r--r--  1 root       root        807 Mar 31  2024 .profile
leviathan0@gibson:~$ cd .backup/
leviathan0@gibson:~/.backup$ ls
bookmarks.html
leviathan0@gibson:~/.backup$ less bookmarks.html 
leviathan0@gibson:~/.backup$ cat bookmarks.html | grep password
<DT><A HREF="http://leviathan.labs.overthewire.org/passwordus.html | This will be fixed later, the password for leviathan1 is 3QJ3TgzHDq" ADD_DATE="1155384634" LAST_CHARSET="ISO-8859-1" ID="rdf:#$2wIU71">password to leviathan1</A>
leviathan0@gibson:~/.backup$ 

1->2

leviathan1@gibson:~$ ls -lha                                        
total 36K                                                           
drwxr-xr-x  2 root       root       4.0K Sep 19 07:07 .
drwxr-xr-x 83 root       root       4.0K Sep 19 07:09 ..
-rw-r--r--  1 root       root        220 Mar 31  2024 .bash_logout
-rw-r--r--  1 root       root       3.7K Mar 31  2024 .bashrc
-r-sr-x---  1 leviathan2 leviathan1  15K Sep 19 07:07 check
-rw-r--r--  1 root       root        807 Mar 31  2024 .profile
leviathan1@gibson:~$ ./check                                        
password: abc                                                       
Wrong password, Good Bye ...

There's a binary with the suid-bit set and with owner leviathan2, i.e. the effective user ID of the process is leviathan2. The goal is to escalate our leviathan1 privileges to leviathan2 privileges, s.t. we can read the flag in /etc/leviathan_pass/leviathan2.

The password is probably stored somewhere inside the binary.

leviathan1@gibson:~$ strings check
...
strcmp
...
secr
love
password:
/bin/sh
...

I checked secr and love but none of them is the correct password.

However, cat gives a few more candidates: sex, god, love.

leviathan1@gibson:~$ cat check
...
eE1EsexEsecrEretEgodEloveE
...

As it turns out, any password with beginning sex seems to be correct:

leviathan1@gibson:~$ ./check
password: sex
$ whoami
leviathan2
$ cat /etc/leviathan_pass/leviathan2
NsN1HwFoyN
$ exit

This does make sense when we look at the disassembly: objdump -D -Mintel | less:

080491d6 <main>:
 ...
 80491f3:       c7 45 e0 73 65 78 00    mov    DWORD PTR [ebp-0x20],0x786573
 80491fa:       c7 45 ed 73 65 63 72    mov    DWORD PTR [ebp-0x13],0x72636573
 8049201:       c7 45 f0 72 65 74 00    mov    DWORD PTR [ebp-0x10],0x746572
 8049208:       c7 45 e4 67 6f 64 00    mov    DWORD PTR [ebp-0x1c],0x646f67
 804920f:       c7 45 e8 6c 6f 76 65    mov    DWORD PTR [ebp-0x18],0x65766f6c
 ...
 804922a:       e8 31 fe ff ff          call   8049060 <getchar@plt>
 804922f:       88 45 dc                mov    BYTE PTR [ebp-0x24],al
 8049232:       e8 29 fe ff ff          call   8049060 <getchar@plt>
 8049237:       88 45 dd                mov    BYTE PTR [ebp-0x23],al
 804923a:       e8 21 fe ff ff          call   8049060 <getchar@plt>
 804923f:       88 45 de                mov    BYTE PTR [ebp-0x22],al
 8049242:       c6 45 df 00             mov    BYTE PTR [ebp-0x21],0x0
 ...
 8049249:       8d 45 e0                lea    eax,[ebp-0x20]
 804924c:       50                      push   eax
 804924d:       8d 45 dc                lea    eax,[ebp-0x24]
 8049250:       50                      push   eax
 8049251:       e8 da fd ff ff          call   8049030 <strcmp@plt>
 8049256:       83 c4 10                add    esp,0x10
 8049259:       85 c0                   test   eax,eax
 ...
 804925b:       75 2b                   jne    8049288 <main+0xb2>
 ...
 804926e:       e8 3d fe ff ff          call   80490b0 <setreuid@plt>
 ...
 8049279:       68 13 a0 04 08          push   0x804a013
 804927e:       e8 1d fe ff ff          call   80490a0 <system@plt>
 ...
 8049288:       83 ec 0c                sub    esp,0xc
 804928b:       68 1b a0 04 08          push   0x804a01b
 8049290:       e8 fb fd ff ff          call   8049090 <puts@plt>
 ...
 80492b7:       c3                      ret
  • the DWORD PTRs are probably the strings we see in the cat output, where 0x786573 is the position of "sex"
  • then we read three characters from the password prompt and append a \0 byte
  • then we compare "sex" with what we've just read
  • if the strings match, the leviathan2 ID is set, and we spawn a shell via the system("/bin/sh") syscall; else we print the error via puts and return

An easier alternative for finding the correct password is to trace libary calls:

leviathan1@gibson:~$ ltrace ./check
__libc_start_main(0x80490ed, 1, 0xffffd3b4, 0 <unfinished ...>
printf("password: ")                                                                              = 10
getchar(0, 0, 0x786573, 0x646f67password: abc
)                                                                 = 97
getchar(0, 97, 0x786573, 0x646f67)                                                                = 98
getchar(0, 0x6261, 0x786573, 0x646f67)                                                            = 99
strcmp("abc", "sex")                                                                              = -1
puts("Wrong password, Good Bye ..."Wrong password, Good Bye ...
)                                                              = 29
+++ exited (status 0) +++

2->3

This level is quite tricky.

leviathan2@gibson:~$ ls -lh
total 16K
-r-sr-x--- 1 leviathan3 leviathan2 15K Sep 19 07:07 printfile
leviathan2@gibson:~$ ./printfile /etc/leviathan_pass/leviathan3
You cant have that file...

I guess that would've been too easy. Creating a symlink to the target also doesn't work:

leviathan2@gibson:~$ mktemp -d
/tmp/tmp.7dpdBN9bhh
leviathan2@gibson:~$ cd /tmp/tmp.7dpdBN9bhh
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ ln -s /etc/leviathan_pass/leviathan3 leviathan3
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ ~/printfile leviathan3 
You cant have that file...

I guess I have to see how exactly the permission check is performed. When I do a strace ~/printfile /etc/leviathan_pass/leviathan3 it shows:

...
access("/etc/leviathan_pass/leviathan3", R_OK) = -1 EACCES (Permission denied)
...

In the access manpage it says:

access() checks whether the calling process can access the file pathname.  If
pathname is a symbolic link, it is dereferenced.
...
The  check  is  done  using the calling process's real UID and GID, rather
than the effective IDs

That's the reason why simply creating a symlink to /etc/leviathan_pass/leviathan3 doesn't pass the check.

Let's see what happens when we pass a file that leviathan2 owns:

leviathan2@gibson:~$ mktemp -d
/tmp/tmp.7dpdBN9bhh
leviathan2@gibson:~$ cd /tmp/tmp.7dpdBN9bhh
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ touch foo
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ ltrace ~/printfile foo
__libc_start_main(0x80490ed, 2, 0xffffd354, 0 <unfinished ...>
access("foo", 4)                                                                    = 0
snprintf("/bin/cat foo", 511, "/bin/cat %s", "foo")                                 = 12
geteuid()                                                                           = 12002
geteuid()                                                                           = 12002
setreuid(12002, 12002)                                                              = 0
system("/bin/cat foo" <no return ...>
--- SIGCHLD (Child exited) ---
<... system resumed> )                                                              = 0
+++ exited (status 0) +++

At first, access is called on the argument, then /bin/cat.

You probably know that when you can pass multiple arguments to cat by separating them by whitespace. For example, cat foo bar is treated as cat foo; cat bar.

However, "foo bar" is treated as one file with whitespace in the name in case of access.

And that difference in behavior can be exploited here:

  • we create a file "foo leviathan3" that leviathan2 owns
  • then we create a symlink leviathan3 to /etc/leviathan_pass/leviathan3
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ touch "foo leviathan3"
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ ln -s /etc/leviathan_pass/leviathan3 leviathan3
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ ls -lh
total 0
-rw-rw-r-- 1 leviathan2 leviathan2  0 Jan  5 15:31 foo leviathan3
lrwxrwxrwx 1 leviathan2 leviathan2 30 Jan  5 15:31 leviathan3 -> /etc/leviathan_pass/leviathan3
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ 

The manpage also says:

A file is accessible only if the permissions on each of the directories in the
path prefix of pathname grant search (i.e., execute)  access.

which is why we need to

leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ chmod 777 .

Now, we call printfile on "foo leviathan3". We will pass the access("foo leviathan3") check because we've previously touched that file. Then, /bin/cat foo leviathan3 is executed under the leviathan3 user ID, which will print the password:

leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ ~/printfile "foo leviathan3"
/bin/cat: foo: No such file or directory
f0n8h2iWLP
leviathan2@gibson:/tmp/tmp.7dpdBN9bhh$ 

3->4

leviathan3@gibson:~$ ls 
level3
leviathan3@gibson:~$ ./level3 
Enter the password> abc
bzzzzzzzzap. WRONG
leviathan3@gibson:~$ ltrace ./level3 
__libc_start_main(0x80490ed, 1, 0xffffd3a4, 0 <unfinished ...>
strcmp("h0no33", "kakaka")                                                          = -1
printf("Enter the password> ")                                                      = 20
fgets(Enter the password> abc
"abc\n", 256, 0xf7fae5c0)                                                     = 0xffffd17c
strcmp("abc\n", "snlprintf\n")                                                      = -1
puts("bzzzzzzzzap. WRONG"bzzzzzzzzap. WRONG
)                                                          = 19
+++ exited (status 0) +++

We can see that our input is compared to "snlprintf\n". That's the password!

leviathan3@gibson:~$ ./level3 
Enter the password> snlprintf
[You've got shell]!
$ whoami
leviathan4
$ cat /etc/leviathan_pass/leviathan4
WG1egElCvO
$ exit
leviathan3@gibson:~$ 

4->5

leviathan4@gibson:~$ ls -a                                          
.  ..  .bash_logout  .bashrc  .profile  .trash                      
leviathan4@gibson:~$ cd .trash/                                     
leviathan4@gibson:~/.trash$ ls
bin
leviathan4@gibson:~/.trash$ ./bin                                   
00110000 01100100 01111001 01111000 01010100 00110111 01000110 00110100 01010001 01000100 00001010
leviathan4@gibson:~/.trash$ python3
Python 3.12.3 (main, Sep 11 2024, 14:17:37) [GCC 13.2.0] on linux   
Type "help", "copyright", "credits" or "license" for more information.
>>> flag_bin = "00110000 01100100 01111001 01111000 01010100 00110111 01000110 00110100 01010001 01000100 00001010"
>>> for byte in flag_bin.split():
...     print(chr(int(byte, 2)))
...                                                                 
0
d
y
x
T
7
F
4
Q
D


>>>

The flag is 0dyxT7F4QD.

5->6

leviathan5@gibson:~$ ls
leviathan5
leviathan5@gibson:~$ ./leviathan5 
Cannot find /tmp/file.log
leviathan5@gibson:~$ touch /tmp/file.log
leviathan5@gibson:~$ ltrace ./leviathan5 
__libc_start_main(0x804910d, 1, 0xffffd3a4, 0 <unfinished ...>
fopen("/tmp/file.log", "r")                                                                       = 0x804d1a0
fgetc(0x804d1a0)                                                                                  = '\377'
feof(0x804d1a0)                                                                                   = 1
fclose(0x804d1a0)                                                                                 = 0
getuid()                                                                                          = 12005
setuid(12005)                                                                                     = 0
unlink("/tmp/file.log")                                                                           = 0
+++ exited (status 0) +++

It seems like the /tmp/file.log is read and then written to the console:

leviathan5@gibson:~$ echo "foo" >/tmp/file.log
leviathan5@gibson:~$ ./leviathan5 
foo

We can use that to read the next flag:

leviathan5@gibson:~$ ln -s /etc/leviathan_pass/leviathan6 /tmp/file.log
leviathan5@gibson:~$ ./leviathan5                                               
szo7HDB88w

6->7

leviathan6@gibson:~$ ls
leviathan6
leviathan6@gibson:~$ ./leviathan6 
usage: ./leviathan6 <4 digit code>
leviathan6@gibson:~$ ./leviathan6 1234
Wrong

We can easily brute-force the correct code:

leviathan6@gibson:~$ for guess in {0000..9999}; do echo $guess; out=$(./leviathan6 $guess); if [[ $out != *"Wrong"* ]]; then break; fi; done
0000
...
7123
^C

Alternatively, you can check the disassembly (objdump -D -Mintel leviathan6):

080491c6 <main>:
 ...
 80491da:       c7 45 f4 d3 1b 00 00    mov    DWORD PTR [ebp-0xc],0x1bd3
 ...
 8049212:       e8 89 fe ff ff          call   80490a0 <atoi@plt>
 8049217:       83 c4 10                add    esp,0x10
 804921a:       39 45 f4                cmp    DWORD PTR [ebp-0xc],eax
 ...

0x1bd3 is decimal 7123.

Then use it to get the shell:

leviathan6@gibson:~$ ./leviathan6 7123
$ whoami
leviathan7
$ cat /etc/leviathan_pass/leviathan8
cat: /etc/leviathan_pass/leviathan8: No such file or directory
$ cat /etc/leviathan_pass/leviathan7
qEs5Io5yM8
$ exit

7

leviathan7@gibson:~$ ls
CONGRATULATIONS
leviathan7@gibson:~$ cat CONGRATULATIONS 
Well Done, you seem to have used a *nix system before, now try something more serious.
(Please don't post writeups, solutions or spoilers about the games on the web. Thank you!)

What I've Learned

  • ltrace, strace, etc. are really useful for peeking into binaries without using gdb
  • it can be dangerous if a program uses a command to check user-controlled input (e.g. access), and another command then handles that input (e.g. cat), but the commands interpret the input (slightly) different

Previous

(Auto-Adjust Key Repeat Speed and Delay on Linux Using udevd)

(Patching a Binary)