Несоответствие парсеров: как определить незаметное внедрение кода на сервер

Несоответствие парсеров: как определить незаметное внедрение кода на сервер

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

image

Что такое парсер?

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

Парсеры находят широкое применение в различных областях информационных технологий. Например, в программировании они используются в компиляторах и интерпретаторах для преобразования исходного кода в машиночитаемую форму. В обработке естественных языков парсеры помогают анализировать текст на человеческом языке для понимания его грамматической структуры. В веб-разработке они применяются для обработки кода 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 на самом деле делает.

Выводы

Что можно сказать на основе примеров об общих свойствах несоответствия парсера?

  • Уязвимость требует наличия двух парсеров, которые должны следовать одной и той же спецификации или, по крайней мере, двум очень похожим спецификациям, но на практике различаются. На практике спецификация может не быть четко определена, но если два парсера идентичны, то не имеет значения, насколько сильно они отклоняются от спецификации или как они заполняют пробелы. Даже при наличии хорошо написанных парсеров даже одно единственное отклонение от спецификации в одном из них, которое не совпадает с другим, может быть достаточным для создания уязвимости.
  • Определение «парсера» здесь более широкое, чем обычно. Если две системы по-разному понимают две структуры данных, это все равно является несоответствием парсера, точно так же, как если бы входные данные были строками.
  • Эксплуатация уязвимости более вероятна с входными данными, нарушающими спецификацию, но все же принимаемыми обоими парсерами. Или это могут быть входные данные, для которых спецификация не определяет поведение достаточно четко. (Ни один из предоставленных примеров не включает входные данные, соответствующие спецификации, и, хотя эксплуатация с использованием таких данных может быть возможна в некоторых случаях, это не является общим случаем.) В некотором смысле неоднозначные входные данные — оптические иллюзии для программного обеспечения.
  • Два местоположения кода часто представляют собой пару страж/злоумышленник, где первое место кода контролирует, выполнено ли второе место, в более общем смысле, находится «выше» в управлении.

Последствия эксплойта:

  • Следование спецификации, хотя это очень важно, не гарантирует безопасность;
  • Уязвимость, связанная с несоответствием парсеров, иногда может быть более правильно описана как уязвимость в интеграции, а не в каком-либо конкретно определенном пакете кода. Оба местоположения кода могут находиться в одном программном пакете или быть на разных языках программирования и поддерживаться разными людьми;
  • Следовательно, просмотр любого фрагмента кода не выявит окончательно уязвимость несоответствия парсера, а в лучшем случае может предположить ее возможность;
  • Эксплуатация уязвимости несоответствия парсеров похожа на программирование странной машины, так как атакующему необходимо провести реверс-инжиниринг парсеров, чтобы выяснить, что они реализуют на практике, а не то, что они должны были реализовать.
  • Следование части закона Постела (принцип надежности) «будьте либеральны в том, что вы принимаете» резко увеличивает вероятность уязвимости несоответствия парсеров, поскольку оно побуждает парсеры расширять то, что они принимают, неконтролируемым и нескоординированным образом.
  • Любой стандарт, который вводит грамматику, следует тщательно изучить на предмет неопределенных граничных случаев. Грамматики должны быть максимально простыми и компонуемыми, чтобы уменьшить вероятность разногласий между реализациями. Существует несколько RFC с разделом «Безопасность», где просто говорится «нет последствий для безопасности» после введения грамматики – такие спецификации на данный момент мы должны считать противоречивыми.

Связь с другими классами уязвимостей

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

  • От времени проверки до времени использования (TOCTOU) — класс уязвимостей «двух локаций», при котором отношения стража и злоумышленника плохо контролируются. При атаке TOCTOU злоумышленник использует временной интервал между проверкой ресурса и его использованием, изменяя состояние безопасности ресурса в этом временном интервале. Например, атакующий может изменить разрешения или содержимое файла после выполнения проверки безопасности, но до того, как к файлу получит доступ авторизованный пользователь.
  • Инъекции различных видов (SQL, XSS и т.д.) связаны только тем, что они сосредоточены на парсинге. Такие атаки возможны, когда злоумышленник может контролировать или изменять форму дерева разбора синтаксиса. Иногда бывает синергия, например, когда несоответствие парсеров позволяет обойти аудитор XSS или другие попытки смягчения последствий.

Вы также можете увидеть некоторые подклассы несоответствия парсеров с собственными названиями, так же как уязвимости инъекции делятся на XSS, SQL, разделение HTTP-заголовков и так далее. Единственный подкласс несоответствия парсеров, который мы видели до сих пор, это «путаница парсера URL» (URL parser confusion), но, вероятно, скоро появятся и другие. Возможно, рано или поздно появится название для плохой обработки многозначных HTTP-заголовков, но пока я такого не видел.

Теперь у вас есть четкое представление о том, что такое несоответствие парсера, и вы будете готовы распознать его при просмотре кода или попытке решить, как исправить уязвимость.

Не ждите, пока хакеры вас взломают - подпишитесь на наш канал и станьте неприступной крепостью!

Подписаться