Райтап на задачу «OnlyBugs Video»

velvetway

Автор:

velvetway

1 ноября 2026

43

01.11.2026
OnlyBugs Video
Категория:

Web

Задача «OnlyBugs Video»

Автор:

LastTeamLead

Первая кровь:
Huobase
Сложность:
medium
Дата публикации:
20.06.2026
Решений:
31
Рейтинг:
4.7/5
Ссылка:
Решать

Из описания таска мы узнаём, что Пикси запустила приватный стрим на OnlyBugs и показывает там свой флаг. Открываем сайт, и нас встречает страница с формой входа и регистрации.

Стартовая страница

Стартовая страница

Регистрируемся, входим и попадаем в список эфиров.

Список эфиров

Список эфиров

Видим две трансляции от Пикси: публичную «Пикси приподнимает майку» (chat12), куда можно зайти кнопкой «Войти», и приватную «Пикси показывает флаг» (chat93) с пометкой private и кнопкой «Закрыто» — именно она нам и нужна.

Публичный эфир chat12

Публичный эфир chat12

Внизу сайдбара есть форма «Новый эфир», где любой пользователь может создать собственную трансляцию, в том числе приватную.

Создание нового эфира

Создание нового эфира

Из описания мы знаем, что флаг находится в самом приватном эфире chat93. Значит, наша задача будет получить доступ к нему.

Сложив всё, что у нас имеется, мы можем сформулировать итоговый результат как получение доступа к данным (в нашем случае — трансляции), к которым мы не должны иметь доступ. Попробуем узнать, есть ли какая-то известная уязвимость, которой будет соответствовать такое описание.

Поиск по уязвимостям несанкционированного доступа

Поиск по уязвимостям несанкционированного доступа

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

Разберёмся, как работает эта уязвимость и как нам её проэксплуатировать.

IDOR — Insecure Direct Object Reference

IDOR — Insecure Direct Object Reference

Теперь мы знаем, что нам надо каким-то образом подменить идентификатор объекта (номер чата в нашем случае) на интересующий нас. Но для этого разберёмся, как вообще устроен доступ к видеопотоку. Зайдём в доступную нам комнату и откроем вкладку «Сеть» в DevTools.

Запросы страницы в DevTools (вкладка «Сеть»)

Запросы страницы в DevTools (вкладка «Сеть»)

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

Переход на /api/media/12

Переход на /api/media/12

https://onlybugs-video.t2.bug-makers.ru/api/media/12?callToken=eyJwcm92a...

В запросе напрямую передаётся ID комнаты, значит, мы можем попробовать аналогичным образом обратиться к интересующей нас 93.

Переход на /api/media/93 с чужим токеном

Переход на /api/media/93 с чужим токеном

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

Открываем app.js в DevTools

Открываем app.js в DevTools

Интересовать нас будет app.js, так как именно в нём будет лежать вся клиентская логика. Для удобства можем выкачать его себе и разобрать подробно:

Весь доступ к трансляции состоит из трёх запросов:

  1. Вход в комнату enter
  2. getCallToken
  3. /api/call/join
Функция openRoom

Функция openRoom

const gate = await api(`/api/streams/${roomId}/enter`, { method: "POST" });
rememberCallNonce(gate.callNonce);

Заходим в комнату. Если мы её участник, сервер в ответе присылает нам callNonce — одноразовый ключ, который понадобится дальше для приватного эфира. Клиент его запоминает, причём раскладывает по номеру чата:

function rememberCallNonce(callNonce) {
  if (!callNonce?.value || !callNonce.issuedForChatId) return;
  state.callNonces[String(callNonce.issuedForChatId)] = callNonce;
}
Функция connectMedia

Функция connectMedia

Тут видно три разных случая: публичный эфир или наш собственный — токен дают просто по chatId. А вот для чужого приватного нужно приложить callNonce и подпись X-Call-Trace. Смотрим, как эта подпись считается:

Функция makeCallTrace

Функция makeCallTrace

async function makeCallTrace(callNonce, chatId) {
  return sha256Hex(
    `${callNonce}.${chatId}.${state.user.id}.${state.user.username}.pixie-live-web`,
  );
}

Получается, что это просто SHA-256 от следующей склейки: наш callNonce, номер чата, наш ID, наш username и зашитая в код строка pixie-live-web. Все эти значения нам известны, поэтому мы можем посчитать её для любой комнаты сами. Но прежде чем его получить, посмотрим, как конкретно мы получаем видео.

const session = await api("/api/call/join", { method:"POST", body:{ callToken: tokenData.callToken } });
player.src = session.mediaUrl;   // /api/media/:id?callToken=...

По токену join возвращает ссылку вида /api/media/93?callToken=…, которую плеер и проигрывает, а сам эндпоинт media отдаёт видео и проверяет, что chatId внутри токена совпадает с номером в URL.

Получается, чтобы забрать приватное видео chat93, нам нужен токен, выписанный именно на chat93. А чтобы его получить, нужны валидный callNonce и подпись, которую мы посчитаем сами. Nonce приходит только тому, кого пригласили в приватный эфир, а Пикси нас в свой эфир не зовёт. Посмотрим, что сервер на самом деле проверяет у этого nonce и действительно ли он сверяет, что nonce выдан именно для той комнаты, токен для которой мы просим.

Увидеть серверную проверку мы не можем, поэтому просто проверим её на практике.

Для этого регистрируем второй аккаунт, а из-под первого создаём свою приватную комнату и приглашаем в неё второго.

Создание своей приватной комнаты и приглашение второго аккаунта

Создание своей приватной комнаты и приглашение второго аккаунта

Заходим со второго аккаунта и в ответе на запрос enter можем увидеть наш callNonce.value.

Ответ на enter

Ответ на enter

Теперь берём этот callNonce, выданный для нашей комнаты chat123, и просим токен для чужого chat93, а X-Call-Trace считаем уже для chatId 93 и своей личности. Если сервер проверяет только валидность nonce и сходимость подписи, он выпишет токен. А если бы он сверял, что nonce выдан именно для 93?..

getCallToken: nonce от своей комнаты принят для чужого chat93

getCallToken: nonce от своей комнаты принят для чужого chat93

Сервер отвечает 200 и выписывает токен прямо на chat93, а это уже значит, что привязки nonce к комнате нет. Подставляем этот токен в такой же урл, как в начале, /api/media/93?callToken=…, и чужой приватный эфир отдаёт нам видео с флагом.

В чём уязвимость?

Получилось так, что getCallToken не привязывает callNonce к конкретной комнате, поэтому nonce от любой комнаты (в том числе нашей собственной) принимается при выдаче токена на чужой chatId. X-Call-Trace это не исправляет и считается из полностью подконтрольных нам данных. По итогу мы получаем возможность чтения чужого приватного потока без приглашения.

Как закрыть уязвимость

Достаточно в getCallToken сверять, что nonce выдан именно для запрошенной комнаты и принадлежит текущему пользователю.

OWASP Top 10

A01:2025 — Broken Access Control: пользователь получает доступ к приватному ресурсу, к которому не был допущен.

CWE

По CWE это классический CWE-639 (Authorization Bypass Through User-Controlled Key) — IDOR: доступ к чужой комнате по подконтрольному chatId.

Также можно выделить CWE-284 (Improper Access Control), так как callNonce не связан с конкретной комнатой.

Если вы нашли опечатку или ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter

Также по теме:

Райтап на задачу «Вектор атаки»

/Разборы задач

Райтап на задачу «Вектор атаки»

Используем внешнюю XML-сущность в SVG-аватарке, чтобы прочитать файл с флагом, и разбираем причину XXE.
Прочитать

11

Райтап на задачу «YOFA 1.0 – grep challenge»

/Разборы задач

Райтап на задачу «YOFA 1.0 – grep challenge»

Разбираем поиск пароля в исходниках YOFA 1.0 и альтернативное получение флага через изменение JWT-токена.
Прочитать

14