Привет, наш дорогой читатель! Как обещали, продолжаем наш цикл статей про участие нашей команды Codeby в кибербитве на полигоне The Standoff. Начало нашего приключения можно почитать здесь. В этот раз вы узнаете технические подробности реализованных бизнес-рисков, уязвимостей в веб-приложениях и некоторые хитрости, которые в какой-то мере помогли нам стать победителями. Мы уже писали, что разбились на две команды и разделили цели. При этом в каждой команде был инфраструктурщик, вебер, фишер. Несмотря на то, что это дало свои плоды, оглядываясь назад, мы понимаем, что таким образом у нас уменьшилась коллаборация между специалистами одной категории. Например, для вебера со знаниями PHP попадался ресурс на Ruby, и наоборот. Из-за этого в некоторых моментах мы потеряли время, что непростительно в конкурсе, где каждая команда пытается быстрее эксплуатировать уязвимость и закрыть ее. В следующем году, возможно, стоит сделать разделение именно по специализациям для более быстрого захвата входных точек. В этом блоке я (Алексей - @SooLFaa) (Rambler Group) расскажу, как нами были взяты некоторые риски в части WEB и ряд RCE.  В первый день соревнования мы сразу поделились на две команды. Те, кто пробивали внешний периметр, и те, кто дальше закреплялись внутри доменов (веб и инфраструктура). Я был в первой команде и сразу сконцентрировался только на одной цели - это сервис покупки авиабилетов. Хотя там были уязвимости типа SQL Injection, гадский WAF не оставлял шансов проломиться через них, тогда я стал исследовать логику работы ресурса.  Риск. Покупка бесплатных авиабилетов и билетов на аттракционы. Для начала  разобрался в логике работы процесса покупки билета, по шагам перехватывая каждый запрос в burpsuite от этапа регистрации до этапа оплаты билета.  Забиваем параметры поиска и находим любой рейс. Заполняем персональные данные на нужный рейс и жмакаем кнопочку “Купить”. Перехватываем этот запрос в burp и видим следующее: 4) Сохраняем запрос и идем дальше.  А дальше меня перекидывает на платежный шлюз, но никаким способом купить билет не удавалось. Я снова прошелся по всем шагам и нашёл в исходном коде страницы закомментированную форму, которая ссылается на скрипт success_payment.php с теми же именами параметров, что и запрос в пункте 3. БИНГО! Вероятно, это скрипт подтверждения оплаты.  5) Изменив в POST запросе целевой скрипт, я вернулся в свой личный кабинет и увидел в нем “купленный” билет. Нашему счастью не было предела, мы подумали, что реализовали риск: Логика простая: если билет стоил 1000 рублей, то, купив его за 0 рублей, мы кратно изменили цену. Но, по замыслу организаторов, это оказалось не так. Пока наш капитан ушёл доказывать организаторам нашу правду, тем временем, Мурат (@manfromkz) сумел проломить портал города с помощью уязвимости в заливке файлов. Всё оказалось очень просто: под видом картинки он загрузил payload для выполнения команд на сервере.  Найти путь до картинки тоже не составило труда. Его было видно прямо в разметке HTML.  В итоге наш бэкдор выглядел так. Скачали все исходники, залили полноценный бэкдор и ушли с сервера. Но это оказалось нашей роковой ошибкой, и об этом я расскажу чуть позже. Тем временем, наш капитан вернулся с великолепной новостью: оказывается, это была незапланированная уязвимость, но нам её засчитали и обозначили как новый риск!. Примечательно, что почти одновременно со мной делала тоже самое команда Deteact. Список всех команд, участвующих на киберполигоне в 2020, можете посмотреть на оф. сайте Но и это ещё не всё. После недолгих исследований, мы выяснили, что точно такой же скрипт был и на кассе аттракционов. За дело взялся Олег (@undefi), проделав, всё тоже самое, точно таким же способом мы смогли покупать билеты и там. В итоге +2000 очков в кармане.  Дальше мы разбежались каждый по своим таргетам. Кто-то пытался проломить другие хосты, я продолжил брать риски на авиакассе, Мурат уже стрелял как из пулемета в багбаунти (за что мы его прозвали "пулеметчик"). Об этом он вам расскажет в главе ниже. Но вернемся к нашей кассе… Риск. Сделать недоступным регистрацию на рейс. Тем же способом я начал исследовать каждый запрос в функционале регистрации на рейс.    И я задумался, а что будет, если из запроса удалить id документа и попытаться зарегистрировать несколько билетов на несуществующее место? Одним выстрелом я делаю сразу и то и то. Изменяем запрос. sit=-11&ticketid=6084&transfer=true&transferid=&regdoc= Хорошо, запрос, хоть и выполнился успешно, но регистрация не была пройдена. Когда я снова попытался перехватить запрос, кнопка регистрации просто не реагировала. БИНГО!!! Сломали систему регистрации, но только для одного билета.  Смотрим дальше внимательней, видим, что id билета - это числовой идентификатор.  Burp Intruder - пришло твоё время!  Таким образом, недоступны стали все билеты для регистрации. Ну и + ещё 1000 очков. В итоге на данный момент остается нереализованным еще один риск, и это: Риск. Утечка данных о пассажире со специальным идентификатором 1605157946597718.  Посмотрев исходники города, слитые Муратом, обнаружили помимо файла service_logic.php ещё файл city_dump.sql. И тут пришла идея: если есть такой файл на главном портале, значит на сайте авиакомпании может быть что-то похожее. И почти сразу же угадали имя файла avia_dump.sql  Изучив дамп базы, мы обнаружили поле reg_role и его особенность: если при регистрации добавить этот параметр в запрос и задать числовое значение 3 (reg[role]=3), то наш новый пользователь получал админскую роль. У нас открывается скрытый функционал. Вбиваем идентификатор….. ……..Иииииии… ВНИМАНИЕ!!!!  Ничего нету!!! Видимо, просто до нас уже кто-то удалил эти данные. Но тем не менее риск нам засчитали как полагается. Пора уже оставить в покое эту бедную кассу и перейти в город. Ведь там нас ждал целый дамп с исходниками, который я и начал ресёрчить. Риск. Получить доступ к базе данных городского портала. Изменить размер штрафа пользователя X Изучая исходник, я откопал следующие куски кода Создание штрафов пользователям: ``` Удаление штрафов пользователям: Думаю, вы уже догадались какой вектор.  Запрос на создание штрафа: В результате создан штраф Test с задолженностью -1 И запрос на удаление штрафа: Штрафа не оказалось. Что же, оформляем отчет, сдаём риск и получаем ответ от организаторов: “Не так надо было, риск не засчитан!”. Почему не засчитали, собственно, понятно, ведь сказано было, что править надо в базе. Я надеялся, что по опыту прошлых рисков, может засчитают параллельный вектор.  Но делать нечего, продолжаем ресёрчить исходники.  Почти сразу нахожу в том же коде connectionString.  $db = new db('172.17.61.218:3306', 'script', ';zxxslfc', 'city'); Разумеется, напрямую с публичной сети база недоступна и очевидно, что нужно снова зайти на хост города и через него попытаться дернуть данные из внутреннего сервера. И вот тут-то и начались танцы с бубнами. Уязвимость больше недоступна из-за жесткого WAF, который отрезал просто всё: и легитимные запросы, и нелегитимные. Даже авторизоваться стало нельзя. Больше двух часов я потратил на попытки вернуться по пути Мурата, но всё тщетно. Общение с организаторами тоже ничего не дало. Что же, придется выкручиваться иначе. Смотрим на публичную сеть города и ищем другие хосты рядом, через которые мы также могли бы попасть в базу. Большинство из них были уже недоступны или закрыты WAF’ом (защитники тоже не спали всё это время) наши бэкдоры потеряны, но таки улыбнулась нам удача, нашли мы всё-таки хост http://10.126.40.5:3000/ с какой-то "голосовалкой". Объединившись с Муратом, стали исследовать ресурс. В корне веб сервера нашли файл auth.json, который содержал ключи для авторизации.  Само же веб приложение предлагало функционал голосования, ключевой запрос которого был следующий: Так же нашли файл на сервере package.json и поняли, что перед нами NodeJS.  Google - наше всё. Оказывается, в одном из установленных пакетов была уязвимость в десериализации. Подробнее почитать можно тут можно тут. Стало ясно, что заголовок X-Auth-Token ни что иное, как JWT.  Перебором директории находим ключ для генерации JWT в файле key.pem А дальше дело техники. Генерируем JWT токен с нагрузкой для десериализации: В поле X-Auth-Token подставляем наш JWT и выполняем запрос с нагрузкой “cat /etc/passwd > /opt/pollingapp/pollings/ptestcoder”, которая запишет в файл /opt/polling-app/pollings/ptestcoder содержимое файла /etc/passwd.  Ну и читаем файл через Path Traversal  Сдаем RCE в баунти и возвращаемся к штрафам. Хоть мы и получили шелл, клиента mysql не было на сервере, gopher тоже. То есть ничего, что бы помогло нам хоть как-то взаимодействовать с базой. Тогда мне пришла идея дернуть прям родными средствами NodeJs данные из базы. Нагрузка получила следующий вид: Как вы думаете, что произошло? Ответ - ошибка: “mysql2 module not found”. То есть нет ничего, что помогло бы нам хоть как-то получить данные. Так как сервер отрезан от интернета, “npm install” тоже не помог. Потанцевав ещё с бубнами, Увы и Ах, мы так и не смогли добраться до этих штрафов. Все доступные ранее RCE в этой сети уже не работали из-за WAF, сам портал тоже умер. Хоть и хочется закончить этот риск хэппи эндом, но, к сожалению, он так и остался не взят. Теперь вы поняли, про какую роковую ошибку я говорил? Надо было, как только взяли RCE в городе, сразу проресёрчить скрипт и выполнить риск со штрафами. История про ещё одно RCE Во время соревнования мы периодически меняли фокус внимания на багбаунти и искали SQL Injection, SSRF, LFI на всех доступных ресурсах, занимались также уязвимостями в банке (uptime которого оставлял желать лучшего). Мурат продолжал отстреливать SQL инъекциями, я положил на стол ещё пару LFI, периодически помогая команде инфраструктуры, Олег крутил RCE в Мантисе. В общем, все были при деле. И так мы наткнулись на интересный таргет.  Система, которая позволяла рисовать календарь. Перехватив запрос, увидели следующее: Время было 3 часа ночи. Мы с Олегом ковыряли этот сервис, но никакие вектора LFI не поддавались. Тогда мне на ум приходит идея затестить SSRF, подставив в параметр http://свой_белый_ip, я получил заветный reverse-connect. SSRF на лицо. Оформили отчет, сдали в баунти. Разумеется, стали пробовать вектора с RCE (RFI), но я всё таки решил пойти поспать свои 5 часов (2-5 часов - это был наш максимум сна в те дни). На утро пришёл Олег и встретил меня двумя новостями: хорошей и плохой. Хорошим известием было то, что он таки докрутил вектор до RCE. Достаточно было положить файл на внешний сервер с python кодом: “eval(‘тут_любая_unix_команда’)” (Ларчик просто открывался). Когда наконец прилетела сессия: Пример вывода ls -la /home Посмотрев исходники, всё встало на свои места. Т.к. на главной странице был пример исходного кода на php, мы мудрили со всякими нагрузками типа <?php shell_exec(‘cmd’) ?>, но веб сервис был написан на python. В итоге за эту уязвимость мы получили баллы сначала как за SSRF, потом ещё как за RCE. Да, оказывается, так тоже можно было :D. Но была и плохая новость. Уязвимость закрыли в течение 15 минут после её эксплуатации. Причём сделали это брутально: полностью запретили обращение скрипта к файлам, т.е. даже легитимный запрос на вывод календаря перестал работать :( Конечно, все найденные уязвимости мы описывать не будем, большинство из них были тривиальны, некоторые были неэксплуатабельны из-за действий защитников и WAF’ов. Если все их описывать, то, боюсь, на Хабре кончатся буквы :) Ну а подытожить свою часть хочу, на мой взгляд, самым простым, но при этом зрелищным риском.  Риск. Вывод хакерского видео на главный экран города До конца соревнования остаётся буквально час. В последние сутки мы договорились, что перестаем сдавать отчеты и копим их, дабы не мотивировать другие команды набирать очки. Все уже уставшие, но никто не сдаётся, отрыв от ближайших конкурентов был всего лишь на несколько тысяч баллов, т.е. достаточно им сдать всего один большой риск и нас бы обошли.  Забавно, что этот риск мы могли бы сделать в первый же час соревнования, когда я сбрутил учетку “operator:operator” с первой же попытки :D Но тогда мы не понимали, что именно необходимо сделать. Залогинившись под юзером, стали исследовать все возможные запросы и нашли следующий скрипт Он возвращал Json-объект пользователя с токеном для сброса пароля.  Используем его для установки нового пароля. А дальше просто меняем ссылку главного видео ролика портала на свою, и весь город любуется нашим логотипом, ну и вы полюбуйтесь Вроде всё просто? Но не тут-то было. Помимо нас в конце соревнования в этот таргет валились и другие команды и так же меняли пароль администратора всё это время, соответственно у нас то и дело умирала сессия. Началось противостояние между хакерами за право владения админской учеткой.  Капитан, который даёт команду “Сейчас я скажу и сразу меняй пароль прям за 5 секунд”. А потом крики “Твою ж *, опять не успели”. В итоге буквально в последние минуты соревнования мы реализовываем, кое как оформляем и отправляем этот риск. И как оказалось не зря, всё, что мы сдали в последний час, сыграло важную роль, чтобы после закрытия соревнования сохранить лидерство.  На этом я с вами, дорогие Хабравчане, прощаюсь и передаю слово своему соратнику по команде Мурату - "пулеметчику", который расскажет подробно, за что он получил своё прозвище и как он сделал большинство очков в баунти.  Эту мини-главу расскажу я - Мурат (@manfromkz). И так, похекали поехали! В сети компании Nuft по адресу 10.126.100.167 находился сайт для онлайн регистрации студентов на разные курсы (Online course registration - OCR). LMS на минималках. Этот ресурс помог нам заработать около 4400 очков, и вы скоро узнаете как. Сначала была найдена слепая time-based SQL-инъекция в форме авторизации на главной странице ресурса: Много времени было потрачено на раскрутку этой инъекций, но в итоге хэш пароля и логин администратора удалось получить: Тут ждала еще одна засада - хэш никак не хотел брутиться. Не помогла даже специально собранная брут-машина с крутыми видеокартами и с огромными словарями. Наверняка другая команда уже вошла в систему и сменила пароль администратора на сложный. Хост был заброшен из-за этого на некоторое время. Позже я просто попробовал ввести в форму авторизации классический вектор ' or 1='1… и тут запахло горелым. Нет, дело не только в том, что мы потратили кучу времени на раскрутку ресурсозатратной уязвимости и не в тщетных попытках брута небрутабельного хэша, а в том, что реально что-то горело. И это не то место, о котором вы возможно подумали =) Начал чувствоваться сильный запах гари. Я срочно встал и начал вынюхивать все рабочее место, чтобы исключить неисправность кабелей или ноутбука. В рабочей комнате не смог найти источник запаха, и начал носиться по всей квартире, пока не услышал шум на лестничной площадке. Как только открыл дверь, на меня пошли клубки дыма и ужасный запах, а все соседи были на лестничной площадке, кто-то в спешке выводил своих детей на улицу. Спросил у любого рандомного соседа:- Нашли источник дыма?Ответ был кратким и содержательным:- Да, на первом этаже. Сказал про себя “отлично, but not today!”, закрыл дверь, открыл окна и продолжил исследовать хост, в надежде, что не сгорю или не задохнусь. Вернемся к нашему объекту. После авторизации под студентом, можно редактировать свой профиль. Диоксид углерода сделал свое, и я решил просто загрузить php-файл вместо фотки студента: Получилось. Да, все просто. Получаем заветный шелл: Шелл у нас приватный и классный (спасибо @explorer), естественно, что местами лили и общедоступные шеллы и забирали шеллы у других команд или даже “маскировали” свой под легитимные страницы. Было предпринято огромное количество попыток повышения привилегий полученного пользователя www-data. Но, как назло - ядро свежее, кривых сервисов нет, все энумерации ничего годного не показали. “Палить” 0day эксплойты под последние версии ядра Linux было нецелесообразно, поэтому мы решили пойти иным путем.  Так как шелл у нас приватный и классный, через пару нажатий кнопок в интерфейсе я скачал исходный код системы OCR. BugBounty программа The Standoff принимала уязвимости типа SSRF, XXE, LFI/RFI, RCE, SQL-injection. Открыв код системы, я понял, что сейчас на нашей улице будет праздник, если не сгорю :-). Так можно продолжать еще на две страницы, если не больше. Везде ничего не фильтруется. Сколько уязвимостей видите в приведенном участке кода из файла check-availability.php? Я вижу одну. А в edit-course.php? Тоже одну? Я вижу четыре. Точно так же в course.php вижу четыре SQL-инъекции, а в student-registration.php вижу еще две. Чисто технически, уязвимость каждого параметра каждого скрипта - это отдельная уязвимость. Несложная арифметика 1+4+4+2=11. Считайте уже 11*150=1650 баллов у нас в кармане. И это только начало. Просто покажу список отправленных репортов только по этому хосту и только по SQL-injection: Да, были конечно моменты, от которых “горело” не только в подъезде, но и в душе. Когда брали вектор, нас “выпуливали”, мы не сдавались и бились до конца. Но иногда включались или организаторы или защитники и правили код, убирая уязвимости напрочь. Таких случаев было не мало и каждый совершенно различен, но вот просто очень охота, чтобы Вы это прочли и поняли каково это. Изложение “матное” и дико увлекательное про хост 10.126.80.187 от Олега (@undefi) Изначально, там был сервис с двумя уязвимыми файлами. Первой нашли читалку файлов, через которую прочитали исходники и нашли второй файл с RCE. Скрипты выполнялись от рута. Залились на хост, пытались с него пробиться в локалку, но у него не было доступа. Залили RAT, залили себе шелл на будущее для майнеров и пошли дальше. Спустя какое-то время вернулись к хосту и обнаружили, что нас выкинули. Залились снова, спрятали шелл. Мура начал изучать хост, нашёл шелл другой именитой команды, выкинули их. Увидели, что шелл льётся снова, и то, что противники закрепились по крону, заблочили им каталог для залива. И снова мы оставили хост до лучших времен. Пошли тренировать навыки на KoTH (https://tryhackme.com/games/koth) Когда настала пора майнеров, мы вернулись к хосту и обнаружили что? Правильно, похекеров! Во-первых, нас снова выкинули. Во-вторых, уязвимость с RCE запатчили. Кто это был, красные или синие, нам неизвестно. Но запатчили красиво - повесили на уязвимый параметр экранирование (escapeshellargs()). Но читалка файлов осталась (^_^) Ранее, изучая хост, мы нашли там ещё веб-сервисы. Стали изучать через читалку и обнаружили ещё один уязвимый файл с RCE. Но картину нехило так портил WAF. Но, мы ребята не простые, поэтому “щёлкнули” этот файрволл и смогли залиться на хост. И вот тут начинается весь фатал-карнавал. Заваливаемся с Мурой на хост, после - стандартные команды: w, ps. А там швах: 7 или 8 рутовых ssh-сессий, куча бэк-коннектов.  Зовём на помощь зал - Кролика, Богдана и говорим, что сейчас будет битва :) Одновременно коннектимся к хосту, меняем рутовый пароль, начинаем рубить бэкконнекты и рвать рутовые сессии. В то же мгновение кто-то подключается снова. Снова выкидываем, одновременно судорожно пытаемся искать шеллы. Кто-то кидает броадкаст сообщение через wall в духе: “@#$!, хватит выкидывать". Выкидывают нас, мы снова заходим, идёт борьба на лучший пинг и самые быстрые пальцы. Отрубаем bash у рута, видим, что есть ещё юзеры типа rooot, ro0t и учётки, похожие на орговские. Дропаем всех, но коннекты продолжаются. Отрубаем авторизацию ssh по паролю, меняем порт, Мура генерит ключ и на какое-то время это помогает. Выдыхаем. В это время в чате капитанов начинается бугурт.  Мы расслабляемся, ищем неспешно шеллы, находим снова уже знакомый нам шелл, окончательно его вычищаем, заливаем RAT и смотрим дальше. И тут нас “выпуливают”, закрывают порт ssh и дропают RAT. Я так понимаю, это были организаторские “властелины” или те, кто увёл учётку у них. В чате капитанов продолжается холивар, все кидаются жалобами друг в друга, мы материмся в дискорде, обстановка напряжённая, но разрядку внёс один из капитанов мемчиком. Несколько часов спустя хост вернулся в строй после реверта, но как-то странно. Системные настройки вернули, а вот скрипт с RCE как был экранирован, так и остался до конца. Благо, что мы нашли другую уязвимость, снова залились и просто на нём майнили и юзали, как прокси для обхода WAF на другом хосте. Всегда стоит вернуться назад спустя время, и root в кармане Когда сдавали последние отчеты по данному хосту, в личные сообщения нашего капитана Тимура (@BadBlackHat) написал Михаил Левин (один из организаторов кибербитвы): Но мы хотели еще. Только дальше не получилось, так как кто-то начал удалять базу данных хоста, причем через крон. Любая попытка восстановить базу в тот же момент проваливалась. Кто бы это не был, это было очень жестоко, но с одной стороны полезно для нас, так как мы уже успели сдать много уязвимостей. А дальнейшая судьба хоста нас мало волновала. На тот момент, сама статистика сданных уязвимостей на сайте The Standoff уж точно взбудоражила другие команды: Кстати, после The Standoff ко мне постучался сосед и сообщил, что я его топлю. Я просмотрел все места в ванной и не нашел источник, показал соседу. Он побежал на этаж выше. Источник был там. Оказывается моя квартира выступила как прокси для соседа выше, чтобы тот затопил соседа подо мной. Так что, можно смело сказать - огонь и медные трубы я прошел. А турнирная таблица выглядела так: Вы скажете “всего лишь третье место”, а я скажу “всего лишь расслабленные соперники”. Об этом в хитростях, в следующей мини-главе. А мораль сей басни - если есть возможность, всегда скачивайте исходники для ручного анализа. Любой нами отправленный баг в BugBounty программу почти моментально фиксился, не оставляя шансов на закрепление. Иногда просто отрубали весь сервис, иногда удаляли весь уязвимый скрипт, иногда забрасывали все под фаервол. Да, правильных патчей было мизерное количество. И тут мы начали задумываться, а зачем гонятся за баллами в BugBounty, если это отрезает нам путь для дальнейшего исследования инфраструктуры? Какая разница, на какой строчке мы находимся в турнирной таблице, если важна только конечная позиция? Ведь хорошо смеется тот, кто смеется последним. И тут в общем чате для репортов начались появляться такие репорты: Думаю, суть ясна. Отчеты можно было сдать в самом конце, дав соперникам возможность расслабиться. Но, правда, реализованные ранее риски все равно вывели нас на первое место, и запас был почти бесполезен, но укрепил нашу позицию. Были и другие “козыри”, в виде большого количества захваченных хостов на инфраструктуре, но об этом в следующей части. # До встречи в SCADA Пишу на правах вице-капитана (@clevergod) отступление к 3-й и самой огромной и невероятно захватывающей части. Таких ситуаций за 5 суток, чистым материалом набралось “вагон и маленькая тележка” и, поверьте, затронутые в статье вещи никогда не опишут самого процесса участия. Инфраструктура менялась на ходу и была очень большой и не статичной. Замечаете разницу наших статей от статей, поданых другими командами с прошлых PHDays? Мы всегда мечтали почитать подобный материал для простых ребят, чтобы не читалось, как статья вендора или газета “Правда”. Мы хотим лишь доступно пояснить всё, что мы переживали, какие были эмоции, трудности, успехи, что приходилось делать для победы и что у каждого из нас были помимо “битвы” семейные и рабочие дела, различные ЧП. Но не смотря на все это, мы ни разу не повздорили и старались выйти из даже самых трудных или тупиковых ситуаций. В следующей части, надеемся заключительной, мы опишем инфраструктурный процесс. Как мы брали хосты, как боролись с другими командами за звание “короля горы”, как нас переполняли эмоции при регулярном “отлупе”, как мы развернули самую масштабную майниг-ферму The Standoff и многое другое. Вместо тысячи слов, подогревая интерес, покажу, что Вас ждет дальше. Спасибо организаторам, зрителям, участникам за крутое мероприятие! И до встречи в следующей заключительной части.