§5.5. Внутри сервера

Внутреннее устройство веб-серверов может различаться в зависимости от их предназначения: одни отдают файлы и простые странички, другие — формируют профили в социальной сети или даже оформляют результаты сложных вычислений. Мы рассмотрим самые популярные случаи и выясним, какие уязвимости у них бывают.

Просто файлы

С момента создания веба очень долгое время он существовал как набор статичных страничек на разных серверах. Большие компании разрабатывали сложные интерактивные сайты, но большинство пользователей, если и создавали свои страницы, просто писали на HTML и выкладывали полученное на сервер своего интернет-провайдера (да-да, интернет-провайдеры часто предоставляли почту и хостинг). Кроме того, HTTP используется для передачи изображений, видео и другого контента. Такой контент мало отличается по своей сути от статических HTML-страниц. Для раздачи файлов, а также первичной обработки HTTP-запросов существуют программы, называемые веб-серверами. В простейшей реализации веб-сервер просто отдаёт файлы, хранящиеся в указанной директории. Конечно, современные веб-серверы умеют гораздо больше — кэшировать данные, отдавать файлы частично, проверять логины и пароли пользователей. Пример такого сервера — nginx.

Уже здесь могут возникать уязвимости. Например, предположим, что сервер был настроен на выдачу файлов из директории /srv/http/root. Мы уже знаем, что с помощью .. в пути обозначается переход на уровень выше. Например, /home/test/../other — это другой способ сослаться на /home/other. Сервер берёт путь, который прислал пользователь (например, для http://example.com/images/cat.jpg это images/cat.jpg), и отдаёт файл по этому пути. В таком случае мы можем отправить запрос с путём ../../../etc/passwd. Сервер соберёт путь /srv/http/root/../../../etc/passwd, и мы получим файл /etc/passwd. В этом файле традиционно хранятся записи о пользователях в UNIX. Такая уязвимость называется AFR (англ. Arbitrary File Reading — произвольное чтение файлов). Этот класс уязвимостей известен уже много лет, и тем не менее в 2018 году в nginx была найдена именно такая проблема.

Обратите внимание, что браузер может ещё до отправки запроса убирать ../ из пути. Чтобы отправить такой запрос, можно воспользоваться curl. Ему нужно передать флаг --path-as-is, чтобы путь не исправлялся.

Страницы в динамике

Веб-серверы обычно берут часть забот разработчиков приложения на себя — они принимают запросы, в случае HTTPS расшифровывают их, проверяют на корректность, самостоятельно передают статические файлы, а все остальные запросы перенаправляют другому, внутреннему серверу. При этом внутренний сервер может генерировать HTML-страницу по запросу клиента — таким образом, разным пользователям может демонстрироваться разное содержимое. Кроме того, веб-сервер может не перенаправлять запрос, а напрямую запускать программу, которая сгенерирует веб-страницу для пользователя.

Из-за неправильной настройки сервера приложениий могут возникать различные проблемы — потеря производительности, неэкономное использования ресурсов сервера, внезапно остановленные экземпляры приложения. Мы не будем подробно останавливаться на этих особенностях: они зависят от конкретных технологий, используемых в приложении.

Современные веб-сервисы нередко вообще не используют генерацию веб-страниц. Вместо этого страница, используя JavaScript, сама запрашивает нужные данные с сервера и меняет собственный вид. Для передачи данных с сервера на браузер чаще всего используется формат данных JSON (англ. JavaScript Object Notation, язык объектов JavaScript) — это простой формат для передачи структурированных записей. Вот пример:

{
  "first name": "John",
  "last name": "Smith",
  "age": 25,
  "address": {
    "street address": "21 2nd Street",
    "city": "New York",
    "state": "NY",
    "postal code": "10021"
  },
  "phone numbers": [
    {
      "type": "home",
      "number": "212 555-1234"
    },
    {
      "type": "fax",
      "number": "646 555-4567"
    }
  ],
  "sex": {
    "type": "male"
  }
}

SQL и маленькая кавычка

Так уж повелось, что практически любой веб-сервис можно разделить на одинаковые составные части.

Веб-приложение делится на

  • фронтенд (англ. frontend) — совокупность HTML, CSS и JavaScript, то есть то, что отображает ваш браузер;

  • бэкенд (англ. backend) — приложение, формирующее веб-страницу, или отвечающее JSON-объектами на запросы;

  • базу данных — хранилище, в котором содержат записи о пользователях, их сообщения, настройки, заказы — в общем, всё, что сохраняет веб-приложение.

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

Вся ценная информация, конечно же, хранится в базе данных, которая является целью любого злоумышленника. На протяжении многих лет создания приложений выделился целый класс атак, которые разработчики игнорируют с завидной регулярностью — инъекции. О них мы поговорим подробнее.

Уязвимости на основе инъекций возникают там, где какая-либо команда составляется простым сложением строк. Предположим, что вы составляете фразу на русском языке «Получить текст всех сообщений, где содержится слово X.» Вы позволяете пользователю передавать то, что записывается в X (например, «погода»). Однако, злоумышленник может запросить нечто другое, передав строку «„погода“, и удалить все данные». После подстановки выйдет: «Получить текст всех сообщений, где содержится слово „погода“, и удалить все данные.». Уже не так безобидно, правда?

Самым популярным видом баз данных остаются реляционные базы данных, первые реализации которых появились ещё в 70-х годах XX века. В таких базах все данные хранятся в таблицах с заданными колонками, и связываются друг с другом ссылками. Для запросов в такие базы создали язык запросов SQL. Вот пример выражения на нём:

SELECT first_name, last_name, password FROM users WHERE id = 4;

Это означает: «верни поля first_name, last_name и password из таблицы users для всех записей, у которых поле id равно 4». В языке SQL есть классические арифметические операторы +, -, * и /, логические операторы AND и OR и многое другое — в каком-то смысле это настоящий язык программирования. Приведём пример запроса посложнее:

SELECT name FROM customers WHERE city = 'Moscow' AND debit + credit > 0;

Простота использования SQL привела к его популярности среди разработчиков. Особо нерадивые из них составляют SQL-запросы простым сложением строк, оставляя уязвимость — SQL-инъекцию. Предположим что есть сайт, поиск на котором реализован внутри следующим образом:

# Получить строку, которую хочет найти пользователь.
search_string = request.get_parameter("search_string")
# Получить содержимое постов, в которых содержится фраза из запроса пользователя.
posts_content = run_query("SELECT content FROM posts WHERE content ~ '" + search_string + "'")
# Выдать результат пользователю.
return posts_content

Где же именно скрыта уязвимость? Заметьте, что строка search_string приходит от пользователя, и никак не проверяется. Сначала исследователь находит такую уязвимость, пробуя писать в текстовые поля одинарную кавычку '. В таком случае конечный запрос примет вид: SELECT content FROM posts WHERE content ~ '''. Три одинарные кавычки подряд — это синтаксическая ошибка в языке SQL, и сервер скорее всего вернёт ошибку.

Найдя уязвимость, нужно построить такой запрос, который вернёт искомые данные. Конкретный пример будет зависеть от используемой базы данных и исходного запроса внутри приложения, которые нам неизвестны. Однако существует несколько универсальных хитростей, которые следует помнить.

Первая из них — это комментарий. В запросах на языке SQL можно оставлять комментарии с помощью конструкции -- . Она помечает весь текст, идущий от неё и до конца строки, как комментарий, который игнорируется базой. Например:

-- Вот такой вот запрос:
SELECT "user_name" FROM "users" WHERE "is_active"; -- вернуть имена активных пользователей

Обычно комментарии направлены на благое дело — документировать написанные запросы. Однако мы также можем использовать их в инъекциях. Заметим, что в запросе выше после строки пользователя будет добавлена одинарная кавычка. Как избавиться от неё? Можно добавить комментарий в конце строки, например погода' -- . Тогда весь запрос получится такой: SELECT content FROM posts WHERE content ~ 'погода' -- ', и успешно выполнится. Таким образом мы можем дописывать произвольный код в конце запроса, не беспокоясь о проблемах из-за кавычки.

Вторая хитрость — это конструкции LIMIT и UNION. Вместе они позволяют нам полностью поменять смысл запроса. Конструкция LIMIT устанавливает ограничение на количество строк, возвращаемых запросом. Eсли же поставить LIMIT 0, то ни одна запись из таблицы возвращена не будет. Например, если злоумышленник напишет в строке поиска ' LIMIT 0 -- , то результат будет такой: SELECT content FROM posts WHERE content ~ 'погода' LIMIT 0 -- '. Этот запрос будет успешным, однако не вернёт результата — как будто мы ничего не нашли. Отключить так исходный запрос бывает необходимо, если результат ожидается лишь один, и просто полезно чтобы просматривать результат было проще.

Конструкция UNION позволяет объединять два несвязанных запроса и возвращать результаты обоих вместе. Например:

SELECT city FROM customers -- получить города всех клиентов
UNION
SELECT City FROM suppliers; -- и вместе с ними города всех поставщиков

С помощью этой конструкции мы заставим веб-приложение выполнить вредоносное действие — вернуть в качестве текста поста пароль администратора сайта. Для этого в строке поиска нужно ввести ' LIMIT 0 UNION SELECT password FROM users WHERE is_admin = 1 -- . Итоговый вредоносный запрос будет выглядеть так:

SELECT content FROM posts WHERE content ~ '' LIMIT 0 UNION SELECT password FROM users WHERE is_admin = 1 -- '

Вуаля, мы получаем секретную информацию прямо там, где обычно находится текст свежих статей ведущих исследователей погодных явлений!

Мы рассмотрели самое простое применение SQL-инъекций. К сожалению, эта тема очень обширна и всё рассказать в рамках этого курса тяжело. Например, мы не рассмотрели, как получить список всех таблиц в базе данных. Также бывает полезным узнать, какая именно база данных используется — это облегчает дальшейшее построение запроса. Чтобы лучше разбираться в этом, стоит начать с изучения самого языка SQL.

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

Выводы

  1. Файлы и статические веб-страницы браузеру отдаёт веб-сервер.

  2. Веб-сервера могут быть уязвимы. Одна из известных возможных уязвимостей — недостаточная проверка пути, который запрашивает пользователь. Ваши пароли могут утечь благодаря специальной директории ...

  3. Веб-приложение состоит из фронтенда (самой страницы), бэкенда (приложения на сервере) и базы данных (хранилища информации).

  4. Самый распространённый вид базы данных — SQL-совместимые. Такие базы хранят всё в табличках со ссылками друг на друга.

  5. Инъекции — это класс уязвимостей, возникающих из-за выполнения команд, которые собираются сложением строк.

  6. SQL-инъекции в частности — это инъекции кода на SQL через приложение в базу данных. Кавычка ' помогает взламывать целые сайты.

Задача А. Безопасный банк ⟶