# Цветёт, но дурно пахнет: странный кейс с Bloom filter в Next.js Доклад: «Цветёт, но дурно пахнет: странный кейс с Bloom filter в Next.js» Спикер: Анатолий Катюшин ## Вступление У нас будет очень удаленный и очень близкий докладчик. Многие его знают. Всем привет! Это самая красивая презентация, так просят меня говорить другие докладчики. И что мне нравится: тут веб-хакинг сочетается с математикой. И то, и другое я не очень люблю, но здесь оно сочетается. Добро пожаловать. Собственно, кто не знает, я занимаюсь безопасностью. Сейчас работаю как [неразборчиво], security researcher [неразборчиво]. Также нахожусь [неразборчиво], таким образом мы делаем эту конференцию международной. На международной конференции я просто позвоню. Презентация не про меня, она в одну страну есть. А о чем она? ## Исходная задача Мне дали приложение на анализ. Само собой, в лучших традициях веба: приложение black box, под трафиком, никаких стейджей, ничего. Но хотя бы я мог зарегистрироваться сам. Учеток мне тоже не дали. Неважно как, но я нашел там аккаунт [неразборчиво]. Соответственно, я зарегистрировал одну учетку, вторую, сделал с одной и с другой определенные манипуляции. Дальше возник следующий вопрос: нужно как-то получить привилегированный аккаунт. А как его получить? Давайте рассмотрим хотя бы чуть-чуть, что вообще за приложение. Оно написано на Next.js: на фронте React, на бэке Next. Какие вводные мы знаем: можно регистрироваться и, как я уже сказал, нужно получить информацию о себе. Соответственно, у нас есть что-то типа API. Логичный вопрос: могу ли я брутить API? Мне ответили: «Нет, максимум ты можешь два потока с секундной задержкой между восьмью и полуночью, где-то вечером». Брутить так, соответственно, невероятно удобно. Что мы можем еще сделать? Мы можем пойти анализировать статические файлы: может быть, там что-то найдется. Что-то я нашел, но не совсем полезное; в общем-то, просто узнал немного больше о приложении. Можно использовать Google dorks: может быть, они что-то подскажут. Приложение довольно новое, не проиндексировано, находок, в общем-то, нет. Можно попросить [неразборчиво] пойти поискать кто знает что. Тоже как бы не удалось. Можно еще попробовать использовать какие-то auto-discovery-подходы. Вот последний вариант я и решил попробовать. ## Build manifest и странный фильтр Как я уже сказал, это Next.js. В нем есть такая штука, как build manifest. Думаю, все абсолютно знают и видели такой файл. Там могут быть какие-то routes, какие-то правила, какие-то фильтры для routing в том числе. В общем, он находится по пути вида `/_next/static/{buildId}/_buildManifest.js`. Я сделал запрос, пришел в него и смотрю: файл есть, но почему-то routes я там никаких не вижу. Зато вижу, что там есть `routerFilterStatic`, и он большой. Там много букв `E` и `R`. Я подумал: если `E` — это 6, а `R` — это 7, то как бы все вообще на своих местах. Но оказалось, что нет: `e` равно единице, `r` равно [неразборчиво]. Дальше я полез немного поискать в коде Next.js, что это вообще такое и где такая структура встречается. Нашел, что есть такая штука, называется Bloom filter. Он использует под собой MurmurHash. В структуре есть какое-то количество items, есть error rate, и видно, что даже error rate очень маленький. «As this feature compresses very well», как написал разработчик. Соответственно, я пошел в Google. Wikipedia говорит, что это вероятностная структура данных. Вы можете перейти по QR-коду и сами посмотреть, что там написано. Ее реализовал Бертон Блум в 1970 году. Зачем она нужна? Она нужна, чтобы понять, является ли какой-то элемент частью какого-то множества. Работает она так: false positives возможны, false negatives невозможны. То есть буквально мы спрашиваем: «Скажи, ты знаешь вот это?» Фильтр отвечает либо «возможно», либо «нет». От этого мы дальше будем плясать. Если посмотреть в интернете, Bloom filter вообще используется как раз для того, чтобы быстро просеивать данные и откидывать те данные, которые не должны попадать в определенные процессы. ## Как это работает в Next.js Как оно работает в данном случае, в Next.js, если используется Webpack? На этапе сборки приложения он прогоняет routes через MurmurHash, кладет результаты в один из фильтров. Дальше в приложении, когда происходит какой-то request, оно проверяет, входит путь в этот фильтр или нет. Если не входит, то можно сразу вернуть 404. Если входит теоретически, то мы начинаем его обрабатывать. Таким образом все работает быстрее. Я решил: ладно, проблема так проблема. Написал скрипт, вытащил свой код, скопировал все routes из Burp во вкладке Target — кто еще помнит, как пользоваться Burp, — сохранил эти файлы, получил какие-то 30-40 записей, которые были известными, и прогнал. Я вижу: да, вот календарь, какие-то items, login, все отлично. И тут я вижу, что сюда еще попадают API. Я такой: «Так, а почему у нас API туда попадают?» У нас есть вот этот фильтр, у него шанс ошибки очень-очень низкий, количество items — 141. Клиенту я вижу где-то 20-30, но просто по фильтру видно, что количество items — 141. Что в этом случае можно сделать? Давайте просто грузить локально. Зачем нам отправлять запросы куда-то, если мы можем здесь проверить, есть путь или нет? Берем небольшой словарь, где-то 140 тысяч routes. Пишем опять же небольшую обертку относительно того, что мы знаем, и того, что можем искать. Получаем на входе словарь примерно на 2 миллиона 400 тысяч routes. Теоретически прогоняем через фильтр и получаем 642 записи. То есть это те записи, которые как раз-таки можно очень медленно отправить на сервер в два потока. Само собой, из этих 642 примерно 638 тоже были false positive. Но еще несколько были правильными. За счет этого — судя по всему, это был какой-то [неразборчиво] — у меня получилось обратиться к ручке, которая не была закрыта. То есть там был broken object level authorization, и я нашел список пользователей. Таким образом, [неразборчиво] аккаунт, я знаю почту суперадмина, и все, [неразборчиво] приложения [неразборчиво]. ## Почему API попал в фильтр Почему это так? Почему вообще так случилось? В принципе, типичное приложение на Next.js: в нем есть либо `app`, либо `src`, отдельно лежит `pages/api`, ну и определенные папки. На самом деле, особенно если вы используете какую-то либу, она спокойно может положить папку `api` внутрь папки `app`. Это будет работать, там будет немного другой формат описания самого JavaScript-файла, [неразборчиво]. Но суть в том, что Webpack без разницы: он знает, что ему нужно собрать файлы, и он также включает их в Bloom filters. По словам спикера, с Next 16 используется Turbopack, на Next 15 использовали Webpack. Но и в Next 16 можно просто указать, что build использует Webpack, и все будет работать. ## Демо Пока я пытался понять, почему это так, я собрал для себя небольшое демо. Оно происходит с такой случайностью приложения. Мы берем файл вида `api/v1/[id]` [неразборчиво], даем ему случайное имя: то есть от 0 до 5 миллиардов. Пишем middleware, которая говорит: «Вот сюда запросы только раз в минуту, все остальные возвращаем с 429 ошибкой». Для `api/v2` мы такую middleware не подключаем: мы просто всегда возвращаем там что-то «вкусненькое». [Неразборчиво] они на темной стороне, как здесь. Пробуем сделать запросы. Видим, что все здесь возвращается. И видим, что когда обращаемся к `v1`, к сожалению, получаем ошибку 429. Давайте сходим в само приложение и посмотрим, что находится в build manifest. В принципе, build ID совершенно не проблема найти для любого Next.js-приложения. Практически всегда есть страница, либо там немного другой формат, можно выбрать JS, но он есть. Я забираю этот Bloom filter. Видим, что количество items — всего 6, количество байт в результате довольно маленькое. Все остальное стандартное, мы здесь больше ничего не делаем. Пишем простой скрипт, который использует этот фильтр, перебирает какие-то данные и возвращает нам результат. Соответственно, у нас где-то 100 тысяч вариантов от нуля до 5 миллиардов. Наш результат — 8 из 100 тысяч, те, которые теоретически подходят. Так как демо в данном случае будет занимать 8 минут, мы не будем делать запросы напрямую. Зайдем в контейнер, потому что контейнер как раз в сборке случайно [неразборчиво], и просто посмотрим, какое на самом деле действительное имя мы ищем. Мы grep'аем по `id` [неразборчиво], смотрим наш список, который сами перебрали, и видим, что он там есть. Просто делаем запрос к искомому приложению и видим флаг. ## Что было дальше Дальше, в общем-то, ничего. Мне просто стало интересно: на bounty это где-то еще встречается? Я написал скрипт, который проходит все bounty, ищет Next.js, ищет Next.js, в котором есть Bloom filter, и начинает искать там routes. Я нашел где-то 10 похожих кейсов, но дальше это уже было не репортабельно. Потому что, в общем, сами понимаете: это не обязательно секрет. То есть [неразборчиво] может быть [неразборчиво] на других входах, и тебе нужна еще какая-то база. Но сама концепция мне показалась интересной. Я думаю, что, возможно, другие исследователи смогут докрутить это до чего-то большего. Спасибо большое за ваше время. Давайте вопросы, если они есть. ## Вопросы Вопрос: я там в середине слышал про dynamic route. Я не совсем понял: в итоге в этом Bloom filter предполагались только client-side routes, и это ошибка, что API попал в client-side Webpack? Или там все-таки бывают dynamic routes? Что за dynamic route? Ответ: API туда не должен попадать. Да, динамические routes могут быть на клиенте. То есть у тебя может быть какой-то параметр, который ты не будешь прописывать, и также [неразборчиво] на новый dynamic route. Понял, спасибо. Спасибо, что сделал «Ёпрст» международным. Спасибо вам. ## Неоднозначные места - Вступление спикера сильно искажено ASR: должность, место нахождения и несколько реплик про международность конференции оставлены как [неразборчиво]. - Название/роль найденного аккаунта в начале доклада не разобраны. По контексту речь о привилегированном аккаунте, но конкретная роль не восстановлена. - Фраза про «попросить [неразборчиво] поискать» не восстановлена: возможно, упоминался LLM-инструмент или другой сервис. - В описании внутренней структуры Bloom filter не полностью разобраны значения `e`/`r` и часть английской цитаты разработчика. - Фрагмент после нахождения BOLA и списка пользователей искажен: вероятно, речь о дальнейшем получении почты суперадмина или развитии атаки, но деталей недостаточно. - В части про структуру Next.js-приложения не полностью разобраны названия папок и формата файла; восстановлены только уверенные `app`, `src`, `pages/api`, `api` и Webpack/Bloom filters. - В демо не полностью восстановлен точный путь файла `api/v1/[id]`, фраза про «вкусненькое» в `api/v2`, а также команда `grep` и конкретное значение `id`. - В финальном блоке про bug bounty не разобрано, почему найденные похожие кейсы были «не репортабельны»: сохранен общий смысл, но отдельные условия помечены как [неразборчиво].