Год назад, 21 марта 2019, в баг баунти программу Mail.ru на HackerOne пришел очень хороший багрепорт от maxarr. При внедрении нулевого байта (ASCII 0) в POST-параметр одного из API-запросов веб-почты, который возвращал HTTP-редирект, в данных редиректа виднелись куски неинициализированной памяти, в которых чаще всего раскрывались фрагменты из GET-параметров и заголовков других запросов к тому же серверу.
Это критическая уязвимость, т.к. запросы содержат в том числе сессионные куки. Через несколько часов был сделан временный фикс, который фильтровал нулевой байт (как потом выяснилось, этого было недостаточно, т.к. оставалась возможность инъекции CRLF /ASCII 13, 10, что позволяет манипулировать заголовками и данными HTTP-ответа, это менее критично, но все равно неприятно). Одновременно с этим проблема была передана аналитикам безопасности и разработчикам для поиска и устранения причин возникновения бага.
Почта Mail.ru — это очень непростое приложение, в формировании ответа может участвовать большое количество различных фронтенд/бекенд-компонентов, как опенсорсных (большое спасибо всем разработчикам свободного ПО), так и собственной разработки. Удалось исключить все компоненты кроме nginx и openresty и локализовать проблему до вызова ngx.req.set_uri() в OpenResty-скрипте, который вел себя не так, как ожидалось (воткнуть нулевой байт или перевод строки через GET-параметры с rewrite в ngx_http_rewrite_module, который, согласно документации используется и, казалось бы, должен работать абсолютно аналогично не получится). Были устранены возможные последствия, добавлена максимально строгая фильтрация и было проверено, что фильтрация устраняет все возможные векторы. Но механизм, который приводил к утечке содержимого памяти так и остался загадкой. Через месяц багрепорт закрыли как разрешенный, а разбор причин возникновения бага отложили до лучших времен.
OpenResty — это весьма популярный плагин, позволяющий писать Lua-скрипты внутри nginx, и он используется в нескольких проектах Mail.ru, поэтому проблема не считалась решенной. И спустя некоторое время к ней все-таки вернулись, чтобы понимать истинные причины, возможные последствия и составить рекомендации для разработчиков. В раскопках исходного кода участвовали Денис Денисов и Николай Ермишкин. Выяснилось что:
- В nginx при использовании rewrite с пользовательскими данными есть возможность directory traversal (и вероятно SSRF) в некоторых конфигурациях, но это известный факт, и он должен обнаруживаться статическими анализаторами конфигураций в Nginx Amplify и Gixy от Яндекс (да, мы его тоже используем, спасибо). При использовании OpenResty такую возможность легко пропустить, но нашу конфигурацию это не затрагивало.
пример конфигурации:
location ~ /rewrite { rewrite ^.*$ $arg_x; } location / { root html; index index.html index.htm; }
результат
curl localhost:8337/rewrite?x=/../../../../../../../etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
...
- В nginx есть ошибка, приводящая к утечке содержимого памяти, если строка rewrite содержит нулевой байт. При отдаче редиректа nginx выделяет новый буфер памяти, соответствующий полной длине строке, но копирует туда строку через строчную функцию, в которой нулевой байт является терминатором строки, поэтому строка копируется только до нулевого байта, остаток буфера содержит неинициализированные данные. Подробный разбор можно найти здесь.
пример конфигурации (^@ нулевой байт)
location ~ /memleak { rewrite ^.*$ "^@asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdasdf"; } location / { root html; index index.html index.htm; }
результат
curl localhost:8337/secret -vv
...
curl localhost:8337/memleak -vv
...
Location: http://localhost:8337/secret
...
- Nginx защищает GET-параметры от инъекции служебных символов и дает возможность использовать в rewrite только GET-параметры. Поэтому эксплуатировать инъекцию через контролируемые пользователем параметры в nginx не получается. POST параметры при этом не защищены. OpenResty позволяет работать и с GET и с POST параметрами, поэтому при использовании POST параметров через OpenResty появляется возможность инъекции специальных символов.
пример конфигурации:
location ~ /memleak { rewrite_by_lua_block { ngx.req.read_body(); local args, err = ngx.req.get_post_args(); ngx.req.set_uri( args["url"], true ); } } location / { root html; index index.html index.htm; }
результат:
curl localhost:8337 -d "url=secret" -vv
...
curl localhost:8337 -d "url=%00asdfasdfasdfasdfasdfasdfasdfasdf" -vv
...
Location: http://localhost:8337/{...может содержать secret...}
...
Дальнейшая реакция
О проблеме сообщили разработчикам nginx и OpenResty, разработчики не рассматривают проблему как ошибку безопасности в nginx, т.к. в самом nginx нет возможности эксплуатации ошибки через инъекцию спецсимволов, фикс раскрытия содержимого памяти был опубликован 16 декабря. За 4 месяца с момента репорта в OpenResty так же не было сделано каких-либо изменений, хотя было понимание, что необходим безопасный вариант функции ngx.req.set_uri(). 18 марта 2020 мы опубликовали информацию, 21 марта OpenResty выпустила версию 1.15.8.3, которая добавляет проверку URI.
Portswigger написал хорошую статью и взял комментарии у OpenResty и Nginx (правда комментарий о том, что раскрывается только небольшой фрагмент памяти является неверной и вводит в заблуждение, это определяется длиной строки, следующей за нулевым байтом и, при отсутствии явных ограничений на длину может контролироваться атакующим).
Так в чем же была ошибка и что делать, чтобы ее предотвратить?
Была ли ошибка в nginx? Да, была, потому что утечка содержимого памяти это в любом случае ошибка.
Были ли ошибка в OpenResty? Да, как минимум не был исследован и документирован вопрос о безопасности предлагаемой OpenResty функциональности.
Была ли допущена ошибка конфигурации / использования OpenResty? Да, потому что в отсутствии явного указания, было сделано непроверенное предположение о безопасности используемой функциональности.
Какая из этих ошибок является уязвимостью безопасности с баунти на $10000? Для нас это в общем-то не важно. В любом ПО, особенно на стыке нескольких компонент, особенно предоставляемых разными проектами и разработчиками никто и никогда не может гарантировать, что все особенности их работы известны и документированы и отсутствуют ошибки. Поэтому любая уязвимость безопасности возникает именно там, где она влияет на безопасность.
В любом случае, хорошей практикой будет нормализовать или максимально ограничивать/фильтровать входные данные, которые уходят в любой внешний модуль/API, если нет явных указаний и однозначного понимания, что этого не требуется.
Errata
По опыту предыдущей статьи, ради сохранения чистоты языка:
баг баунти — конкурс охоты за ошибками
багрепорт — уведомление об ошибке
редирект — перенаправление
опенсорсный — с открытым кодом
errata — работа над ошибками
firk
Не уверен что это так. Хотя скорее всего ошибка есть, ведь существует например переменная $binary_remote_addr в которой нули могут быть штатно (хотя сувать её в rewrite вряд ли кому придёт в голову, да и длина её всего 4 байта). Что же касается нулей из запроса, то nginx (без стороннних модулей) проверяет входные данные для обеспечения равенства strlen() и сохранённой длины строк, так что именно там бага нет.
z3apa3a Автор
Вот такая конфигурация
location ~ /memleak {
rewrite ^.*$ "^@asdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdasdf";
}
location / {
root html;
index index.html index.htm;
}
без использования openresty приводит к раскрытию данных другого запроса:
curl localhost:8337/secret -vv
...
curl localhost:8337/memleak -vv
...
Location: http://localhost:8337/secret
...
Это весьма искусственная, но все-таки демонстрация наличия ошибки в самом nginx. Вопрос может быть является ли эта ошибка уязвимостью безопасности.
z3apa3a Автор
Добавил примеры в статью