Làm thế nào để tiếp cận hiệu quả Pentest trong kiểm thử bảo mật ứng dụng Web (Phần II)
Bài đăng này đã không được cập nhật trong 7 năm
Nhận dạng Cơ Sở Dữ Liệu
Mặc dù ngôn ngữ SQL theo chuẩn nhưng mỗi CSDL đều có đặc điểm riêng và khác nhau ở nhiều khía cạnh như các câu lệnh đặc biệt hay các chức năng để truy xuất dữ liệu như tên người dùng và các cơ sở dữ liệu, các đặc tính riêng, hay các dòng comment, ... Khi các tester chuyển sang tìm hiểu SQL Injection nâng cao hơn, họ cần biết cơ sở dữ liệu mặt backend là gì.
- Cách đầu tiên để biết cơ sở dữ liệu backend nào đang được dùng chính là quan sát lỗi mà ứng dụng trả về. Sau đây là một số ví dụ về các nội dung báo lỗi trả về:
MySql:
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
Một câu truy vấn UNION SELECT đầy đủ với version()
cũng có thể giúp ta biết được cơ sở dữ liệu mặt backend là gì.
SELECT id, name FROM users WHERE id=1 UNION SELECT 1, version() limit 1,1
Oracle:
ORA-00933: SQL command not properly ended
MS SQL Server:
Microsoft SQL Native Client error ‘80040e14’ Unclosed quotation mark after the character string SELECT id, name FROM users WHERE id=1 UNION SELECT 1, @@version limit 1, 1
PostgreSQL:
Query failed: ERROR: syntax error at or near "’" at character 56 in /www/site/test.php on line 121.
- Nếu không có nội dung báo lỗi nào, tester có thể cố chèn vào một số trường string bằng cách sử dụng các kỹ thuật ghép chuỗi khác nhau:
MySql: ‘test’ + ‘ing’
SQL Server: ‘test’ ‘ing’
Oracle: ‘test’||’ing’
PostgreSQL: ‘test’||’ing’
Các kỹ thuật khai thác SQL Injection
Kỹ thuật khai thác Union
Toán tử UNION được dùng trong các SQL injection để nối một truy vấn (do tester cố tình giả mạo) với truy vấn ban đầu. Kết quả của truy vấn giả mạo này sẽ được nối với kết quả của truy vấn ban đầu, điều này cho phép tester có được các giá trị của các cột trong bảng khác. Chẳng hạn như truy vấn sau sẽ được thực thi từ server:
SELECT Name, Phone, Address FROM Users WHERE Id=$id
Chúng ta sẽ thiết lập giá trị $id dưới đây:
$id=1 UNION ALL SELECT creditCardNumber,1,1 FROM CreditCardTable
Chúng ta sẽ có truy vấn sau:
SELECT Name, Phone, Address FROM Users WHERE Id=1 UNION ALL SELECT creditCardNumber,1,1 FROM CreditCardTable
Truy vấn này sẽ nối kết quả của truy vấn ban đầu với tất cả số thông tin thẻ tín dụng trong bảng CreditCardTable. Từ khóa ALL là cần thiết để tránh các truy vấn sử dụng từ khóa DISTINCT. Ngoài ra, chúng ta cần lưu ý rằng ngoài các thông tin về số thẻ tín dụng, chúng ta còn chọn 2 giá trị khác nữa. Hai giá trị này là cần thiết vì 2 truy vấn đó phải có số lượng bằng nhau về các tham số/cột để tránh lỗi cú pháp.
Chi tiết đầu tiên mà một tester cần khai thác lỗ hỏng về SQL injection bằng cách sử dụng kỹ thuật này chính là tìm đúng số lượng các cột trong câu SELECT.
Để làm điều này, tester có thể sử dụng mệnh đề ORDER BY mà theo sau đó là một số cho biết số của cột cơ sở dữ liệu được chọn:
http://www.example.com/product.php?id=10 ORDER BY 10--
Nếu truy vấn đó thực hiện thành công theo suy đoán của tester thì trong ví dụ này, sẽ có từ 10 cột trở lên trong câu lệnh SELECT. Nếu truy vấn không thành công, sẽ có ít hơn 10 cột được trả về. Nếu lỗi xảy ra, nội dung có thể sẽ là:
Unknown column '10' in 'order clause'
Sau khi tester tìm ra được số lượng các cột, bước tiếp theo sẽ là tìm loại các cột đó. Giả sử có 3 cột trong ví dụ trên, thì tester có thể thử từng loại cột, bằng cách sử dụng giá trị NULL để trợ giúp:
http://www.example.com/product.php?id=10 UNION SELECT 1,null,null--
Nếu truy vấn không thành công, tester sẽ có thể nhìn thấy nội dung tương tự sau:
All cells in a column must have the same datatype
Nếu truy vấn thành công, cột đầu tiên có thể là một số nguyên. Tiếp đó tester có thể thực hiện các bước tiếp theo:
http://www.example.com/product.php?id=10 UNION SELECT 1,1,null--
Sau bước thu thập thông tin thành công, tùy vào ứng dụng mà nó chỉ có thể cho tester kết quả đầu tiên, vì ứng dụng đó chỉ có thể xử lý dòng đầu tiên trong chuỗi kết quả. Trong trường hợp này, ta có thể dùng mệnh đề LIMIT hoặc tester có thể thiết lập một giá trị không hợp lệ, bằng cách chỉ làm cho truy vấn thứ hai trở thành hợp lệ (giả sử không có đầu vào trong trong cơ sở dữ liệu mà có ID = 99999):
http://www.example.com/product.php?id=99999 UNION SELECT 1,1,null--
Kỹ thuật khai thác Boolean
Kỹ thuật khai thác Boolean rất hữu ích khi tester tìm thấy một trường hợp SQL Injection "mù quáng" , mà trong đó không biết gì cả về đầu ra của một hành động. Chẳng hạn như, hành vi này sẽ xảy ra trong các trường hợp mà lập trình viên đã khởi tạo một trang lỗi mặc định mà trang này không tiết lộ bất cứ điều gì trên cấu trúc của truy vấn đó hay trên cơ sở dữ liệu. (Trang đó không trả về lỗi SQL nào mà chỉ trả về một lỗi HTTP 500, 404 hay điều hướng lại).
Bằng cách sử dụng các phương pháp suy luận, ta có thể tránh được vấn đề này để khôi phục thành công một số giá trị của các trường mong muốn. Phương pháp này bao gồm việc thực thi một chuỗi các truy vấn boolean dựa vào server, bằng cách quan sát các câu trả lời và cuối cùng suy luận xem ý nghĩa của những câu trả lời đó. Chúng ta xem xét tên miền www.example.com và giả sử nó chứa một lỗ hỏng id đặt tên theo tham số vào SQL injection. Điều này đồng nghĩa với việc thực thi yêu cầu sau:
http://www.example.com/index.php?id=1'
Ta sẽ nhận được một trang báo lỗi thông báo tùy chỉnh do lỗi cú pháp trong truy vấn. Ta giả sử rằng truy vấn được thực thi trên server là:
SELECT field1, field2, field3 FROM Users WHERE Id='$Id'
Câu truy vấn này có thể được thực thi thông qua các phương pháp đã xem xét trước đó. Những gì ta muốn có chính là các giá trị của trường tên người dùng. Các công việc kiểm tra mà chúng ta sẽ thực hiện sẽ cho phép chúng ta có được giá trị của trường tên người dùng, bằng cách tách giá trị đó theo ký tự lần lượt. Điều này có thể thực hiện được thông qua việc sử dụng một số chức năng chuẩn, có trong mọi cơ sở dữ liệu. Trong trường hợp ví dụ của chúng ta, ta sẽ sử dụng cac tính năng pseudo như sau:
SUBSTRING (text, start, length):
trả về một substring bắt đầu từ vị trí "start" của văn bản và chiều dài "length". Nếu "start" lớn hơn chiều dài văn bản, chức năng đó sẽ trả về một giá trị null.
ASCII (char):
trả về giá trị ASCII của ký tự nhập vào. Một giá trị null được trả về nếu char = 0.
LENGTH (text):
trả về số lượng các ký tự trong text nhập vào.
Thông qua các chức năng này, ta sẽ thực thi các kiểm thử trên ký tự đầu tiên, và khi chúng ta thấy được giá trị đó, ta sẽ chuyển tiếp đến ký tự thứ hai và cứ thế tiếp tục cho đến khi ta thấy được toàn bộ giá trị. Các kiểm thử này sẽ "lợi dụng" tính năng SUBSTRING để chọn duy nhất một ký tự mỗi thời điểm (chọn một ký tự duy nhất đồng nghĩa với việc sẽ đặt thông số length về 1), và tính nắng ASCII dùng để thu về giá trị ASCII sao cho ta có thể thực hiện so sánh số. Kết quả của việc so sánh này sẽ được thực hiện với tất cả giá trị của bảng ASCII cho đến khi giá trị đúng được tìm thấy. Ví dụ như, ta sẽ sử dụng giá trị sau cho Id:
$Id=1' AND ASCII(SUBSTRING(username,1,1))=97 AND '1'='1
Giá trị trên sẽ tạo ra câu truy vấn sau (có thể gọi là "truy vấn suy luận"):
SELECT field1, field2, field3 FROM Users WHERE Id='1' AND ASCII(SUBSTRING(username,1,1))=97 AND '1'='1'
Ví dụ trước đó sẽ trả về kết quả chỉ khi nào ký tự đầu tiên của trường tên người dùng bằng với giá trị ASCII 97. Nếu ta nhận được một giá trị sai, thì ta sẽ tăng index của bảng ASCII từ 97 lên 98 và lặp lại request đó. Nếu ta nhận về giá trị đúng, chúng ta sẽ đặt zero cho index của bảng ASCII và chúng ta phân tích ký tự kế tiếp, bằng cách chỉnh sửa các thông số của tính năng SUBSTRING. Vấn đề trong việc hiểu rằng theo cách nào chúng ta có thể phân biệt được các kiểm thử trả về giá trị đúng với các kiểm thử trả về giá trị sai. Để làm được, ta cần tạo ra một câu truy vấn mà luôn luôn trả về sai. Điều này có thể thực hiện được bằng cách sử dụng giá trị sau cho Id:
$Id=1' AND '1' = '2
Giá trị trên sẽ tạo ra câu truy vấn sau:
SELECT field1, field2, field3 FROM Users WHERE Id='1' AND '1' = '2'
Response trả về từ server (mã HTML) sẽ là giá trị sai cho các kiểm thử của chúng ta. Điều này đủ để xác minh xem giá trị nhận được từ thực thi truy vấn suy luận có bằng với giá trị đạt được từ kiểm thử đã thực thi trước đó. Đôi khi phương pháp này không hoạt động. Nếu server trả về 2 trang khác nhau dưới dạng kết quả của 2 yêu cầu web liên tiếp giống hệt nhau, ta sẽ không thể phân biệt giá trị đúng với giá trị sai. Trong các trường hợp cụ thể này, chúng ta cần sử dụng các bộ lọc cụ thể cho phép chúng ta loại trừ mã thay đổi giữa 2 yêu cầu và để có được một template. Sau đó, với mỗi yêu cầu suy luận được thực thi, ta sẽ xuất ra template tương ứng từ phản hồi đó bằng cách sử dụng chức năng tương tự, và ta sẽ thực hiện một điều khiển giữa 2 template để quyết định xem kết quả của kiểm thử đó.
Ở phần thảo luận trước đây, chúng ta vẫn chưa giải quyết vấn đề về việc xác định xem điều kiện kết thúc cho các kiểm thử của chúng ta, chẳng hạn như khi nào chúng ta sẽ kết thúc việc suy luận. Một kỹ thuật để thực hiện điều này sẽ sử dụng một đặc điểm của tính năng SUBSTRING và tính năng LENGTH. Khi kiểm thử so sánh ký tự hiện tại với mã ASCII 0 (như là giá trị null) và kiểm thử này sẽ trả về giá trị đúng, sau đó ta sẽ được thực hiện cùng với quy trình suy luận (mà ta đã quét cả chuỗi string), hoặc giá trị mà ta đã phân tích có chứa giá trị null.
Chúng ta sẽ chèn vào giá trị sau cho trường Id:
$Id=1' AND LENGTH(username)=N AND '1' = '1
Trong đó, N là số lượng các ký tự mà ta đã phân tích đến thời điểm hiện tại (không tính giá trị null). Câu truy vấn lúc đó sẽ là:
SELECT field1, field2, field3 FROM Users WHERE Id='1' AND LENGTH(username)=N AND '1' = '1'
Câu truy vấn sẽ trả về một là đúng hay là sai. Nếu đúng nghĩa là ta đã hoàn tất được suy luận và vì thế ta sẽ biết được giá trị của tham số đó. Nếu sai nghĩa là ký tự null đó đang có trong giá trị của tham số đó, và chúng ta phải tiếp tục phân tích tham số kế tiếp đến khi chúng ta tìm thấy giá trị null khác.
Tấn công bằng SQL injection theo kiểu "mù quáng" cần số lượng lớn các truy vấn thế nên tester có thể cần một công cụ tự động để khám phá ra lỗ hỏng đó.
(To be continued)
THAM KHẢO:
All rights reserved