1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

alias, слеш и путь наружу

Коротко

Одна дыра, которую находят сканером за секунду и правят одним символом. location /trav без слеша вместе с alias открывает соседние каталоги.

  • Замер: /trav../secrets/db.env вернул 200 и содержимое файла из каталога, которого в конфиге нет вовсе.
  • Со слешем (location /trav/) тот же запрос даёт 404.
  • Нормализация URI тут не помогает: .. стоит внутри одного сегмента, схлопывать нечего.
  • Правило шире одной директивы: у alias слеши держат парой, как и у proxy_pass.

Если помнишь про alias без слеша и умеешь проверить его одной командой - листай до «Теперь сам».

Сначала ответь сам

/etc/nginx/conf.d/shop.local.confhttp server
location /trav {
    alias /var/www/uploads/;
}

Рядом с uploads лежит каталог secrets с файлом db.env. Что вернёт запрос /trav../secrets/db.env?

$  команда
curl http://shop.local/trav../secrets/db.env
↳  выводcurl http://shop.local/trav../secrets/db.env
пароли

Файл. Целиком - curl печатает его тело. Код при этом 200 (виден через -o /dev/null -w '%{http_code}'), а со слешем в location тот же запрос даёт 404 - разница в одном символе.

На что это похоже

Табличка на двери «склад» и правило: всё, что написано после слова «склад», дописывать к пути /дом/склад/. Посетитель пишет на бумажке «склад../сейф» - получается /дом/склад/../сейф, то есть /дом/сейф.

Ошибка не в посетителе, а в том, что слово «склад» разрешили продолжать. Слеш в конце - это и есть точка, после которой продолжать нельзя.

Механизм: где склеивается путь

location /trav (без слеша) /trav ../secrets/db.env склеиваем: /var/www/uploads/ + ../secrets/db.env = /var/www/secrets/db.env location /trav/ (со слешем) хвост начинается после слеша, и «..» уже не приклеивается к каталогу файла /var/www/uploads/../secrets/db.env по такому пути не находится - 404
Дыра живёт в склейке строк, а не в обработке пути.

Нормализация URI из четвёртого раздела тут не спасает: она схлопывает /a/../b в /b, а здесь точки стоят внутри одного сегмента - /trav.. - и схлопывать нечего. Именно поэтому атака и записывается без слеша перед точками.

Правило

/etc/nginx/conf.d/shop.local.confhttp server
location /uploads/ {          # слеш в location
    alias /var/www/uploads/;  # и слеш в alias
}

Слеши держат парой - то же правило, что у proxy_pass из седьмого раздела. Если слеш в location не нужен по смыслу маршрута, вместо alias берут root:

/etc/nginx/conf.d/shop.local.confhttp server
location /uploads {
    root /var/www;            # даст /var/www/uploads/...
}

root складывает путь целиком и такой дыры не имеет вовсе: он не вырезает совпавшую часть, а дописывает URI к корню.

Разбор: как проверить свой конфиг

$  команда
curl -s -o /dev/null -w '%{http_code}\n' https://shop.local/uploads../etc/passwd

404 или 403 - хорошо. 200 - у тебя дыра, и чинится она одним символом.

Найти кандидатов в чужом конфиге можно за секунду:

$  команда
nginx -T | grep -B2 alias | grep -E 'location [^/]*[^/]$'

Смотреть надо на все блоки с alias, у которых location не кончается слешем. Их обычно один-два, и оба - наследие копирования.

Разбор: что ещё вытекает через alias

Вторая беда той же директивы - разъехавшиеся слеши без всякой атаки:

/etc/nginx/conf.d/shop.local.confhttp server
location /static/ {
    alias /var/www/assets;     # слеша нет
}

Запрос /static/app.js превращается в /var/www/assetsapp.js. В логе будет честное open() "/var/www/assetsapp.js" failed (2: No such file or directory), и по нему причина видна сразу - но только если в лог заглянуть.

Третья - alias внутри регулярного location. Там подстановка идёт по группам, и без явного указания хвоста получается путь, который тебе не нужен:

/etc/nginx/conf.d/shop.local.confhttp server
location ~ ^/img/(.+)$ {   # (.+) захватывает хвост пути в $1
    alias /var/www/pictures/$1;   # хвост подставляем сами
}

Без $1 такой блок отдал бы один и тот же файл на любой запрос.

Что ломается без этого

Дыра не абстрактная: curl со строкой /trav../ есть в любом сканере, и проверяют её первым делом вместе с .git и .env из следующего урока. Достаётся оттуда обычно не /etc/passwd (nginx работает не от root и его чаще всего не прочитает), а гораздо более полезное для нападающего: конфиги приложения этажом выше, .env с паролем к базе, бэкапы, ключи API.

Поэтому правильная реакция на находку - не только починить слеш, но и считать утёкшим всё, что лежало в соседних каталогах: пароли меняют, ключи перевыпускают.

Зачем это в работе

Ревью конфига на эту тему занимает минуту и делается по трём вопросам:

  1. Есть ли alias там, где location без слеша?
  2. Есть ли alias, у которого слеш только с одной стороны?
  3. Можно ли вообще заменить alias на root?

Третий вопрос самый полезный. alias нужен, когда путь на диске не совпадает с адресом; если совпадает - root проще и безопаснее. В большинстве конфигов alias стоит просто по привычке.

Теперь сам

В конфиге location /files { alias /srv/files/; }. Коллега предлагает исправить на location /files/ { alias /srv/files/; }, но замечает, что тогда сломается адрес /files без слеша, по которому у них ходит старый клиент. Что делать?

Исправлять слеш обязательно, а старый адрес поддержать отдельной строкой: location = /files { return 301 /files/; }. Точное совпадение обрабатывается раньше префиксов, старый клиент получает редирект, дыра закрыта. Оставлять location /files с alias ради совместимости нельзя - это ровно та конфигурация, которую находят сканером.

Главное

location без слеша вместе с alias открывает соседние каталоги: замер даёт 200 и содержимое файла из /var/www/secrets/, хотя в конфиге этого каталога нет. Нормализация URI не помогает - точки стоят внутри одного сегмента. Слеши у alias держат парой, как у proxy_pass, а где можно - берут root, который такой дыры не имеет вовсе. Проверяется одной командой curl с .. после имени блока; найденную дыру чинят символом, но всё, что лежало рядом, считают утёкшим.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.