August 29

Server-Side Request Forgery [Complete web app pentesting series #10]

Введение

Представьте уязвимость, которая позволяет злоумышленникам проникать в вашу внутреннюю сеть, получать доступ к конфиденциальным данным или даже удалять критически важные ресурсы — и всё это путём простой манипуляции с URL. Это не научная фантастика, а серверная подделка запросов (SSRF), одна из самых недооценённых, но опасных уязвимостей в современных веб-приложениях.

Атаки SSRF используют доверительные отношения между серверами, превращая безобидные функции, такие как проверка запасов или инструменты для создания скриншотов, в оружие для злоумышленников. В этой статье мы разберём SSRF на практических примерах и реальном задании, раскрыв такие техники, как DNS-ребиндинг, обход чёрных списков и захват открытых перенаправлений. Независимо от того, являетесь ли вы охотником за багами, разработчиком или энтузиастом безопасности, вы получите практические знания для эксплуатации и защиты от этих скрытых атак.

Тестирование черного ящика (Black Box)

1: Базовый SSRF против локального сервера

URL Лаборатории

После нескольких кликов в лаборатории мы находим функцию «проверка запасов» (check stocks). В отличие от других запросов на сайте, этот запрос интересен, так как это POST-запрос. В запросе есть параметр stockApi, который принимает URL. Что, если мы изменим URL, чтобы получить доступ к внутреннему ресурсу? В таком случае сайт уязвим к SSRF. Давайте попробуем.

При клике на любой URL, например:

https://0a2500c90385725e849b7c4a00ad000c.web-security-academy.net/product?productId=1

и выборе «check stock», мы видим POST-запрос, отправленный на сервер. Рассмотрим его в Burp Suite.

Если изменить параметр stockApi на следующие значения, мы получаем доступ к панели администратора:

http://localhost
http://127.0.0.1

Теперь перейдём к /admin. В ответе, при включении опции «pretty view», мы видим точный путь для удаления пользователя carlos:

http://localhost/admin
http://127.0.0.1/admin

Посетив следующий endpoint, мы удаляем пользователя:

http://localhost/admin/delete?username=carlos
http://127.0.0.1/admin/delete?username=carlos

После нажатия на follow redirection и отправки запроса данная задача будет успешно завершена.

2: Базовый SSRF против другой бэкэнд-системы

URL лаборатории

Нам дан внутренний IP-адрес 192.168.0.1 и порт 8080. Мы можем перебирать другие порты, но в описании лаборатории указано проверить последний октет. Возьмём список чисел от 1 до 255 и перебираем последний октет в Burp Intruder.

При переборе мы видим, что последний октет со значением 82 возвращает ответ 404, но, несмотря на ошибку, мы получаем доступ к панели администратора:

http://192.168.0.82:8080/admin

Используем Burp Repeater для доступа к этой панели. Повторяем шаги из предыдущей лаборатории: нажимаем «pretty», берём ссылку для удаления пользователя carlos:

http://192.168.0.82:8080/admin/delete?username=carlos

При доступе к этому endpoint мы решаем лабораторию. Как и в предыдущем случае, получаем перенаправление, и после следования ему лаборатория завершена.

3: SSRF с фильтром на основе чёрного списка

URL лаборатории

Как и в предыдущих лабораториях, мы видим POST-запрос с параметром stockApi. Однако localhost и 127.0.0.1 находятся в чёрном списке. Мы можем обойти эти фильтры, используя адреса вроде:

http://127.1/
http://2130706433/
        

Второй вариант — это десятичное представление IP-адреса IPv4. Можно использовать сайты, такие как https://www.ipaddressguide.com/ip, для конвертации IP в десятичный формат. В CTF второй вариант обычно работает, но в этой лаборатории он не срабатывает.

Используем http://127.1 и получаем доступ к панели администратора. При попытке доступа к http://127.1/admin мы видим опцию для удаления пользователя carlos. Однако /admin также в чёрном списке. Чтобы обойти это, используем двойное URL-кодирование символов в слове admin, получая %25%36%31. Итоговый payload для доступа к внутреннему ресурсу выглядит так:

http://127.1/%25%36%31dmin/delete?username=carlos

При изменении stockApi на этот URL мы получаем перенаправление и решаем лабораторию.

4: SSRF с обходом фильтра через уязвимость открытого перенаправления

URL лаборатории

В этой лаборатории на каждой странице есть опция «посмотреть следующий продукт». При клике мы видим следующий GET-запрос:

/product/nextProduct?currentProductId=2&path=/product?productId=3

Обратите внимание на параметр &path. Если заменить его на любой URL, endpoint становится уязвимым к открытому перенаправлению. Этот endpoint возвращает код 302 (перенаправление), и, следуя ему, мы попадаем на:

/product?productId=3

Используем этот параметр в stockApi, чтобы получить доступ к панели администратора. Комбинируем /product?productId из перенаправления и &path из запроса, создавая новый параметр:

stockApi=/product?productId=3&path=http://192.168.0.12:8080/admin/

В ответе, просматривая исходный код, мы видим ссылку для удаления пользователя carlos:

/http://192.168.0.12:8080/admin/delete?username=carlos

Итоговый payload для stockApi:

stockApi=/product?productId=3&path=http://192.168.0.12:8080/admin/delete?username=carlos

С этим мы решаем лабораторию. Это был нестандартный случай, который заставил понять, как можно манипулировать опцией «посмотреть следующую страницу», чтобы добиться желаемого результата.

Тестирование белого ящика (White Box)

URL лаборатории

Baby Cached Web

Обзор задания

В этом задании мы рассматриваем веб-приложение на Flask с двумя основными endpoint’ами:

  • /cache: Принимает URL через JSON POST-запрос, загружает страницу с помощью headless-браузера (Selenium с Firefox), делает скриншот и кэширует изображение.
  • /flag: Возвращает секретное изображение с флагом, доступное только с localhost.

На первый взгляд, приложение кажется безопасным благодаря следующим проверкам (см. CachedWeb/web_cached_web/challenge/application/util.py):

  1. Проверка схемы URL
    Код разрешает только URL с протоколами http или https: if scheme not in ['http', 'https']: return flash('Invalid scheme', 'danger')
  2. Проверка внутреннего IP
    При обработке запроса приложение использует socket.gethostbyname для разрешения домена и проверяет, что полученный IP не принадлежит внутренним диапазонам (127.0.0.0/8, 10.0.0.0/8 и т.д.): def ip2long(ip_addr): return struct.unpack('!L', socket.inet_aton(ip_addr))[0] def is_inner_ipaddress(ip): ip = ip2long(ip) return ip2long('127.0.0.0') >> 24 == ip >> 24 or \ ip2long('10.0.0.0') >> 24 == ip >> 24 or \ ip2long('172.16.0.0') >> 20 == ip >> 20 or \ ip2long('192.168.0.0') >> 16 == ip >> 16 or \ ip2long('0.0.0.0') >> 24 == ip >> 24 if is_inner_ipaddress(socket.gethostbyname(domain)): return flash('IP not allowed', 'danger')
  3. Защита localhost для /flag
    Декоратор в util.py гарантирует, что только запросы с 127.0.0.1 и без реферера могут получить доступ к endpoint’у /flag: def is_from_localhost(func): @functools.wraps(func) def check_ip(*args, **kwargs): if request.remote_addr != '127.0.0.1' or request.referrer: return abort(403) return func(*args, **kwargs) return check_ip Этот декоратор используется в маршруте (CachedWeb/web_cached_web/challenge/application/blueprints/routes.py): @web.route('/flag') @is_from_localhost def flag(): return send_file('flag.png')

Уязвимость: DNS-ребиндинг + TOCTOU

Приложение уязвимо к атаке DNS-ребиндинга и гонке условий TOCTOU (Time-of-Check to Time-of-Use), несмотря на защитные меры. Вот почему:

  1. Начальная проверка DNS
    Функция cache_web использует socket.gethostbyname(domain) для разрешения домена, проверяя, что IP не внутренний. Эта проверка происходит один раз, до передачи URL в Selenium.
  2. Отдельное разрешение DNS в Selenium
    Браузер выполняет отдельные DNS-запросы при выполнении команды driver.get(url) в функции serve_screenshot_from. Злоумышленники, использующие DNS-ребиндинг, могут обойти начальную проверку IP, перенаправляя домен на 127.0.0.1 или внутренние адреса позже.
  3. Окно TOCTOU
    Период между проверкой DNS (time-of-check) и фактическим запросом браузера (time-of-use) создаёт гонку условий. Это окно позволяет злоумышленникам изменять DNS-записи, перенаправляя трафик на 127.0.0.1 или внутренний endpoint /flag.

DNS-ребиндинг в простых словах

DNS-ребиндинг: Сайт обманывает систему, изменяя свой IP-адрес после первоначального принятия компьютером. Это техника обхода политики одного источника (same-origin policy) через неожиданное разрешение IP-адреса домена браузером.

Гонка условий TOCTOU в простых словах

TOCTOU (Time-of-Check to Time-of-Use): Эта уязвимость возникает, когда между проверкой состояния и использованием её результатов проходит заметное время. Система работает с устаревшими или манипулированными данными, если они изменяются в этот промежуток, например, в ответе DNS.

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

Шаги эксплуатации

  1. Создание вредоносного URL
    Злоумышленник использует сервис DNS-ребиндинга, который сначала указывает на безопасный IP, а затем перенаправляет на 127.0.0.1 перед выполнением URL в Selenium.
  2. Обход проверки IP
    Проверка DNS в cache_web происходит в момент, когда IP-адрес ещё не изменён, поэтому она успешно проходит.
  3. Запуск уязвимости TOCTOU
    После завершения DNS-проверки Selenium перенаправляет домен на внутренний адрес, позволяя злоумышленнику получить доступ к защищённому endpoint’у /flag.

Получение флага

1. Найдём IP-адрес Google для ребиндинга на localhost. Используем команду nslookup: ❯ nslookup google.com Server: 127.0.0.53 Address: 127.0.0.53#53 Non-authoritative answer: Name: google.com Address: 142.250.195.110 Name: google.com Address: 2404:6800:4007:81b::200e

2. Используем сайт, например, https://lock.cmpxchg8b.com/rebinder.html, для атаки DNS-ребиндинга.

3. Копируем адрес и вставляем его в поле ввода несколько раз, пока не сработает гонка условий и не будет получен флаг. Обычно достаточно трёх попыток: http://7f000001.8efac36e.rbndr.us/flag

Задание Baby Cached Web показывает, как даже надёжные на первый взгляд защиты от SSRF могут быть подорваны через DNS-ребиндинг и гонки условий TOCTOU. Используя разрыв между начальной проверкой DNS и последующим запросом браузера, злоумышленник может манипулировать разрешением DNS для доступа к защищённым внутренним endpoint’ам, таким как секретное изображение с флагом.

Заключение

SSRF — это не просто уязвимость, а шлюз в вашу внутреннюю инфраструктуру. От обхода чёрных списков с помощью 127.1 до использования DNS-ребиндинга — мы увидели, как злоумышленники переходят от простых параметров URL к полномасштабным взломам.

Ключевые выводы:

  • Всегда проверяйте и очищайте URL, предоставленные пользователем.
  • Предполагайте, что ограничения localhost могут быть обойдены; используйте строгие белые списки.
  • Отслеживайте разрывы в разрешении DNS в рабочих процессах, подверженных TOCTOU.

Защита от атак SSRF

  1. Валидация и очистка ввода
    Используйте библиотеку валидации, например Zod, чтобы проверять URL. Схема должна требовать валидные URL и ограничивать протоколы HTTPS: const urlSchema = z.string().url().startsWith("https://"); try { urlSchema.parse(userInput); } catch (e) { blockRequest(); }
  2. Принудительное использование схем URL
    Функция startsWith("https://") блокирует домены без HTTPS, предотвращая доступ к локальным файлам через протоколы вроде file://. Отклоняйте URL с закодированными символами, такими как %0D%0A, для защиты от CRLF-инъекций.
  3. Белый список доменов
    Используйте фиксированный список доверенных доменов, например api.unsplash.com. Проверяйте URL.hostname для строгого соответствия: const userHost = new URL(userInput).hostname; if (!trustedDomains.includes(userHost)) throw Error("Untrusted domain");
  4. Веб-брандмауэры приложений (WAF)
    Используйте облачные WAF, такие как AWS WAF или Cloudflare, для блокировки запросов к внутренним IP (RFC 1918) и loopback-адресам (127.0.0.1, 169.254.169.254).
  5. Управление зависимостями
    Интегрируйте инструменты, такие как Snyk или Dependabot, для автоматического сканирования уязвимостей в библиотеках: # CI/CD шаг snyk test && snyk monitor
  6. Смягчение TOCTOU
    Кэшируйте разрешённые IP-адреса после проверки DNS и используйте их для последующих запросов: const resolvedIP = dns.resolve(userHost); if (isInternalIP(resolvedIP)) blockRequest(); fetch(`https://${resolvedIP}/path`);
  7. Усиление сети
    Ограничьте исходящий трафик от процесса Node.js только необходимыми доменами/IP через правила брандмауэра (например, iptables, группы безопасности в облаке).

Оригинальная статья | Перевод: THREAD