По условию задачи у нас была страница с тестированием знаний в области парсинга YAML и условие получения флага — набрать 1337 баллов. Но тест состоит всего из нескольких вопросов, где, даже ответив на все вопросы верно, нельзя набрать такое количество баллов.
Кроме страницы с тестом, в задаче прикреплены исходники бекенда. Их и будем изучать, чтобы разобраться, как можно получить флаг. Ниже приведен самый «интересный» кусок кода.
Прочитав код, видим: для получения флага нужно добиться, чтобы итоговый score стал равен 1337. А также обращаем внимание еще на несколько важных моментов:
сервер дважды парсит одно и то же значение из req.body.result
первый раз используется YAML.parse(..., { version: '1.1' })
второй раз используется YAML.parse(..., { version: '1.2' })
если результаты отличаются, приложение доверяет именно результату парсинга YAML 1.1
То есть нам не нужно «честно» передавать на сервер число 1337. Для решения нужно отправить такой YAML, чтобы:
Все решение строится на различиях между YAML 1.1 и YAML 1.2. В YAML 1.1 есть старые неявные преобразования, которые в YAML 1.2 уже убрали. Из-за этого одно и то же значение в одной версии может стать числом, а в другой остаться строкой. Я нашел два решения, но думаю, что вариантов решения тут можно найти немало, если поэкспериментировать и покопаться в документации к YAML обеих версий:
После прочтения спецификации по YAML
Первый способ решения
YAML 1.1 поддерживает специальную запись вида a:b, которая интерпретируется как число в шестидесятеричной системе: a * 60 + b.
Если передать 22:17, то:
Так мы получаем разные значения после работы двух парсеров, и после сверки результатов итоговое значение берется от парсинга с помощью версии 1.1.
Второе способ решения
Другой вариант основан на старом поведении YAML 1.1 для чисел с ведущим нулем.
Если значение начинается с нуля (0), а остальные цифры не выходят за диапазон от 0 до 7, то YAML 1.1 может интерпретировать его как восьмеричное число.
Таким образом, значение 02471 будет воспринято парсером 1.1 как восьмеричное число и конвертируется в десятичное — 1337. А версия парсера YAML 1.2 посчитает значение строкой и сохранит его в виде "02471". В итоге мы получаем разные значения и в результате используем число 1337.
В чем уязвимость?
Проблема здесь не в какой-то одной «опасной» функции, а в несовместимости версий и в доверии к неоднозначному парсингу.
YAML вообще известен тем, что умеет довольно агрессивно и местами неочевидно преобразовывать значения:
строки могут внезапно стать булевыми значениями
числа могут интерпретироваться в другом формате
синтаксис, который в одной версии является числом, в другой уже остается строкой
Есть очень наглядный квиз, который показывает проблемы и странное поведение YAML. Часть вопросов для задачи были взяты с него, но в оригинальном тесте вопросов побольше — 22 шт: https://www.ohyaml.wtf
Этой задачкой хотелось показать, почему обновления библиотек, смена парсера или даже «безобидный» рефакторинг обработки данных иногда может привести к уязвимостям. Особенно, когда еще и формат может устроить козни с неявным преобразованием типов.
Полезные ссылки