Đọc báo cáo ATTT mới nhất tại đây

VCI
RED TEAM

Tản mạn về SQL Injection — những case thực tế từ quá trình pentest

SQL Injection (SQLi) không phải lỗ hổng mới. Nó xuất hiện trong top OWASP từ nhiều năm nay, được viết trong mọi giáo trình bảo mật, và gần như developer nào cũng từng nghe qua. Vậy mà trên thực tế, lỗ ...

14-07-2026 15:01 GMT Thời gian đọc: 5 phútRED TEAM
Tản mạn về SQL Injection — những case thực tế từ quá trình pentest

SQL Injection (SQLi) không phải lỗ hổng mới. Nó xuất hiện trong top OWASP từ nhiều năm nay, được viết trong mọi giáo trình bảo mật, và gần như developer nào cũng từng nghe qua. Vậy mà trên thực tế, lỗ hổng này vẫn còn tồn tại — không phải vì developer không biết, mà vì nó ẩn mình rất khéo.

Bài viết này không phải lý thuyết. Đây là tổng hợp những case SQLi mình thực sự gặp trong quá trình pentest, bao gồm cả những lần bế tắc, những lần ngồi debug cùng dev để tìm ra nguyên nhân thật sự. Hy vọng qua đây, cả developer lẫn security researcher đều có thêm góc nhìn thực tế hơn về loại lỗ hổng tưởng chừng đã "lỗi thời" này.

Ứng dụng web A: Second-Order SQL Injection khó phát hiện

Sau khi đọc 1 lượt code project để tìm các vị trí nối chuỗi câu truy vấn SQL, tìm được nhiều chỗ nhưng không có api nào nhận trực tiếp giá trị từ request người dùng, tuy nhiên mình vẫn phải dành thời gian để test second-order SQL injection, tốn kha khá thời gian vì từ source đến sink qua 5–6 function. Kết quả không khai thác được gì, còn duy nhất 1 chỗ nhìn có vẻ khả quan hơn.

Biến cols là column được select, biến này không lấy trực tiếp từ request, cần phải update thông qua api khác.

Sau khi chèn ký tự nháy ' và 2 dấu nháy đơn thì mình xác nhận có thể khai thác được, dựa vào response trả về:

{"code":400,"message":"Failed to execute query","success":false}

Đến đây tưởng đã xong nhưng không, khi chèn payload trigger dbms_sleep thì báo lỗi server. Ban đầu mình nghĩ do payload sai, ngồi test thêm mình nhận thấy độ dài của cột giới hạn 20 ký tự. Mình tạm dừng ở đây và qua test các lỗi khác.

3 ngày sau mình quay lại pentest lỗi này tiếp, đọc lại từng dòng code một. Lúc này mình mới nghĩ ra cách chia nhỏ payload ra nhiều phần, trừ ký tự comment /**/ thì payload còn lại ít hơn 16 ký tự. Trigger sleep cũng không khả thi, payload chỉ còn select user from dual.

Đến đây kết quả không trả về như mong muốn, mình check kỹ không thấy sai ở đâu.

Stop lần 2.

Ngày cuối đóng task, mình mới nhắn tin cho dev nhờ debug đoạn code này. Sau một hồi debug thấy code chạy đúng, câu truy vấn mình inject cũng đúng. Sau đó mình nhờ bổ sung data test thì mới khai thác thành công. Và nguyên nhân là không có data nên không có response.

Đối với payload SELECT banner FROM v$version dài hơn, cần phải chia nhỏ ra làm 3 phần. Khi join thì sẽ join ngẫu nhiên thứ tự cột nên cần phải test các trường hợp để ra câu truy vấn hợp lệ

Ứng dụng web B: Khi đoạn code không giống SQL Injection nhưng vẫn có thể bị khai thác

Đoạn code này nhìn qua thì không phải nối chuỗi, không phải kiểu:

'select username from users where id = ''' + req.id + '''

Tuy nhiên khi chèn ký tự ', response báo lỗi cú pháp câu truy vấn.

Task này còn 2 vấn đề ở SQL injection filter:

  • Vấn đề 1 - Check regex nếu là HTML thì bỏ qua check SQL injection. Đây là lỗ hổng thiết kế ngay trong chính lớp bảo vệ: attacker chỉ cần wrap payload trong HTML tag là bypass toàn bộ filter

  • Vấn đề 2 - Dev hỏi AI, AI nói không cần filter ký tự '. Dev làm theo thật vì không hiểu bản chất vấn đề. Kết quả là đoạn code filter dù có dài 50 dòng phía sau cũng vô nghĩa — cái quan trọng nhất đã bị loại bỏ từ đầu.

Ứng dụng web C: Lỗ hổng còn sót lại sau nhiều lần pentest
 

Ứng dụng đã qua nhiều lần pentest và đã bổ sung nhiều SQL injection filter, tưởng như đã yên tâm, an toàn — nhưng còn sót 1 api thực hiện câu truy vấn từ request parameter.

Ở endpoint này có thể chèn câu truy vấn xóa bảng, xóa data trong database — mức độ ảnh hưởng rất lớn.

Bài học ở đây: pentest theo từng đợt không đảm bảo coverage 100%. Mỗi lần release tính năng mới đều có thể kéo theo attack surface mới.

Ứng dụng web D: Rủi ro từ việc tự xây dựng cơ chế truy vấn SQL

Developer thực hiện viết lại các hàm, statement (SELECT, INSERT, UPDATE,...), operator (AND, OR, NOT), clause (WHERE),... của Oracle và cộng chuỗi từ giá trị người dùng nhập vào — nên khi 1 hàm viết lại này bị khai thác, attacker có thể khai thác qua toàn bộ API gọi đến nó.

Đây là anti-pattern kinh điển: build lại từ đầu thay vì dùng ORM hoặc parameterized query có sẵn. Càng phức tạp, càng nhiều chỗ để sai, và càng khó audit.

Mình còn gặp nhiều trường hợp khai thác SQL injection khác, vì lý do bảo mật nên mình không trình bày thêm ở đây.

Từ SQL Injection, attacker có thể làm gì?

  • Truy xuất trái phép dữ liệu trong cơ sở dữ liệu (và đem đi bán đối với blackhat hacker 😄)

  • Chèn account để đăng nhập ứng dụng

  • Inject hash password account admin để thực hiện Account Takeover (ATO)

  • Inject file path để đọc file bất kỳ trên server — đối với api download file theo file_id

  • Inject payload để thực hiện Remote Code Execution (RCE)

Cơ bản nhất là dump data, nhưng tuỳ vào cấu hình server và đặc điểm từng ứng dụng, SQLi có thể leo thang thành RCE — mức nguy hiểm cao nhất.

Nhìn lại 4 ứng dụng trên, có một điểm chung: lỗi không đến từ việc không biết SQL Injection là gì, mà đến từ những góc khuất nhỏ — một biến được update gián tiếp, một lớp filter bị bypass bằng HTML, một lời khuyên sai từ AI, hay một API bị bỏ sót sau nhiều lần pentest.

Một vài điểm rút ra:

Với developer:

  • Không dùng string concatenation để build câu SQL — dùng parameterized query hoặc ORM đúng cách.

  • Filter input không phải giải pháp thay thế cho parameterized query. Filter có thể bị bypass; parameterized query thì không.

  • Đừng hoàn toàn tin AI khi hỏi về bảo mật — AI có thể đúng về syntax, nhưng không phải lúc nào cũng hiểu đúng bản chất vấn đề bảo mật.

  • Second-order injection thường bị bỏ qua vì tester chỉ nhìn vào input trực tiếp. Hãy trace toàn bộ data flow.

Với security researcher:

  • Đừng bỏ cuộc sớm khi không thấy response rõ ràng — đôi khi vấn đề chỉ đơn giản là không có data test.

  • Các giới hạn như độ dài cột hay filter ký tự đặc biệt không phải là bức tường — hãy tìm cách split payload hoặc bypass bằng encoding.

  • Khi gặp bế tắc, hỏi dev là hợp lệ. Pentest không phải cuộc thi một mình; phối hợp với team dev giúp tìm ra root cause nhanh hơn nhiều.

Thời điểm hiện tại, developer sử dụng AI nhiều hơn trong quá trình code nên trong tương lai, các lỗi SQLi kiểu nối chuỗi thô sẽ ít gặp hơn. Nhưng những lỗi phức tạp hơn — second-order injection, bypass filter không đúng cách, hay logic lỗi trong custom ORM — thì vẫn sẽ còn đó. Và đó là lý do pentest vẫn cần con người.

May đo bảo mật theo
quy mô & nhu cầu của Tổ chức

Tìm kiếm đơn vị Bảo vệ An ninh mạng cho tổ chức của bạn?