Год назад, 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 — работа над ошибками