Привет, Хабр. На прошедшем в ноябре 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'

OSINT

Герой задачи – биолог по имени 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 года)

  • Глаза – зеленые

  • Живет – в Потсдаме

  •  Биолог, профессор, ученая степень доктора – и просто хороший несуществующий человек

  • Недавно ездил в Стамбул, Турция

Digital forensic

Задача разработана командой нашего центра 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.

Вывод строк в PEStudio
Вывод строк в PEStudio

Так как файл написан на .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:

Просмотр кода в dnSpy
Просмотр кода в dnSpy

Из листинга программы определим функционал и алгоритм работы функции 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):

Ресурс «SOC_forum»
Ресурс «SOC_forum»

Ресурс «Data» является строкой в Base64:

Ресурс «Data».
Ресурс «Data».

Декодируем строку любым удобным способом и получаем скрипт на языке 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» и является флагом, который необходимо было извлечь из файла, задание успешно выполнено.

Ну, что? Кто сколько задач решил - делитесь в комментариях!

Комментарии (0)