На конференции OffZone 2025 мы впервые провели наш CTF— Solar 4RAYS FlagHunt. Подготовили для участников 11 тасков разной степени сложности. Всего в соревновании приняли участие более 200 человек, а 89 из них успешно решили хотя бы одну задачу. В этой статье мы подробно разберем способы выполнения заданий.
Miracle elixir
Категория: Reverse
Сложность: Easy
Текст задания:
Команда реагирования на инцидент принесла пачку файлов. Даже удалось записать трафик. Посмотрите, может, что-то удалось украсть хакерам?
В качестве дополнительных файлов были также дамп трафика и *.beam-файлы.
Решение
Предлагаем сначала посмотреть на дамп трафика dump.pcap. Когда его откроем, увидим трафик системы erlang
distribution. В контексте этого протокола данные передаются в открытом виде, а wireshark позволяет разобрать
конкретные значения передаваемых переменных. Если просмотреть трафик полностью, то можно увидеть взаимодействие двух
нод: ihost@172.22.1.3
и cc@172.22.1.2
. В начале происходит установка соединения «ihost» с
«cc», после чего — выполнение команд. Команды идут с хоста «cc» на хост «ihost». Посмотрев на команды, в конце
увидим чтение файла /secret_data/flag1.txt
и отправку его содержимого. Содержимое выглядит как
нечитаемые
данные. Энтропия, конечно, невысокая, но и данных не много.

Так как задание на реверс, переходим к beam-файлам. Этот формат содержит байт-код для виртуальной машины erlang. По названию файлов приходим к тому, что это байт-код, сгенерированный эликсиром. Для декомпиляции есть прекрасный проект на GitHub. Рассмотрев разные файлы, приходим к выводу, что файлы при отправке шифруются. А ключ устанавливается заранее.

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

Осталось совсем немного, у нас есть функция шифрования (Crypto.encrypt), но нет функции расшифровки (Crypto.decrypt). Но если внимательнее посмотреть, то данные шифруются при помощи обычного гаммирования. Следовательно, можно использовать Crypto.encrypt и для расшифровки.

Потому берем iex и используем возможность вызывать функции из скомпилированных модулей. На выходе получаем флаг:
4rays{3lix1r_i5_co0l_and_bad@ss_jo2av30sm}

Persist hunters
Категория: Forensics
Текст задания:
Пока я устанавливал свои любимые приложения, сработал антивирус. Не помню, на что именно, но теперь что-то будет запускаться самостоятельно без моего ведома. Я смог собрать некоторые артефакты с системы — помоги найти все закрепленные вредоносные импланты. Для всех 5 заданий по форензике используется один и тот же архив Triage_Win10.zip, а флаг содержит в себе номер задания.
Для выполнения задания участникам предоставлялся архив с некоторыми артефактами «зараженной» системы Windows 10, среди артефактов были: MFT, файлы реестра, история браузеров, Prefetch, файлы планировщика задач, а также WMI, WER и WDI. В данных артефактах можно было обнаружить пять «вредоносных» нагрузок, которые были тем или иным образом закреплены в системе и могли запуститься при выполнении определенных условий (например, при перезагрузке системы).
Чтобы решить задание, требовалось поискать в интернете или спросить своего ИИ-ассистента, что такое Windows Persistence, а далее — идти по порядку. Угадайка? Нет, ведь это реальная рутина форензика — проверить, что в системе нет вредоносных имплантов в автозагрузке.
Уровень 1: раздел реестра Autoruns
Сложность: Easy
Вероятно, самый известный способ автозагрузки, которым пользуются злоумышленники, если не боятся быстрого обнаружения, это разделы реестра Run/RunOnce, которые есть в SOFTWARE и NTUSER.DAT.
SOFTWARE\Microsoft\Windows\CurrentVersion\Run
Так, при проверке раздела Run в SOFTWARE вы могли обнаружить ключ «1», который выполняет запуск закодированной в Base64 команды PowerShell.
Для получения флага оставалось выполнить следующие шаги:
RegistryExplorer > Значение из Run > CyberChef > From Base64 > Remove null bytes > Flag

Уровень 2: служба Windows
Сложность: Easy
Немного повысим сложность: теперь требуется найти вредоносную службу в Windows:
SYSTEM\ControlSet001\Services
В результате анализа командлайнов всех служб вы можете обнаружить, что в системе установлена служба WinDefender (мимикрия под Windows Defender), которая запускает cmd-команду, содержащую Base64.
Шаги для получения флага:
RegistryExplorer > Командлайн службы WinDefender > CyberChef > From Base64 > From Base64 > Flag

Уровень 3: директория Autoruns
Сложность: Medium
Альтернативой раздела Run являются директории автозагрузки:
C:\Users\<User>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp
Смотрим MFT и находим start.bat в автозагрузке в ProgramData. Открываем файл и забираем флаг.
Шаги для получения флага:
start.bat > CyberChef > From Hex > XOR > Flag
Q: А как мы откроем файл, если у нас лишь MFT?
A: Резидентные файлы NTFS — если файл занимает меньше 1 КБ, то он хранится в самой записи MFT.

Уровень 4: WMI
Сложность: Medium
Детские игры закончились. WMI — мощный инструмент администрирования и достаточно редкий способ закрепления вредоносной нагрузки. WMI позволяет подписываться на события системы для последующего запуска определенных команд. Основным методом анализа является поиск созданных в системе EventFilter и EventConsumer.
Для этого требуется файл базы данных WMI — OBJECTS.DATA:
C:\Windows\System32\wbem\Repository\OBJECTS.DATA
По данному файлу требуется поискать EventFilter.Name и EventConsumer.Name, в результате будет обнаружено 2 подписки: SCM Event Log (легитимно) и SecurityStatusWmi (что-то инородное)
Следующий шаг — поиск по OBJECTS.DATA команды, которую выполняет SecurityStatusWmi.
Шаги для получения флага:
Командлайн SecurityStatusWmi > CyberChef > From Base64 > From Hex > XOR > Flag

Уровень 5: Планировщик задач
Сложность: Hard
Вы внимательно исследовали всю директорию «C:\Windows\System32\Tasks»? Увы, ранее на PhD мы рассказывали, что это не имеет смысла, ведь все задачи давно хранятся в реестре Windows (выступление, статья). Все, что вам требовалось, это исследовать значения раздела реестра TaskCache:
SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache
Самый простой способ — использовать наш плагин для Registry Explorer, который доступен бесплатно в нашем GitHub, ведь на задачу даже сработал встроенный в плагин Alert (поставьте звезду в гите, если мы подарили вам легчайшие офкоины).
Как решить без нашего плагина? Тяжко, но возможно:
- В системе есть 2 задачи «\Microsoft\Windows\Diagnosis\Scheduled» и «\Microsoft\Windows\Diagnosis\Scheduler», которые, согласно DynamicInfo, созданы одновременно 06.08.2025 08:29:02+00:00.
- При анализе значений в дереве TaskCahce можно обнаружить, что раздел Scheduled создан в 2019 году (т. к. это системная задача), а Scheduler — 11.08.2025 19:23:50+00:00, в окрестности размещения прочих вредоносных нагрузок.
- Далее требуется найти Actions задачи {C3944556-16CF-467E-89E3-29D4BFD3EC5A} в разделе TaskCahce\Tasks, где вы сможете обнаружить командлайн:
cmd.exe /c start http://localhost:3000/?ccmmdd=<Payload>
Либо, если вы робот, можно было посмотреть все 210 Actions в hex-формате и найти заветный флаг.
Дальнейшие шаги для получения флага:
Payload > Url Decode > From Base85 > XOR > Flag

Homyak Tap
Категория: PPC
Сложность: Easy
Разбор вашего любимого задания:
Сразу при переходе на страничку видим большую зеленую кнопку, которая увеличивает число кликов. Для получения флага необходимо кликнуть 8888 раз.
Для этого посмотрим, что происходит, когда мы нажимаем на кнопку.

При нажатии на кнопку мы отправляем запрос с координатами клика. Можно также заметить, что при нажатии кнопки в одном месте сервер обнуляет клики.
Чтобы решить данное задание, воспользуемся Python. Для этого напишем следующий код:
def send_random_tap(api_url: str, homyak_value: str):
x = random.randint(600, 900)
y = random.randint(600, 900)
payload = {"x": x, "y": y}
cookies = {"homyak": “[your cookie]”}
requests.post(
f"{api_url}/api/tap",
data=json.dumps(payload),
cookies=cookies,
headers={"Content-Type": "application/json"}
)
После достижения необходимого количество кликов можно получить флаг по кнопке Flag.
Homyak Exchange
Категория: PPC
Сложность: Easy
По кнопке Exchange попадаем на Homyak Exchange. Тут мы видим волатильный актив $HOMYAK, на котором нам предлагается заработать 500 000.

Для этого нам необходимо торговать фьючерсами. Всего доступно 3 фьючерса на 3–5–7 минут.
Обратимся к консоли, чтобы посмотреть, что происходит. Тут видно, что биржа каждые две секунды получает курс актива и время до экспирации фьючерсов, отправляя json { t:0 }.

Попробуем понять, что мы получаем при отправке другого значения t. Увидим, что при отправке отрицательных значений мы получаем курс в будущем.
Для решения этого таска можно разработать скрипт на питоне, который будет выводить цену актива к моменту экспирации фьючерса.
def get_futures():
def get_price(t):
cookies = {'homyak': '[your_cookie]’}
params = {'t': t}
response = requests.post(url, json=params, cookies=cookies)
return response.json()
data = get_price(0)
while True:
for f, t in data.get("futures").items():
print(f, get_price(-(t//2)).get('price'))
time.sleep(min(data.get("futures").values())//2)
if __name__ == "__main__":
get_futures()
Данный скрипт можно улучшить, создав торгового бота, который будет самостоятельно заключать сделки.
Также можно воспользоваться тем, что в случае продажи на счет начисляются кредитные деньги.
После достижения необходимого количество кликов можно получить флаг по кнопке Flag.
Reactive Homyak
Категория: Web/MISC
Сложность: Easy
Для решения таска достаточно увидеть, что фронт написан на React, а значит, мы можем сразу увидеть все страницы сайта в исходниках.
Заходим в devtools, и смотрим, какие файлы были подгружены.

Видим некую admin.js, упоминания которой нет на фронте. Пробуем перейти на /admin — нас отправляет на главную страницу, поскольку страница еще в разработке.

Проходимся дальше по исходникам и видим упоминанийе API-ключа для тестирования.

Повторяем данный код в любой JS-консоли и видим, что полученный key подозрительно похож на Base64. Переводим из Base64 и получаем флаг.

Doppelgänger
Категория: OSINT
Сложность: Medium
Нам предоставлен домен некого сайта. Перейдя по нему, видим лишь одностраничник, без какого-либо взаимодействия с API. Открытых портов, помимо SSH и HTTP/HTTPS, на сервере нет. Попытка перебора страниц или брутфорса SSH ни к чему не приводит.
На сайте также видим упоминание некого Secret, однако доступ к нему запрещен.

Возвращаемся к описанию задания — «две звезды связаны невидимой нитью». Звезды в интернете могут ассоциироваться с серверами или IP-адресами. Нужно найти некую связь между двумя хостами. Получаем IP-адрес домена и обращаемся к Censys, FOFA или иной платформе для сканирования интернета.
На порте 443 (HTTPS) обнаруживаем SSL-сертификат, по хешу или иному уникальному значению которого можно искать хосты в интернете с таким же сертификатом.


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

Rusty love
Категория: Reverse
Сложность: Hard
Нам представлен бинарный файл, написанный на Rust. Можем понять это по строкам.

Бинарный файл требует на вход флаг. Введенный флаг проходит определенные проверки, а затем выводится сообщение о правильности или неправильности данного флага. Если флаг неправильный, выводится сообщение «Ooooowieeeeeee! You should try harder!!». Находим данное сообщение в IDA, по кросс-рефам попадаем в функцию main.

Эта строка выводится в ветке false при проверке флага. В декомпиляторе видим символ }. Вероятно, этот участок кода отвечает за проверку формата флага.

Попробуем потрейсить до этого участка в дебаггере. Замечаем функцию sub_20728, в которую передается указатель на какую-то последовательность байтов, а также число. После исполнения этой функции в памяти можно увидеть строку 4rays{.

На этом моменте гипотеза подтверждается. Участок кода с проверкой символа } отвечает за проверку формата флага. Флаг выглядит так: 4rays{xxx…xxx}. Однако на данном этапе мы еще не знаем точный размер флага (чисто технически размер флага не нужен при решении задачи, однако мы захотели добавить его в код, чтобы направить решающих в правильную сторону). Также о функции sub_20728 можно сделать предположение, что, вероятно, она как-то связана с шифрованием строк. Эта гипотеза подтвердится, если после инициализации «потыкать» кросс-референсы на эту функцию.
Далее происходит проверка размера строки:

Как можно видеть, размер флага должен быть 10, не считая самой обертки 4rays{}.
Далее видим в декомпиляторе следующую проверку:

Похоже, что здесь проверяется какая-то чек-сумма. Если заглянуть в саму функцию 1ef6e, увидим следующую картину:

Действительно! В этой функции подсчитывается какая-то чек-сумма, также видим, что перед этим расшифровывается строка.

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

Зная сам алгоритм, можно теперь восстановить флаг. Для этого будем использовать SMT-солвер. Возьмем z3 для нашей задачи.
Для начала символизируем 10 байтов, создадим объект солвера и зададим ограничения для наших 10 байтов (это цифра или буква в верхнем регистре).
chars = [BitVec(f'c{i}', 8) for i in range(10)]
s = Solver()
for c in chars:
s.add(Or(
And(c >= ord('0'), c <= ord('9')),
And(c >= ord('A'), c <= ord('Z'))
))
val = BitVecVal(0, 64)
Далее дописываем сам алгоритм, получаем примерно следующее решение:
from z3 import *
helper = b"{T0_B3_H0N3ST_175_N0T_4_K3Y_TRY_H4RD3R}"
def solver(target_val):
chars = [BitVec(f'c{i}', 8) for i in range(10)]
s = Solver()
for c in chars:
s.add(Or(
And(c >= ord('0'), c <= ord('9')),
And(c >= ord('A'), c <= ord('Z'))
))
val = BitVecVal(0, 64)
xor_acc = 0x5A
for i, c in enumerate(chars):
# Если цифра, вычитаем 0x30, иначе вычитаем ord('A')
digit_val = If(And(c >= ord('0'), c <= ord('9')),
c - ord('0'),
c - ord('A') + 10)
digit_val ^= (((xor_acc >> (i % 8))) + helper[i] << (i % 8)) & 0xFF
val = (val * 36 + ZeroExt(56, digit_val)) & 0xFFFFFFFFFFFFFFFF
s.add(val == target_val)
# Решение найдено
if s.check() == sat:
m = s.model()
return ''.join(chr(m[c].as_long()) for c in chars)
else:
return None
if __name__ == "__main__":
target = 0x5869e06025c4ad
flag = solver(target)
print(f"Flag:", flag)
SMT-солвер решает для нас систему уравнений, и мы получаем флаг 4rays{W3L0V3RU57}.
Вот и все! Надеемся, вы получили удовольствие, решая наши таски. Мы уже работаем над 4RAYS FlagHunt 2. Следите за обновлениями в нашем телеграм-канале или здесь, в блоге!