Иллюзия безопасности: как я научился обходить виртуальные очереди (и почему клиентская защита — это театр)

от автора

Как забавно, но с развитием прогресса мы получили немного старого доброго Советского Союза в интернете — речь про виртуальные очереди. Правда в интернете стоять в очереди не так утомительно, как в реальности, но тоже не очень приятно. 

Тут не получится через знакомого продавца встать поближе или вообще зайти с заднего хода и получить желаемую вещь раньше остальных… Или получится?

Я нашел на просторах интернета интересную статью, про хаккинг виртуальных очередей — вот ее немного адаптированный перевод.

Искра

Я пытался купить лимитированную виниловую пластинку моей любимой группы. Они объявили о внезапном релизе — 2000 копий на весь мир, пронумерованных вручную, без возможности переиздания.

Я был их фанатом одиннадцать лет. И даже набил их тексты в виде тату на своем предплечье.

Старт продаж был назначен на 15:00. Я взял на работе отгул на полдня. Завел три будильника. Открыл и подготовил страницу заранее, за 15 минут.

Наступает 15:00:00. Я делаю клик. 

«Пожалуйста, подождите. Вы помещены в очередь. Примерное время ожидания: 52 минуты.»

Внутри все сжалось. Я смотрел, как тают цифры. 48 минут. 39 минут. 22 минуты. У меня начала появляться надежда. Но итог был плачевным: 

«Извините, этот товар распродан.»

Я просидел ошеломленный еще очень долгое время. Что-то внутри меня надломилось в тот день. Не было никакой драмы — я не переворачивал столы и не клялся отомстить вселенной. Просто пустота, которая заполнила все внутри.

А потом я подумал: А как это вообще работает? И впервые в жизни открыл инструменты разработчика в браузере.

Учимся видеть

Первый месяц больно бил по моему самолюбию.

Я не понимал, что такое сетевой запрос. Не разбирался, как работают cookies. Никогда не слышал об эндпоинтах API. Консоль разработчика казалась мне скоплением китайских иероглифов для избранных.

Но я упрям. Патологически упрям. Я начал с обучающих видео на YouTube. Затем перешел к документации. Затем — к форумам, где люди обсуждали парсинг и автоматизацию. Медленно, мучительно, но туман непонимаия рассеивался.

Я узнал, что веб-сайты не монолитны. Это наборы сервисов, общающихся друг с другом. Фронтенд говорит с бэкендом. Бэкенд говорит с базой данных. Промежуточное ПО расположено между ними и принимает решения.

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

Но вот что меня заинтересовало: очереди не видят полную картину. Они видят только то, на что настроены. А настройку делают люди. Люди, которые ошибаются. Люди, которые забывают об исключениях. Люди, которые деплоят в пятницу вечером и что-то упускают.

Первая победа

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

Я понимал, что они будут использовать виртуальную очередь. К тому времени ее использовали все. Но я был готов.

За две недели до дропа я заходил на их сайт каждый день. Не для того, чтобы смотреть вещи, а чтобы изучить его. Я отслеживал каждый сетевой запрос. Документировал каждый эндпоинт. Построил карту их цифровой инфраструктуры.

Очередь была настроена на срабатывание по URL-адресам страниц продуктов: /products/collab-*

Стандартная настройка с разумной защитой. Но я заметил кое-что еще. На их сайте была функция списка желаний (wishlist). И у этого списка был собственный API:

POST /api/wishlist/add{"product_id": "collab-hoodie-black", "size": "L"}

Добавление чего-либо в список желаний не должно вызывать очередь. Это пассивное действие. Вы ничего не покупаете. Но у их API списка желаний была одна интересная деталь. Если товар был в наличии, а вы были авторизованы с сохраненными платежными данными, в ответе возвращался quick_buy_token.

Этот токен был действителен для прямой оплаты. Никакого посещения страницы товара не требовалось. Никакого триггера очереди.

В день релиза я запустил скрипт, который опрашивал API списка желаний каждые 200 миллисекунд. В тот самый момент, когда товары появились в продаже, у меня уже были токены для оформления заказа — еще до того, как система очереди поняла, что товары доступны.

Я получил худи своего размера. Мои руки тряслись, когда я получил письмо с подтверждением.

Усвоенный урок: Очереди защищают конкретные пути. Любой незащищенный эндпоинт — это потенциальный короткий путь.

Погружение

Тот первый успех опьянял. Но мне просто повезло — я наткнулся на ошибку конфигурации, специфичную для одного конкретного сайта.

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

И я обратился к queue-it — это провайдер систем очередей, который публикует обширную документацию. Они подробно объясняют, как работает их система: триггеры, валидация, управление токенами, всё.

Большинство людей, которые стоят в виртуальных очередях никогда не читают подобное. Я прочитал ее трижды.

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

Распространенные типы триггеров:

  • Сопоставление шаблонов URL (URL pattern matching) — очередь активируется при посещении определенных страниц.

  • Выполнение JavaScript — клиентский код перенаправляет вас в комнату ожидания.

  • Наличие файлов Cookie — определенные cookies указывают на то, что вас нужно поставить в очередь.

  • Инспекция заголовков (Header inspection) — перехват вызывают определенные заголовки запроса.

  • Ограничение частоты запросов (Rate limiting) — слишком большое количество запросов за короткое время отправляет вас в очередь.

У каждого типа этих триггеров есть слабые места. И каждое слабое место можно использовать.

Заблуждение о JavaScript

Многие реализации очередей полагаются на JavaScript.

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

Логика выглядит примерно так:

(function() {    if (!hasValidQueueToken()) {        window.location.href = '/queue/waiting-room';    }})();

Просто и эффективно, но ошибочно.

Потому что этот код выполняется на вашей машине. В вашем браузере. Под вашим контролем.

Я научился перехватывать JavaScript до его выполнения. Расширения для браузера. Прокси-инструменты. Иногда просто полностью отключал JavaScript и смотрел, что будет.

Одна билетная платформа отдавала всю страницу мероприятия с проверкой очереди в JavaScript. Отключаешь скрипты — получаешь сырую страницу вместе с работающими кнопками «Купить билеты».

И кнопки реально работали. Почему? Потому что интеграция очереди была сделана лениво. Разработчики повесили красивую “дверь” в виде JS-скрипта на витрине, но забыли поставить “охранника” на складе базы данных.

Да, они валидировали саму транзакцию на стороне сервера. Но на бэкенде не было ни единой строчки кода, которая бы проверяла: а есть ли у этого покупателя токен прохождения очереди? Очередь была чисто косметической. Это классическая архитектурная ошибка — перекладывание критической логики безопасности на сторону клиента, где пользователь является царем и богом.

В том месяце я купил билеты на три концерта, используя лишь опцию «Отключить JavaScript» в Firefox.

Усвоенный урок: Безопасность на стороне клиента — это не безопасность. Это театр.

Забытые эндпоинты

Современные веб-приложения — это своего рода археологические раскопки.

Слой за слоем можно снимать код, накапливавшийся годами. Новые функции строятся поверх старой инфраструктуры. API объявляются устаревшими, но не всегда отключаются. Эндпоинты, о существовании которых уже давно никто не помнит.

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

  1. Их текущий чекаут на React (защищенный очередью).

  2. Устаревший чекаут на PHP из 2018 года (защищенный частично).

  3. Древний чекаут для мобильного веба из 2015 года (совершенно незащищенный).

Мобильный чекаут все еще функционировал. Он принимал те же ID продуктов, те же платежные токены, всё то же самое. Но просто находился по другому URL-адресу, который никто не догадался защитить.

Текущий: checkout.example.com/cartУстаревший: example.com/checkout.phpДревний: m.example.com/buy.php

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

Никаких очередей. Никакого ожидания. Прямой доступ к системе обработки заказов.

Этот паттерн повторяется повсеместно. Компании растут. Кодовые базы расширяются. Меры безопасности применяются к новому коду, но не применяются ретроактивно к старому.

Усвоенный урок: Всегда ищите призраков из прошлого. Устаревшие системы преследуют современные приложения.

Игра в наперстки с сессиями

Системам очередей нужно отслеживать, кто находится в режиме ожидания, а кто уже прошел валидацию. Они делают это с помощью токенов — уникальных идентификаторов, хранящихся в cookies или локальном хранилище (local storage).

Интересный вопрос: что именно доказывают эти токены? В теории, токен доказывает, что вы дождались своей очереди. На практике, токены часто доказывают гораздо меньшее.

Я сталкивался с системой, где логика защиты была реализована до смешного халатно. Разработчики вынесли генерацию токена на сторону клиента, а саму “секретную соль” случайно оставили в публичном бандле JavaScript. В другой системе вообще не заморачивались с криптографией и просто кодировали параметры сессии с помощью предсказуемого Base64.

token = MD5(session_id + timestamp + "secret_salt")

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

Другая система использовала последовательные ID токенов. Первый человек в очереди получал токен 1. Второй человек — токен 2. И так далее. Валидация проверяла, был ли номер вашего токена меньше номера “текущего обслуживаемого”. Если текущий обслуживаемый был 5000, валидными считались токены 1-4999.

При такой логике можно просто взять любой токен от 1 до 4999 и получить немедленный доступ.

Усвоенный урок: Системы токенов надежны ровно настолько, насколько надежна логика их генерации и валидации. И то, и другое часто имеет изъяны.

Гонка со временем

Эта техника требует точности, но она поразительно эффективна.

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

Это называется “окно внедрения” (inception window).

Во время крупного релиза электроники я неделями изучал их паттерны деплоя. Товары стабильно появлялись в продаже за 2-3 секунды до того, как на их страницах активировалась очередь. Миллисекунды имеют значение. А 2-3 секунды — это целая вечность.

Я настроил скрипты так, чтобы они били по их API добавления в корзину (add-to-cart API) ровно в тот момент, когда товары появлялись в их системе инвентаризации, — что я мог отследить через отдельный, незащищенный эндпоинт проверки остатков.

Мои запросы прилетали прямо в “окно внедрения”. К тому моменту, когда очередь активировалась, мои товары уже лежали в корзине.

Эта техника требует:

  • Ювелирной точности.

  • Заранее подготовленных сессий с сохраненными платежными данными.

  • Стремительного выполнения (часто менее секунды).

  • Резервирования (множество попыток, множество сессий).

Это срабатывает не всегда. Но это работает.

Усвоенный урок: У систем защиты есть задержка при запуске. Скорость убивает очереди.

Географическая лотерея

Глобальные компании управляют глобальной инфраструктурой. А глобальную инфраструктуру трудно поддерживать в синхронизированном состоянии.

Я обнаружил это во время релиза кроссовок, который стартовал одновременно в нескольких регионах. На сайте в США стояла жестокая очередь — время ожидания составляло два часа. Британский сайт показывал ту же картину.

Но австралийский сайт? Пятнадцатиминутная очередь. Минимальная конкуренция. Те же самые товары.

Еще лучше: на их японском сайте очереди не было вообще. Они планировали ее внедрить, но не закончили развертывание. Я совершил покупку через Японию. Отправил на сервис форвардинга посылок. Через десять дней кроссовки были у меня в руках.

Такое случается постоянно. Развертывание систем безопасности происходит поэтапно. Маркетинговые команды забывают скоординироваться с инженерами. Региональные вариации проскальзывают сквозь щели.

Усвоенный урок: Всегда проверяйте альтернативные регионы. Глобальный — не значит единообразный.

Трюк с реферером

Это вообще самое простое.

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

Реферер — это просто HTTP-заголовок. Вы полностью его контролируете.

Referer: https://www.example.com/homepage

Я нашел крупного ритейлера, чья очередь срабатывала только в том случае, если реферер был пустым или от внешнего источника. Если ваш реферер показывал, что вы перешли по внутренней ссылке их сайта, вы полностью обходили очередь.

Одна-единственная модификация заголовка. Это все, что потребовалось.

Побег из комнаты ожидания

Комнаты ожидания — это веб-страницы. У них есть JavaScript. Они опрашивают серверы. Они обновляют вашу позицию. И иногда они сами по себе являются уязвимостью.

Одна из изученных мной реализаций опрашивала эндпоинт статуса каждые пять секунд:

GET /queue/status?token=xyzResponse: {"position": 23481, "ready": false}

Когда статус ready становился true, JavaScript перенаправлял вас на защищенную страницу с токеном завершения. Токен завершения генерировался на стороне сервера. Но логика перенаправления была клиентской.

Что произойдет, если вы сами отправите запрос на эндпоинт завершения, не дожидаясь статуса ready?

GET /queue/complete?token=xyz

Сервер проверил, валиден ли мой токен. Он не проверил, действительно ли я отстоял очередь. Он просто сгенерировал сертификат завершения и пропустил меня. Очередь валидировала формат токена, а не факт прохождения очереди.

Усвоенный урок: Всегда исследуйте выход, а не только вход.

Теневая корзина

Эта техника требует терпения и планирования, но она практически незаметна.

Большинство платформ электронной коммерции позволяют добавлять товары в корзину до того, как они официально “выпущены”. Товар существует в базе данных. Система корзины его принимает. Его просто нельзя оплатить до времени релиза.

Я научился определять ID товаров до официальных анонсов — через паттерны URL, перебор API и даже метаданные в промо-изображениях.

За несколько дней до дропа я добавлял товары в свою корзину прямыми вызовами API:

POST /api/cart/add{"product_id": "unreleased-item-2024", "quantity": 1}

Корзина их принимала и товар лежал в ней и ждал.

Когда наступало время релиза, все остальные начинали свой путь: посетить страницу, подождать в очереди, добавить в корзину, оформить заказ. Я переходил сразу к оформлению заказа. Моя корзина была загружена заранее. Очередь защищала страницу товара — а не ту корзину, которую я собрал несколько дней назад.

Справедливости ради, сегодня этот фокус пройдет далеко не везде. Современные E-commerce гиганты делают жесткую проверку наличия именно в момент финальной транзакции. Но тогда я наткнулся на систему, где статус “доступно для продажи” проверялся только на этапе добавления в корзину. Бэкенд на кассе слепо доверял тому, что уже лежало у меня в виртуальной тележке. 

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

Измерение WebSockets

HTTP — не единственный протокол.

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

Системы очередей мониторят HTTP-трафик. И часто они полностью игнорируют WebSockets.

Я нашел платформу по продаже билетов, где выбор мест происходил через WebSocket-соединения. Очередь защищала HTTP-эндпоинт для просмотра доступных мест. Но как только у вас устанавливалось WebSocket-соединение, вы могли выбирать и бронировать места напрямую.

{"action": "select_seat", "event": "concert-123", "seat": "A-15"}{"response": "seat_held", "hold_token": "abc123", "expires": 300}

Этот токен брони был действителен для оформления заказа. Никакой очереди не требовалось. Фокус состоял в том, чтобы установить WebSocket-соединение заранее — во время окна предпродажи, до активации очереди. А затем поддерживать его до момента релиза.

Соединение установлено. Места выбраны. Очередь пройдена.

Усвоенный урок: Разные протоколы, разные уровни защиты. WebSocket — это часто слепая зона.

Моральный груз

Но нужно кое с чем здесь разобраться. У этих методов есть последствия.

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

Само по себе знание нейтрально: понимание того, как работают системы, по своей сути не несет вреда. Но применение имеет значение. Я использовал эти методы для личных покупок, которые мне действительно были нужны. Я также делился знаниями, которые другие превратили в оружие ради прибыли.

Я не могу “развидеть” то, что знаю. Но я могу быть честным в отношении последствий. Очередь существует не просто так. Это несовершенная попытка достичь справедливости. Взлом этой системы служит моим интересам за счет других.

Что на самом деле это останавливает

Для инженеров по безопасности, читающих это, — вот что работает:

  • Валидация на стороне сервера для всего. Каждое важное решение должно приниматься на бэкенде. Ни в чем не доверяйте клиенту.

  • Полное покрытие эндпоинтов. Составьте карту собственной поверхности атаки. Защищайте всё, а не только очевидные пути.

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

  • Обнаружение аномалий в реальном времени. У людей есть паттерны поведения. У ботов есть паттерны поведения. Это разные паттерны. Обнаруживайте их.

  • Защита, независимая от протокола. HTTP, WebSocket, GraphQL — логика очереди должна применяться единообразно ко всем каналам связи.

  • Аудит устаревших систем. Найдите своих призраков. Отключите или защитите их.

  • Глобальная верификация развертывания. Автоматические проверки того, что меры безопасности согласованы во всех регионах и на всех серверах.

Заключение

Три года взлома очередей научили меня тому, чему не научит ни один учебник.

Я узнал, что безопасность — это иллюзия, поддерживаемая по обоюдному согласию. Системы доверяют вам соблюдение правил. А когда вы их не соблюдаете, они зачастую не замечают разницы.

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

Я узнал, что документация — это сила. Ответы спрятаны на самом видном месте: в документации к API, в руководствах по фреймворкам, в собственных технических статьях провайдеров очередей. Читайте всё.

Я узнал, что время решает больше, чем что-либо еще. Зазор между деплоем и включением защиты. Миллисекунды до активации очереди. Окно между анонсом и запуском. Скорость — это ультимативный обход.

Самое главное: я узнал, что понимание систем меняет ваши взаимоотношения с ними. Вы перестаете быть пассивным пользователем и становитесь активным участником. Вы видите строительные леса за красивым фасадом.

ссылка на оригинал статьи https://habr.com/ru/articles/1024416/