В статье исследуется одна из наиболее скрытых и потенциально опасных уязвимостей, которая из-за несоответствия в интерпретации данных может стать причиной различных проблем: от утечек информации до инъекций кода.
Парсер — это специальный компонент программного обеспечения, предназначенный для анализа входных данных (обычно текста) для выявления структуры. Основная задача парсера заключается в преобразовании данных в структурированную форму, чаще всего в дерево анализа или в набор связанных объектов, которые отражают различные элементы исходных данных. Этот процесс известен как парсинг или синтаксический анализ.
Парсеры находят широкое применение в различных областях информационных технологий. Например, в программировании они используются в компиляторах и интерпретаторах для преобразования исходного кода в машиночитаемую форму. В обработке естественных языков парсеры помогают анализировать текст на человеческом языке для понимания его грамматической структуры. В веб-разработке они применяются для обработки кода HTML, CSS и JavaScript. Парсеры также важны в области обработки данных для чтения и анализа различных форматов данных, а в сетевых технологиях они используются для интерпретации данных, передаваемых по сетевым протоколам.
Несоответствие парсеров возникает, когда две части кода пытаются проанализировать одни и те же данные, но расходятся во мнениях относительно того, что означают анализируемые входные данные. В общем, можно увидеть два вида поведения:
Довольно абстрактно. Давайте перейдем к конкретным примерам.
Проблема URL-адресов
Представьте, что у вас есть форум поддержки для веб-сайта, и вы хотите разрешить пользователям оставлять комментарии. Перед тем как сайт примет комментарий, он проверяет все содержащиеся в нём ссылки и отклоняет его, если какие-либо URL-адреса не включены в список разрешённых (возможно, из-за опасений риска спама или не по теме дискуссий). Затем комментарий отображается в браузере.
Кто-то пытается разместить ссылку на http://example[.]net\@github[.]com. Очевидно, что это неправильно сформированный URL. Согласно RFC 3986, в URL не допускается использование обратного слеша. Несмотря на это, можно было бы ожидать, что парсер прервёт схему на двоеточии «:», распознает «//» как начало имени домена и следующий «/» как его окончание, а «@» позволяет найти пользователя или хост. Таким образом, вы бы ожидали хост «github.com» и раздел «example.net».
Что происходит на самом деле?
Возможно, вы используете самый старый парсер URL в Java на своем сервере, java.net.URL, и его метод getHost. Он возвращает github.com, как и ожидалось. GitHub случайно оказывается в вашем списке разрешённых сайтов. Но посмотрите, что происходит в браузере: http://example[.]net\@github[.]com.
Если вы используете Firefox, Chrome или некоторые другие браузеры, вы окажетесь на сайте example.net, а не github.com. В результате атакующий может разместить ссылку, которая выглядит нормально для сервера (домен github[.]com), но которая ведёт на запрещённый домен (example[.]net) в некоторых основных браузерах.
Почему так происходит?
Браузеры исправляют URL, заменяя обратные слеши на обычные прямые слеши. Стоит отметить, что люди иногда путают прямые и обратные слеши при вводе URL-адресов. С полученным значением http://example[.]net/@github[.]com обратный слеш становится разделителем пути, а хостом является «example.net».
Итак. У нас есть два места кода (одно на сервере, другое в браузере), которые используют два разных парсера и которые будут интерпретировать некоторые входные данные по-разному. Это классическое несоответствие парсеров.
На практике такое поведение оказалось довольно серьёзной проблемой. Фактически, именно это несоответствие парсеров принесло Дэвиду Шютцу более $12 000 в виде вознаграждения за обнаружение ошибок от Google [1, 2], когда один конец несоответствия имел возможность предоставлять доступ ко всевозможным внутренним системам Google.
Также стоит отметить, что решение Google заключалось в попытке заставить их сервер вести себя более похоже на браузер, хотя поведение браузеров различается и может изменяться в любой момент. Их решение также было неполным, и Дэвиду удалось несколько раз его обойти, что в сумме принесло ему довольно крупное вознаграждение.
Небрежное разделение заголовков
В HTTP существует ряд заголовков, которые считаются многозначными, то есть они могут присутствовать несколько раз в ответе и могут быть объединены в одно поле заголовка путем разделения их значений запятыми.
Content-Type не должен быть многозначным, и должен содержать только один медиа-тип. Разделение по запятым не только нарушает спецификацию, но и искажает любой параметр в кавычках, который случайно содержит запятую.
Первое место в коде, прокси-сервер Imzy, понял значение заголовка image/foo, text/html как одно значение и проверил, начинается ли оно с image/. Поскольку это так, сервер разрешил ответ. Второе место в коде, браузер, рассмотрел запятую как разделитель значения для многозначного заголовка, и взял последнее значение.
Это было бы так, как если бы сервер сначала отправил Content-Type: image/foo, а затем Content-Type: text/html. Проксируемый ответ в данном случае был обработан как Content-Type: text/html, так как более позднее значение имело приоритет, и эксплойт был выполнен. Опять же, у нас есть входные данные, которые не соответствуют спецификации, и два разных парсера, которые обрабатывали их по-разному. Первый элемент одобрил входные данные, второй действовал на них, но с другим пониманием.
Также возможно, что сервер Imzy вместо этого разделил значения по запятым и взял первое значение, image/foo. Некоторые серверные программы неправильно обрабатывают множественные заголовки именно таким образом. Эффект тот же.
Также обратите внимание, что, как и в примерах с URL, первое место в коде действовало как страж, чтобы предотвратить злоупотребления во втором месте кода. Отправляя неправильно сформированные входные данные, атакующий может внедрить недопустимые или вредоносные данные, минуя стража. Такой паттерн поведения стража/злоумышленника очень распространен при несоответствии парсеров.
Контрабанда HTTP-запросов (HTTP Request Smuggling)
HTTP-запросы могут отправляться либо с заголовком Content-Length, указывающим заранее точное количество байтов для чтения после заголовков, либо могут указывать Transfer-Encoding, чтобы показать, что будет поток фрагментов, каждому из которых предшествует длина. Указание обоих в одном запросе является недопустимым и может привести к несогласованности между прокси-сервером и исходным сервером относительно границ внутри потока HTTP-запросов, которые отправляются через одно и то же постоянное соединение.
Из-за этого разногласия часть одного запроса может в конечном итоге стать частью другого, несвязанного с ним запроса, что позволит перехватить учетные данные, если этот другой запрос был аутентифицирован. Кэши могут быть взломаны. Вредоносные запросы могут обходить WAF (Web Application Firewall). Короче говоря, при обработке этих запросов могут произойти самые разные нежелательные последствия.
Здесь интересно то, что отравление кэша и перехват авторизации выходят за рамки того, что считается обычным шаблоном эксплойтов с несоответствием парсера (страж/злоумышленник). Но причина все та же: разногласия по поводу того, как анализировать необычные входные данные, приводят к уязвимости.
Это также подчеркивает, что HTTP — это то, что анализируется, несмотря на то, что люди называют HTTP «протоколом», а не «форматом». Не отвлекайтесь на эти классификации. Обработка HTTP требует анализа как формата заголовка в целом, так и значений конкретных заголовков. И даже после того, как все заголовки обработаны в структуры данных (например, многозначную карту), все, что использует проанализированные заголовки, все равно должно понимать их связь друг с другом.
JSON как подмножеств JavaScript
Первоначально JSON задумывался как подмножество JavaScript, которое не позволяло выполнять код, а только строить данные: строки, числа, массивы, карты и т. д. Это называется «Нотация объектов JavaScript» (JavaScript Object Notation, JSON). До того, как JSON.parse стал частью языка Javascript, разработчики иногда анализировали JSON, проверяя его корректность и затем передавая его функции в eval.
Однако JSON не был полностью подмножеством Javascript. Были некоторые странные особенности символов Юникода U+2028 'LINE SEPARATOR' и U+2029 'PARAGRAPH SEPARATOR', что приводило к исключениям.
Проблема связана с тем, когда определённый пробельный символ находится в середине того, что в противном случае было бы escape-последовательностью, например «\». При стандартном анализе JSON символ сохраняется, а в JavaScript символ удаляется, и кавычка теперь фактически завершает строку. Выполнение кода становится тривиальным.
Пара страж/злоумышленник на самом деле находится внутри парсера. json2.js представлял себя как парсер, но он полностью делегировал большую часть работы другому парсеру: сначала он запускал регулярное выражение, чтобы проверить, выглядит ли JSON действительным (защита), а затем оценивал JSON как JS (злоумышленник). Здесь несоответствие было между предположениями регулярного выражения о JS и тем, что JS на самом деле делает.
Что можно сказать на основе примеров об общих свойствах несоответствия парсера?
Последствия эксплойта:
Связь с другими классами уязвимостей
Небольшое замечание о двух других классах уязвимостей, которые имеют некоторое сходство с несоответствиями парсера, несмотря на то, что в других отношениях они сильно отличаются:
Вы также можете увидеть некоторые подклассы несоответствия парсеров с собственными названиями, так же как уязвимости инъекции делятся на XSS, SQL, разделение HTTP-заголовков и так далее. Единственный подкласс несоответствия парсеров, который мы видели до сих пор, это «путаница парсера URL» (URL parser confusion), но, вероятно, скоро появятся и другие. Возможно, рано или поздно появится название для плохой обработки многозначных HTTP-заголовков, но пока я такого не видел.
Теперь у вас есть четкое представление о том, что такое несоответствие парсера, и вы будете готовы распознать его при просмотре кода или попытке решить, как исправить уязвимость.
Сбалансированная диета для серого вещества