Сьогодні я хочу розповісти тобі про деякі особливості функціонування веб-додатків, які можуть вплинути на їхню безпеку. Перш за все, пропоную звернути увагу на відмінності між термінами "безпека сайту" і "безпека системи управління сайтом". У самому справі, хоча ці речі і взаємопов'язані, але, як показує практика - вони всього лише пересічні безлічі. Пентестер, що виконує аудит конкретного сайту методом "чорного ящика" може виявити недоліки CMS, на якій цей сайт працює, а може і не виявити. Як пощастить.
Ніколи не здавайся
Трохи лірики ... Одного разу мені довелося досліджувати одну дуже добре захищену систему. Мозкові штурми слідували один за іншим і нічого не приносили, ідеї вичерпувалися, а результат залишався практично нульовим. Я почав писати різноманітні фаззери, смикаючи то один, то інший скрипт в надії витягнути хоч що-небудь, але все було марно. Однак, крилата фраза на сірниковій коробці з чаплею та жабою, підкорила серця багатьох наших співвітчизників, виявилася як завжди бездоганно правильною. У купі відповідей сервера на різноманітні запити в очі кинулися відповіді аномально маленької довжини. Після їх більш докладного вивчення стало ясно, що сервер періодично не встигав відпрацьовувати мої наворочені запити за max_execution_time і скрипт падав з 500-м статусом. Це було вже щось, так як в помилці містилися абсолютні шляхи і імена скриптів на сервері. Вивудивши найважчий для сервера запит (ним виявилася функція створення мініатюри з формату TIFF), я поставив його в цикл в багатопотоковому режимі і став збирати інформацію. Через нетривалий час у мене були відповіді 11 різних типів, кожен з яких розкривав ім'я і шлях до свого класу. Другий раз щастя посміхнулося в Гуглі, коли виявилося, що один з цих класів доступний для скачування на просторах Мережі. Після вивчення ісходника були виявлені слабкі місця та проведена атака перевизначення змінної з Register_Globals = ON. Підбирати ім'я цієї змінної, не бачачи исходников, можна було роками ... Движок здався, а корисний досвід і спонукав мене до написання цієї статті.
Установки PHP
Після такого дебюту відразу стало цікаво знайти інші можливі шляхи до проведення схожих атак. У налаштуваннях інтерпретатора PHP були виділені наступні опції:
max_execution_time
max_input_nesting_level
max_input_time
memory_limit
pcre.backtrack_limit (PHP> = 5.2.0)
pcre.recursion_limit (PHP> = 5.2.0)
post_max_size (PHP> = 4.0.3)
upload_max_filesize
max_file_uploads (PHP> = 5.2.12)
Тут не все, але найбільш поширені опції, що називається common:). Весь список опцій (включаючи різні модулі) можна знайти на php.net / manual / en / ini.list.php. Шукати за ключовими словами max, limit. З усіх параметрів слід було виявити найбільш застосовні. Тут я керувався, насамперед, універсальністю: хотілося знайти параметри, які вдасться компрометувати на як можна більш широкому спектрі налаштувань PHP та веб-серверів. Після довгих мук, описувати які тут не буду, виявилося, що самі придатні до використання - max_execution_time, memory_limit. Вони викидаються в тіло відповіді при налаштуваннях error_reporting = E_ERROR або вище, і display_errors = On.
Таке можна зустріти в більшості дефолтних конфіги. Крім того, варіюючи значення змінних, можна домогтися випадання помилок з різних місць програми. Врезультате ми отримаємо не тільки назви класів, скриптів, шляхи до додатка, але і поняття про ієрархію викликів у програмі. Але це ще не всі дані, які потрібно мати для початку роботи.
Підготовчий етап - URI max length і max_input_nesting_level
Для початку напишемо прості скрипти для визначення двох параметрів сервера - максимальної довжини GET-запиту і максимальної глибини вкладеності вхідних даних. Навіщо вони стануть в нагоді буде розказано далі. Максимальна довжина запиту встановлюється веб-сервером, визначити її дуже просто методом дихотомії (розподілу відрізка навпіл). Код на PHP виглядає приблизно так:
function fuzz_max_uri_len ($ url) {
$ Headers = array ();
$ Data = array ();
$ Left = 500; / / Значення лівого краю шуканого діапазону
$ Right = 64000; / / Значення правого краю шуканого діапазону
$ Accur = 5; / / Точність, з якою визначаємо значення
while (($ right-$ left)> $ accur) {
$ Cur = ($ right $ left) / 2;
$ Data ['x'] = str_repeat ("x", $ cur);
list ($ h, $ c, $ t) = sendGetRequest ($ url, $ headers, $ data);
$ S = intval (substr ($ h, 9,3));
if ($ s <400) {
$ Left = $ cur;
}
else {
$ Right = $ cur;
}
echo "\ n $ cur \ t $ s";
}
return (($ right $ left) / 2);
}
Другий необхідний параметр max_input_nesting_level - властивість вже суворо налаштування інтерпретатора, за замовчуванням дорівнює 64. Це значення визначає максимальну розмірність масиву, яку може мати змінна, що приходить від користувача. Розглянемо для прикладу ось такий код:
<? Php echo $ _GET ['a'];?>
У випадку, якщо, max_input_nesting_level = 1 і ми передамо в запиті? A [][], на екрані нічого не з'явиться, в інтерпретаторі виникне помилка рівня Notice, що говорить про те, що змінна не оголошена. Якщо ж ми збільшимо значення параметра до 2 і повторимо запит, на екрані вже висвітиться "Array". Здавалося б, саме такий самий простий спосіб визначити значення цього параметра - знайти скрипт, який в явному вигляді виводить значення якої-небудь користувальницької змінної та викликати його, збільшуючи вкладеність, поки не зникне напис Array. Такий пошук знову-таки варто проводити методом дихотомії. Проте я спробував написати більш універсальний алгоритм, який буде працювати навіть у випадку, коли у відповідь виводяться змінні, тільки побічно залежать від користувальницької. До цих пір не впевнений з приводу оптимальності обраного алгоритму, так що уявляю його на суд громадськості:). Суть в тому, щоб поступово збільшувати значення розмірності масиву і аналізувати кількість байт відповіді. Якщо довжина відповіді відрізняється від попереднього більше ніж на якесь порогове значення, це вважається аномалією й фіксується в балці. Доповнивши мій PoC нехитрої функцією побудови графіків, я одержав цікаві картинки, які представлені у виносках. У більшості випадків, по спаду графіка залежності розміру відповіді від розмірності масиву й значення параметра. Цей алгоритм нагоді мені й надалі, плюс я написав аналогічний статистичний аналізатор для часу відповіді сервера.
Надмірне вживання пам'яті шкодить вашому скрипту
Повернемося до нашої святої мети - спровокувати помилку "Allowed memory size exhausted". В якості самого тривіального прикладу, розглянемо PHP-код <? Php echo 'OK';?>.
Здавалося б, яке тут споживання пам'яті?! Насправді, такий скрипт може жерти мегабайти ОЗУ. І тут, не сперечаюся, немає провини програміста, який написав його. Для виведення розміру використовуваної пам'яті в PHP служить функція memory_get_usage (). Пропоную дописати її до тривіального скрипту і провести деякі виміри. Для початку викличемо наш скрипт не зі змінною, a методом GET. Споживання зросте десь на 1 Кб. Інтерпретатор вже виділив більше пам'яті під значення змінної, тому, якщо послати запит "? A = aaa", споживання пам'яті не збільшиться. Наше ж завдання - отримати якомога більше виділеної пам'яті при як можна більш короткій довжині GET-запиту (максимальне значення ми вже отримали і тримаємо в думці). Спробуємо тепер передати запит з параметром? A [], кількість спожитої пам'яті збільшиться вже приблизно на 500 байт. Тепер в гру вступає другий параметр, який був визначений вище - max_input_nesting_level. Як тільки розмірність нашого масиву перевищить його, споживання пам'яті буде рівносильно нагоди, коли ми взагалі нічого не передаємо. Для експерименту я перевірив, скільки ж пам'яті буде споживати тривіальний скрипт якщо немає обмеження на розмірність масиву. Виявилося, що при запиті? A ([] x2500 разів) тривіальний скрипт їсть близько 1.2 Мб. Цього, звичайно, занадто мало, щоб вивалитися за memory_limit, але і скрипт наш не схожий на реальне веб-додаток. Щоб моніторити споживання пам'яті будь-якої програми, можна написати дуже простий скрипт:
<? Php echo "marker:". Memory_get_usage ()."#";?>
і додати його до директиви auto_append_file в php.ini. Тепер неважко написати функцію, яка буде шукати у відповіді сервера маркер і отримувати значення споживаної пам'яті. Функція буде така:
function findMarker ($ content) {
$ P1 = strpos ($ content, "ONsec E500 mem:");
if ($ p1 === false) {
return 0;
}
else {
$ P2 = strpos ($ content ,"#",$ p1);
if ($ p2 === false) {
return 0;
}
else {
$ Mem = substr ($ content, $ p1 15, $ p2-$ p1-15);
}
}
return intval ($ mem);
}
Тепер ми можемо спробувати отримати практичну користь від усього написаного. Тут слід запастися удачею. Навскидку, без вихідного коду визначити скрипти, які люблять пам'ять, може бути непросто. Порада така - шукай цикли з обробкою масивів, рекурсії і всього такого ж плану. У ряді випадків може виявитися, що лучшеіспользовать POST, де істотно більші обмеження на довжину переданих даних. Раджу взяти з диска мій PoC і подивитися функцію fuzz_memory_usage (). Її можна використовувати для перебору змінних різними методами (POST, GET, Multipart) і для виявлення найбільш вигідних для виділення пам'яті комбінацій. Там же вбудована перевірка на аномальнию довжину і час відповіді, так що, якщо довгоочікувана помилка з'явиться, ти її не пропустиш.
Повільний скрипт - вразливий скрипт
На відміну від споживання пам'яті, час виконання скрипта залежить від навантаження на сервер і взагалі є величиною, м'яко кажучи, мінливою. Змусити додаток відпрацьовуватися довше, ніж зазначено в параметрі max_execution_time, непросто. Є навіть клас вразливостей в OWASP, називається "dead_code". Це помилки розробника, які можна експлуатувати з метою злому, наприклад, для провокування помилки перевищення часу виконання. Тестуючи додаток або сайт, ти вже маєш якесь уявлення про те, які запити відпрацьовують швидше, а які повільніше, ніж інші. Це, знову ж таки, всілякі цикли. До речі, фільтри безпеки часто грішать повільною швидкістю виконання. Особливо це стосується фільтрів, що виправляють запит. Знаючи як працює фільтр, можна згодувати йому запит, для приведення якого буде потрібно багато ітерацій.
Крім того, небезпечні операції з файлами, наприклад, зловмисник може спробувати завантажити великий файл в декілька потоків. Якщо веб-додаток спробує записати файл в те саме місце, куди ще не дописався цей же файл від іншого запиту, то воно дещо "забариться". Але, знову ж таки, все залежить від використовуваних функцій, ОС, ФС, налаштувань і багатьох факторів. Ось загальні рекомендації, які можна дати для пошуку вразливих скриптів. У загальному випадку, постійно збільшуючи навантаження на сервер, зловмисник рано чи пізно все одно отримає те, на що розраховує. Звичайно, і такі старання неважко припинити, але це вже виходить за рамки веб-додатки.
Розглянемо тепер живий приклад на останній версії Бітрікс і тестовій площі. У системі були виявлені деякі особливості, а саме:
1. При завантаженні файлу в якості аватара, він поміщається в директорію з Трьохсимвольний ім'ям, діапазон символів хексовий (16 ^ 3 = 4096).
2. При оновленні аватара, директорія зі старим аватаром видаляється.
3. При завантаженні аватара з ім'ям довше 250 символів, директорія створюється, а файл не завантажується. Створена в такий спосіб директорія вже не видаляється.
Можна розраховувати на те, що рясне кількість створених директорій буде збільшувати час виконання скрипта завантаження аватара. Перевірити це можна простим запитом Multipart, запущеним в кілька потоків. Знову-таки, перевіряємо на аномалії по довжині і часу відповіді, зберігаючи такі результати у файли. Запустивши такий алгоритм в 20 потоків, я отримав файли, що відрізняються по довжині.
Розбираємо результати
По завершенні вилову заповітних відповідей справа залишається за малим - акуратно розібрати їх, вичленувати шляху з повідомлень про помилки і розташувати за рівнями залежно від довжини відповіді. Це вирішується приблизно таким кодом:
function parseResults ($ dir) {
if (is_dir ($ dir)) {
if ($ dh = opendir ($ dir)) {
$ I = 0;
$ Results = array ();
while (($ file = readdir ($ dh))! == false) {
$ CurFile = $ dir. $ File;
$ Fh = fopen ($ curFile, 'r');
$ Filedata = fread ($ fh, filesize ($ curFile));
$ Fsize = filesize ($ curFile);
$ P1 = strpos ($ filedata, "Maximum execution time of");
if ($ p1 === false) {}
else {
$ P2 = $ p1 52;
$ P3 = strpos ($ filedata, "</ b>", $ p2);
if ($ p3 === false) {}
else {
$ Len = $ p3-$ p2;
$ Path = substr ($ filedata, $ p2, $ len);
$ Unique = true;
/ / Перевіряємо на унікальність
foreach ($ results as $ key => $ value) {
if ($ value ['path']==$ path) {
$ Unique = false;
break;
}
}
if ($ unique) {
$ Len = $ p3-$ p2;
$ Res = array ('path' => substr ($ filedata, $ p2, $ len), 'len' => $ fsize);
$ Results [$ i] = $ res;
$ I;
}
}
}
fclose ($ fh);
}
closedir ($ dh);
$ Size = count ($ results) -1;
/ / Сортуємо результати по довжині
for ($ i = $ size; $ i> = 0; $ i -) {
for ($ j = 0; $ j <= ($ i-1); $ j)
if ($ results [$ j] ['len']> $ results [$ j 1] ['len']) {
$ K = $ results [$ j];
$ Results [$ j] = $ results [$ j 1];
$ Results [$ j 1] = $ k;
}
}
return $ results;
}
}
}
На виході отримуємо відсортований масив з довжинами відповідей та іменами скриптів, у яких виникла помилка. Найприємніше - можна відновити хоч весь стек, тільки це займе чимало часу. До речі, на своїй віртуалку я наловив 126 класів за 30 хвилин. Залишається оформити звіт по рівнях ієрархії в красивому форматі. Власне, все це усередині PoC і міститься - користуйся на здоров'я!
Висновок
Це звичайно не всі можливі варіанти отримання інформації через провокацію помилок. Існує ще безліч варіантів, методик і технік, які можна застосувати як для конкретних сайтів, так і для движків цілком. Всі ці техніки, прийоми та методи належить ще знайти і використовувати, публікувати і модернізувати. Є й безліч проблем - наприклад, оптимізувати PoC для зменшення кількості запитів і зменшення слідів у логах. Ця стаття мала на меті показу основ техніки. Сподіваюся, вийшло. Як завжди, відповідаю в блозі на запитання.