Какие предположения можно сделать относительно следующего HTTP ответа сервера?
Глядя на этот небольшой фрагмент HTTP ответа, можно предположить, что веб-приложение, вероятно, содержит уязвимость XSS.
Почему это возможно? Что обращает на себя внимание в этом ответе сервера?
Вы будете правы, если сомневаетесь насчет заголовка Content-Type. В нем есть незначительный недостаток - отсутствие атрибута charset. Это может казаться неважным, однако, в этой статье мы объясним, как злоумышленники могут использовать этот недостаток для внедрения произвольного JavaScript кода на веб-сайт, сознательно изменяя набор символов, который ожидает браузер.
Кодировка символов
Типичный заголовок Content-Type в HTTP ответе выглядит следующим образом:
Атрибут charset сообщает браузеру, что для кодирования тела HTTP ответа использовалась кодировка UTF-8. Кодировка символов, такая как UTF-8, определяет соответствие между символами и байтами. Когда веб-сервер обрабатывает HTML документ, он соотносит символы документа с соответствующими байтами и передает их в теле HTTP ответа. Этот процесс преобразует (кодирует) символы в байты:
Когда браузер получает эти байты в теле HTTP ответа, он может преобразовать их обратно (декодировать) в символы HTML документа:
UTF-8 является одним из многих способов кодирования набора символов, которые современные браузеры обязаны поддерживать в соответствии со спецификацией HTML. Также существуют множество других алгоритмов, вроде UTF-16, ISO-8859-xx, windows-125x, GBK, Big5, и т.д. Знание того, какую кодировку использовал сервер, является критически важным для браузера, поскольку без этого он не сможет правильно декодировать байты в теле HTTP ответа.
Но, что произойдет, если атрибут charset не указан в заголовке Content-Type или он указан неверно?
В таком случае браузер будет искать тэг в самом HTML документе. Этот тэг также может содержать атрибут charset, который определяет кодировку символов (<meta charset="UTF-8">). Это правило для браузера: для чтения HTML документа, необходимо сначала декодировать тело HTTP ответа. Таким образом, браузеру необходимо сначала сделать предположение о некотором способе кодирования тела HTTP ответа, потом декодировать его, найти тег и, возможно, повторно декодировать байты с указанной кодировкой символов.
Другим, менее распространенным подходом к указанию кодировки символов является указание порядка байтов Byte-Order Mark (BOM). Это специальный символ Unicode (U+FEFF), который может быть помещен перед строкой для указания порядка байтов и кодировки символов. В основном он используется в файлах, но поскольку эти файлы могут передаваться через веб-сервер, современные браузеры поддерживают этот способ. Метка порядка байтов в начале HTML документа даже имеет приоритет над атрибутом charset в заголовке Content-Type и тэге <meta>.
Подводя итог, существуют три распространенных способа, которые браузер использует для определения кодировки символов HTML-документа, упорядоченных по приоритету:
-
Метка порядка байтов в начале HTML документа,
-
атрибут charset в заголовке Content-Type,
-
тэг <meta> в HTML документе.
Отсутствие информации о кодировке
Метка порядка байтов обычно используется очень редко, а атрибут charset не всегда присутствует в заголовке Content-Type или может быть указан неверно. Кроме того, особенно для частичных HTML-ответов от сервера, обычно отсутствует тег <meta>, указывающий кодировку символов. В этих случаях браузер не располагает никакой информацией о том, какой набор символов следует использовать:
Вы когда-нибудь видели подобное сообщение об ошибке? Вероятно, нет, потому что его не существует.
Как и в случае с неправильным синтаксисом HTML, браузеры пытаются восстановить недостающую информацию о наборе символов при анализе полученного от веб-сервера контента, и извлечь из этого максимум возможного. Такое нестрогое поведение способствует хорошему взаимодействию с пользователем, но также может открыть возможности для использования таких техник эксплуатации, как mXSS.
При отсутствии информации о кодировке символов браузеры пытаются сделать предположение, основываясь на содержимом, что называется автоматическим определением (auto-detection). Это похоже на поиск по типу MIME (MIME-type sniffing), но работает на уровне кодировки символов. Например, движок рендеринга Chromium Blink использует библиотеку Compact Encoding Detection (CED) для автоматического определения кодировки символов. Как мы увидим дальше, с точки зрения злоумышленника, функция автоматического определения кодировки символов является очень мощным инструментом.
На данный момент мы познакомились с различными механизмами, которые используют браузеры для определения кодировки символов HTML документов. Но как злоумышленники могут это использовать?
Различия в кодировках
Цель кодирования символов - преобразовать символы в последовательность байтов, обрабатываемую компьютером. Эти байты передаются по сети и декодируются обратно в символы получателем. Таким образом, восстанавливаются все те же символы, которые хотел передать отправитель:
Это работает хорошо только в том случае, когда отправитель и получатель согласовали способ кодировки символов. Если есть несоответствие между кодировкой символов, используемой для кодирования и декодирования, то получатель может увидеть разные символы:
Подобное несоответствие между кодировкой символов, используемой для кодирования и декодирования, и есть то, что мы называем различием в кодировках в этой статье.
Для веб-приложений жизненно важно, когда контролируемые пользователем данные очищаются для предотвращения уязвимостей Cross-Site Scripting (XSS). Если кодировка символов, используемая браузером, отличается от той, которую использует веб-сервер, это теоретически может нарушить очистку данных и привести к уязвимостям XSS.
Само по себе это не является большой новостью, и даже Google сталкивался с подобной проблемой еще в 2005 году. Страница Google 404 не предоставляла информации о кодировке символов, что могло быть проэксплуатировано через добавление полезной нагрузки XSS в кодировке UTF-7, В UTF-7 специальные символы HTML, такие как угловые скобки, кодируются иначе, чем в ASCII, что может быть использовано для обхода процедуры санитизации:
Это наглядно демонстрирует опасность данной кодировки, которая в последующие годы была признана устаревшей для предотвращения подобных проблем с безопасностью. В настоящее время спецификация HTML явно запрещает использование UTF-7 для предотвращения уязвимостей XSS.
Существует множество других поддерживаемых кодировок символов, большинство из которых на самом деле бесполезны с точки зрения злоумышленника. Все специальные символы HTML, такие как угловые скобки и кавычки, используются только в формате ASCII, и поскольку большинство кодировок символов совместимы с ASCII, то для этих символов нет разницы в используемой кодировке. Даже для UTF-16, который не совместим с ASCII из-за использования двух байт на символ, обычно невозможно эксплуатировать ASCII-символы, поскольку их соответствующее байтовое представление такое же, только с нулевым байтом в конце (little-endian) или в начале (big-endian).
Однако существует особенно интересная кодировка: ISO-2022-JP.
ISO-2022-JP
ISO-2022-JP - это японская кодировка символов, определенная в RFC 1468. Это одна из официальных кодировок символов, которую должны поддерживать пользовательские агенты (браузеры), согласно определению стандарта HTML. Особенно интересным в этой кодировке является то, что она поддерживает определенные escape-последовательности для переключения между различными наборами символов.
Например, если последовательность байтов содержит байты 0x1b, 0x28, 0x42, то эти байты не декодируются в символ, а вместо этого указывают, что все следующие байты должны быть декодированы с использованием кодировки ASCII. В общей сложности существует четыре различных escape-последовательности, которые можно использовать для переключения между наборами символов: ASCII, JIS X 0201 1976, JIS X 0208 1978 и JIS X 0208 1983.
Эта особенность стандарта ISO-2022-JP предоставляет не только большую гибкость, но и может нарушить фундаментальные допущения. На момент написания статьи Chrome (Blink) и Firefox (Gecko) автоматически определяли эту кодировку. Появление одной из этих escape-последовательностей обычно достаточно, чтобы алгоритм автоматического определения кодировки считал, что текст HTTP-ответа закодирован в соответствии со стандартом ISO-2022-JP.
В следующих разделах описываются две различные техники эксплуатации, которые могут использовать злоумышленники, когда им удается заставить браузер использовать кодировку ISO-2022-JP. В зависимости от возможностей злоумышленника, это может быть достигнуто, например, путем прямого управления атрибутом charset в заголовке Content-Type или путем вставки тега с помощью уязвимости HTML injection. Если веб-сервер предоставляет недопустимый атрибут charset или не предоставляет его вообще, то обычно не требуется никаких дополнительных условий, поскольку злоумышленники могут легко переключить кодировку на ISO-2022-JP с помощью автоматического определения кодировки.
Метод 1: Устранение экранирования обратной косой черты
Данный метод заключается в том, что контролируемые пользователем данные помещаются в строку JavaScript:
Представим, что есть некий веб-сайт, который принимает два параметра запроса, search и lang. Первый параметр отображается в обычном текстовом контексте, а второй параметр (lang) вставляется в строку JavaScript:
Специальные символы HTML в параметре search закодированы в HTML, а параметр lang обработан путем экранирования двойных кавычек (") и обратной косой черты (). Это не позволит выйти за контекст строки и внедрить JavaScript код:
Режимом по умолчанию для стандарта ISO-2022-JP является ASCII. Что означает, что все байты, полученные в теле HTTP ответа, декодируются в ASCII и результирующий HTML документ выглядит так, как мы ожидаем:
Теперь давайте представим, что злоумышленник внедряет в параметр search escape-последовательность для переключения в режим кодировки символов JIS X 0201 1976 (0x1b, 0x28, 0x4a):
Как мы видим, результат содержит все те же символы, что и ранее, поскольку кодировка JIS X 0201 1976 в основном совместима с ASCII. Однако, если мы внимательно изучим его кодовую таблицу, то заметим, что есть два исключения (выделены желтым цветом):
Байт 0x5c преобразуется в символ йены (¥), а байт 0x7e - в символ надчеркивания (‾). Это отличается от ASCII, где 0x5c сопоставляется с символом обратной косой черты (), а 0x7e - с символом тильды (~).
Это означает, что когда веб-сервер пытается экранировать двойную кавычку в параметре lang с помощью обратной косой черты, браузер больше не видит обратную косую черту, а вместо нее видит знак йены:
Соответственно, добавленная двойная кавычка фактически обозначает конец строки и позволяет злоумышленнику внедрить произвольный JavaScript-код:
Хотя этот способ довольно эффективен, он ограничен обходом санитизации только в контексте JavaScript, поскольку символ обратной косой черты не имеет специального предназначения в рамках HTML. Следующий раздел объясняет более продвинутый способ, который может быть использован в контексте HTML.
Метод 2: Нарушение контекста HTML
Сценарий использования второго метода заключается в том, что злоумышленник может управлять значениями в двух разных контекстах HTML. Наиболее распространенным вариантом использования может быть веб-сайт, поддерживающий разметку markdown. Например, рассмотрим следующий текст markdown:
![blue](0.png) or ![red](1.png)
Результирующий HTML-код выглядит следующим образом:
<img src="0.png" alt="blue"/> or <img src="1.png" alt="red"/>
Важным здесь является то, что злоумышленник может управлять значениями в двух разных контекстах HTML. В данном случае это:
-
Контекст атрибута (описание изображения/источник)
-
Обычный текстовый контекст (текст, окружающий изображения)
По умолчанию, ISO-2022-JP использует режим кодировки ASCII и браузер видит HTML-документ, как и ожидалось:
Теперь предположим, что злоумышленник подставляет escape-последовательность для переключения кодировки на JIS X 0208 1978 в описании первого изображения:
Это вынуждает браузер декодировать все следующие байты используя кодировку JIS X 0208 1978. Эта кодировка использует фиксированное количество из двух байт на символ и не совместима с кодировкой ASCII. Это фактически разрушает структуру HTML-документа:
Однако, вторая escape-последовательность может быть добавлена в текстовый контекст между обоими изображениями для переключения кодировки обратно в ASCII:
Таким образом, все следующие байты снова декодируются с использованием ASCII:
Однако, изучая код HTML, мы можем заметить, что кое-что изменилось. Начало второго тега img теперь является частью значения атрибута alt:
Причина этого в том, что 4 байта между двумя escape-последовательностями были декодированы с использованием JIS X 0208 1978, тем самым поглотив закрывающие двойные кавычки значения атрибута:
К этому моменту значение атрибута src второго изображения, в сущности, больше не является значением атрибута. Таким образом, злоумышленник может заменить это значение обработчиком ошибок JavaScript:
Заключение
В статье мы подчеркнули важность предоставления информации о кодировке символов при работе с HTML-документами. Отсутствие подобной информации может привести к серьезным уязвимостям класса XSS, когда злоумышленники могут изменить набор символов, который обрабатывается браузером.
Мы подробно описали, как браузер определяет набор символов, используемый для декодирования тела HTTP-ответа, и объяснили два различных метода, которые злоумышленники могут использовать для внедрения произвольного кода JavaScript на веб-сайт, используя кодировку символов ISO-2022-JP.
Хотя мы считаем отсутствие указания кодировки символов реальной уязвимостью, автоматическое определение кодировки символов браузером значительно увеличивает ее эффект. Поэтому мы надеемся, что браузеры отключат механизм автоматического определения кодировки в соответствии с нашим предложением - по крайней мере, для кодировки символов ISO-2022-JP.
Автор: Сергей