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.
Login to gameserver:
ssh -o PreferredAuthentications=password leviathan0@leviathan.labs.overthewire.org -p 2223
Password: leviathan0.
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$
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
DWORD PTRs are probably the strings we see in the cat output, where
0x786573 is the position of "sex"\0 byte"sex" with what we've just readleviathan2 ID is set, and we spawn a shell via the
system("/bin/sh") syscall; else we print the error via puts and returnAn 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) +++
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:
"foo leviathan3" that leviathan2 ownsleviathan3 to /etc/leviathan_pass/leviathan3leviathan2@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$
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:~$
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.
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
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
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!)
ltrace, strace, etc. are really useful for peeking into binaries without
using gdbaccess), and another command then handles that input (e.g. cat), but
the commands interpret the input (slightly) different