Ускорение и автоматизация слепой SQL-инъекции

Ускорение и автоматизация слепой SQL-инъекции

В этой статье поговорим о слепых SQL-иньекциях. Я расскажу, как автоматировать Blind SQL-инъекции.

Еще по теме: Поиск и взлом сервера базы данных MSSQL

Что такое «слепые» SQL-инъекции

Здесь мы раз­берем слу­чаи, при которых воз­ника­ют Blind SQL-инъ­екции, а так­же опи­сана базовая тех­ника их экс­плу­ата­ции. Если ты уже c ними зна­ком и понима­ешь, что такое бинар­ный поиск, — сме­ло перехо­ди к сле­дующе­му раз­делу.

Blind SQL-инъ­екции воз­ника­ют, ког­да мы не можем нап­рямую дос­тать дан­ные из при­ложе­ния, но можем раз­личить два раз­ных сос­тояния веб‑при­ложе­ния в зависи­мос­ти от усло­вия, которое мы опре­деля­ем в SQL-зап­росе.

Как работает Blind SQL-инъекция

Пред­ста­вим сле­дующую инъ­екцию (для прос­тоты исполь­зуем язык PHP, а в качес­тве СУБД возь­мем MySQL):

$query = "SELECT id,name FROM products ORDER BY ".$_GET['order'];

Здесь мы можем ука­зать в парамет­ре order не толь­ко имя колон­ки, но и некото­рое усло­вие, нап­ример:

order=(if( (select 1=1),id,name))
order=(if( (select 1=0),id,name))

В зависи­мос­ти от истиннос­ти логичес­кого выраже­ния (1=1) или (1=0) резуль­тат будет отсорти­рован СУБД либо по колон­ке id, либо по колон­ке name. В отве­те веб‑при­ложе­ния мы уви­дим, по какой колон­ке отсорти­рова­ны products, и смо­жем отли­чить истинное усло­вие от лож­ного. Это поз­воля­ет нам «задавать воп­росы» СУБД, на которые мы будем получать отве­ты «да» или «нет».

Нап­ример, «спро­сим» у СУБД — в таб­лице information_schema.tables боль­ше 50 строк или нет?

order=(if( (select count(*)>50 from information_schema.tables),id,name))`

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

order=(if( (select substring(password,1,1)='a' from users where username='admin'),id,name))
order=(if( (select substring(password,1,1)='b' from users where username='admin'),id,name))
order=(if( (select substring(password,1,1)='c' from users where username='admin'),id,name))
...

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

Как выполнить бинарный поиск

Пе­реве­дем сим­вол иско­мой стро­ки в его ASCII-код и сде­лаем некото­рое пред­положе­ние о воз­можных зна­чени­ях это­го сим­вола. Нап­ример, мож­но пред­положить, что он лежит в ниж­ней полови­не ASCII-таб­лицы, то есть име­ет код в диапа­зоне от 0x00 до 0x7f. Раз­делим этот диапа­зон пополам и спро­сим у БД — в какой полови­не диапа­зона находит­ся сим­вол?

order=(if( (select ascii(substring(password,1,1))>0x3f from users where username='admin'),id,name))

Ес­ли сим­вол ока­жет­ся боль­ше 0x3f, зна­чит, целевой диапа­зон сужа­ется до 0x40-0x7f, если мень­ше — до 0x00-0x3f. Далее находим середи­ну диапа­зона и сно­ва спра­шива­ем у БД: целевой сим­вол боль­ше середи­ны или мень­ше? Потом сно­ва сужа­ем диапа­зон и про­дол­жаем до тех пор, пока в диапа­зоне не оста­нет­ся ров­но одно зна­чение. Это зна­чение и будет отве­том.

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

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

Идея алго­рит­ма

  • Для соеди­нения колонок мы можем исполь­зовать опе­рацию кон­катена­ции соот­ветс­тву­ющей СУБД, ста­вя меж­ду ними ред­ко исполь­зуемый раз­делитель (нап­ример, 0x01). Стро­ки (rows) мы так­же можем объ­еди­нить с исполь­зовани­ем спе­циаль­ных фун­кции (group_concat для MySQL, string_agg для PostgreSQL). В качес­тве раз­делите­ля строк будем исполь­зовать тот же самый раз­делитель 0x01.

  • Ана­лизи­руя получен­ный ответ, мы смо­жем отли­чить начало новой стро­ки (row), под­счи­тывая количес­тво колонок. На опре­деле­ние дли­ны стро­ки мы пот­ратим чуть боль­ше зап­росов. Одна­ко количес­тво зап­росов при­мер­но рав­но логариф­му дли­ны стро­ки и, соот­ветс­твен­но, рас­тет мед­леннее, чем сум­ма зап­росов, которые мы пот­ратили бы на опре­деле­ние длин каж­дой стро­ки в отдель­нос­ти.

  • Так­же в дан­ном слу­чае мы можем не опре­делять дли­ну стро­ки вов­се, а прос­то обна­ружи­вать сим­волы, пока они не кон­чатся, как опи­сано в раз­деле «Опре­деле­ние количес­тва row и дли­ны строк». Одна­ко тог­да мы не смо­жем оце­нить, сколь­ко вре­мени зай­мет получе­ние дан­ных, что, ско­рее все­го, неп­рием­лемо на прак­тике.

  • Ко­личес­тво зап­росов, которые будут пот­рачены на опре­деле­ние раз­делите­лей, зависит от исполь­зуемо­го диапа­зона под­бора. Раз­делитель сам по себе добав­ляет­ся как один сим­вол в диапа­зон. Сум­марное количес­тво раз­делите­лей рав­но количес­тву воз­вра­щаемых строк минус 1. Таким обра­зом, мож­но ска­зать, что мы заменя­ем зат­раты на опре­деле­ние длин строк на зат­раты на опре­деле­ние раз­делите­лей.

  • Ес­ли счи­тать, что на опре­деле­ние дли­ны стро­ки в сред­нем тра­тит­ся 7 зап­росов, как ука­зано в раз­деле «Опре­деле­ние количес­тва записей и дли­ны строк», то это поз­воля­ет сэконо­мить 111 зап­росов на каж­дую стро­ку (где  — диапа­зон поис­ка). Так­же мы теря­ем 111 зап­росов на каж­дый сим­вол, т.к. добав­ляем раз­делитель в диапа­зон.

  • Ис­поль­зовать раз­ные диапа­зоны для поис­ка в раз­ных колон­ках мы можем лишь огра­ничен­но — так как мы исполь­зуем мно­гопо­точ­ность и не можем заранее знать — перева­лим мы через раз­делитель при под­боре оче­ред­ного сим­вола и, соот­ветс­твен­но, попадем в дру­гую колон­ку или нет. Поэто­му либо мы дол­жны исполь­зовать широкие диапа­зоны, что уве­личи­вает 111 и сни­жает выиг­рыш от отсутс­твия необ­ходимос­ти опре­деле­ния длин строк, либо мы можем исполь­зовать соот­ветс­тву­ющие текущей колон­ке диапа­зоны и получать допол­нитель­ное количес­тво оши­бок — попада­ний в канаре­ечные сим­волы с пос­леду­ющим пов­торным поис­ком.

Вы­вод: дан­ная тех­ника при­мени­ма толь­ко в опре­делен­ных слу­чаях, нап­ример ког­да все целевые колон­ки име­ют узкий диапа­зон.

Работа с UNICODE

В слу­чае, ког­да в иско­мом тек­сте встре­чают­ся сим­волы, отсутс­тву­ющие в стан­дар­тной англий­ской ASCII-таб­лице, СУБД при­меня­ют для хра­нения Unicode. При­чем в некото­рых СУБД (нап­ример, MySQL) по умол­чанию при­меня­ется UTF-8, а в дру­гих (нап­ример, PostgreSQL) — UTF-16. В обо­их слу­чаях пре­обра­зова­ние сим­вола с помощью фун­кции ASCII/UNICODE при­ведет к получе­нию зна­чения боль­ше 128. Получе­ние зна­чения боль­ше 128 и будет для нас триг­гером того, что в стро­ке встре­чают­ся Unicode-сим­волы.

Unicode-сим­волы име­ют харак­терный вид для раз­личных язы­ков. Нап­ример, рус­ские бук­вы в UTF-8 в качес­тве пер­вого бай­та име­ют 0xDO или 0xD1. Если мы работа­ем с UTF-8, то мы можем пред­положить, что пос­ле одно­го UTF-8 сим­вола с рус­ской бук­вой будет идти еще один. Тог­да пер­вый байт это­го сим­вола мы можем эффектив­но обна­ружить, исполь­зуя алфа­вит из двух зна­чений [0xD0,0xD1], а для вто­рого бай­та мы можем огра­ничить поиск толь­ко зна­чени­ями, которые соот­ветс­тву­ют рус­ским бук­вам.

Од­нако такой под­ход не очень хорошо сов­местим с мно­гопо­точ­ным поис­ком: веро­ятность того, что сим­вол, иду­щий через
сим­волов пос­ле текуще­го, так­же явля­ется рус­ским сим­волом UTF-8, пада­ет с рос­том N.

В сло­ве на рус­ском язы­ке в UTF-8 каж­дый вто­рой сим­вол будет 0xD0 или 0xD1, но пос­ле кон­ца сло­ва, ско­рее все­го, будет сто­ять одно­бай­товый сим­вол про­бела или зна­ка пре­пина­ния. Это сни­жает веро­ятность того, что через чет­ное чис­ло сим­волов пос­ле встре­чен­ного 0xD0 так­же будет 0xD0 или 0xD1.

Вы­вод: при работе с Unicode воз­можным решени­ем явля­ется исполь­зование одно­го потока на каж­дую стро­ку.

Сжатие данных

Ряд СУБД под­держи­вают встро­енные фун­кции сжа­тия (нап­ример, COMPRESS для MySQL и UTL_COMPRESS.LZ_COMPRESS для Oracle). Соот­ветс­твен­но, их при­мене­ние поз­воля­ет умень­шить количес­тво сим­волов, которые нам тре­бует­ся получить. При этом нам не нуж­но делать пред­положе­ния об исполь­зуемом алфа­вите: резуль­тат сжа­тия — BLOB. Мы будем исполь­зовать пол­ный диапа­зон из 256 зна­чений.

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

SELECT LENGTH(COMPRESS('123'))
>> 15

Вы­вод: подоб­ную тех­нику име­ет смысл при­менять сов­мес­тно с тех­никой соеди­нения строк. Объ­еди­нен­ная стро­ка будет длин­ной, и алго­рит­мы сжа­тия смо­гут сжать ее дос­таточ­но эффектив­но. Одна­ко мы будем зас­тавлять СУБД выпол­нять про­цеду­ру сжа­тия длин­ной стро­ки при каж­дом зап­росе, чем можем уве­личить вре­мя выпол­нения каж­дого зап­роса.

А что там sqlmap?

Работа с потоками

Sqlmap не исполь­зует мно­гопо­точ­ность при опре­деле­нии количес­тва записей или длин строк. Одновре­мен­но он ищет толь­ко сим­волы одной стро­ки.

Определение длины строки

Sqlmap ищет дли­ну стро­ки, если исполь­зует­ся нес­коль­ко потоков. Если исполь­зует­ся один поток, то он счи­тает, что стро­ка закон­чилась, как толь­ко обна­ружен пер­вый сим­вол 0x00.

Поиск символов

При поис­ке сим­волов sqlmap исполь­зует сле­дующую тех­нику:

Пер­вый сим­вол ищет­ся пол­ным перебо­ром из 128 сим­волов (7 зап­росов).

Да­лее выбира­ется сим­вол, с которо­го нач­нется сле­дующий поиск на осно­вании пре­дыду­щего обна­ружен­ного сим­вола:
ес­ли это циф­ра, то sqlmap выбира­ет 48 (ASCII-код минималь­ного сим­вола, циф­ры 0);
ес­ли строч­ная бук­ва, то он выбира­ет 96 (код a);
ес­ли заг­лавная бук­ва, то 64 (код A).

Да­лее про­исхо­дит срав­нение с этим опре­делен­ным чис­лом и исполь­зует­ся обыч­ный бинар­ный поиск в оставшем­ся диапа­зоне. То есть если пре­дыду­щий сим­вол — бук­ва, то дела­ется сна­чала срав­нение с 48, а затем, если иско­мое зна­чение боль­ше, дела­ется поиск в диапа­зоне 48-127.

Единс­твен­ное исклю­чение: если ока­зыва­ется, что сле­ва от текуще­го срав­нива­емо­го сим­вола нет ожи­даемых, то он сра­зу перехо­дит к срав­нению с 1. Если срав­нение с 1 показы­вает, что целевой сим­вол мень­ше, то sqlmap счи­тает, что нашел конец стро­ки, и закан­чива­ет поиск.

К плю­сам дан­ного под­хода мож­но отнести эффектив­ное обна­руже­ние кон­ца стро­ки, осо­бен­но если пос­ледний сим­вол стро­ки — чис­ло. Тог­да конец стро­ки будет опре­делен все­го за 3 зап­роса. Но эта тех­ника име­ет и не­дос­таток — низ­кая эффектив­ность поис­ка в час­ти слу­чаев.

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

зап­росов. При этом один зап­рос уже будет сде­лан для срав­нения с 48, так что сум­марно sqlmap пот­ратит 7,4 зап­роса (дру­гими сло­вами, появ­ляет­ся веро­ятность, что он пот­ратит 8 зап­росов). При этом, если бы sqlmap делал пол­ный бинар­ный поиск, то пот­ратил бы ров­но 7 и никак не боль­ше.

Выбор диапазона символов

У sqlmap есть параметр --charset, с помощью которо­го мож­но задать диапа­зон сим­волов, сре­ди которых будет про­водит­ся бинар­ный поиск.

Unicode

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

Дру­гие рас­смот­ренные вари­анты опти­миза­ции в sqlmap не реали­зова­ны (по край­ней мере, я их не обна­ружил).

Выводы

В этой статье я при­вел теоре­тичес­кий обзор основных спо­собов опти­миза­ции экс­плу­ата­ции Blind SQL-инъ­екций. Какие‑то из них более эффектив­ны, какие‑то менее, но я попытал­ся при­вес­ти наибо­лее пол­ный перечень. Если у тебя есть дру­гие идеи и тех­ники опти­миза­ции — пиши мне в телег­рам @sorokinpf, обсу­дим!

Пос­тепен­но я пла­нирую реали­зовать некото­рые из опи­сан­ных спо­собов в сво­ем фрей­мвор­ке для экс­плу­ата­ции Blind SQL-инъ­екций — sqli_blinder (да, с фан­тази­ей у меня так себе). На дан­ный момент фрей­мворк реали­зует баналь­ный бинар­ный поиск, под­держи­вает работу с SQLite, MSSQL, Oracle, MySQL и PostgreSQL. Для работы с ним необ­ходимо написать одну фун­кцию на python, которая опре­делит, куда фрей­мвор­ку под­став­лять зап­росы и как ана­лизи­ровать отве­ты

ВКонтакте
OK
Telegram
WhatsApp
Viber

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *