Server-Side Request Forgery [Complete web app pentesting series #10]
Введение
Представьте уязвимость, которая позволяет злоумышленникам проникать в вашу внутреннюю сеть, получать доступ к конфиденциальным данным или даже удалять критически важные ресурсы — и всё это путём простой манипуляции с URL. Это не научная фантастика, а серверная подделка запросов (SSRF), одна из самых недооценённых, но опасных уязвимостей в современных веб-приложениях.
Атаки SSRF используют доверительные отношения между серверами, превращая безобидные функции, такие как проверка запасов или инструменты для создания скриншотов, в оружие для злоумышленников. В этой статье мы разберём SSRF на практических примерах и реальном задании, раскрыв такие техники, как DNS-ребиндинг, обход чёрных списков и захват открытых перенаправлений. Независимо от того, являетесь ли вы охотником за багами, разработчиком или энтузиастом безопасности, вы получите практические знания для эксплуатации и защиты от этих скрытых атак.
Тестирование черного ящика (Black Box)
1: Базовый SSRF против локального сервера
После нескольких кликов в лаборатории мы находим функцию «проверка запасов» (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 против другой бэкэнд-системы
Нам дан внутренний 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 с фильтром на основе чёрного списка
Как и в предыдущих лабораториях, мы видим 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 с обходом фильтра через уязвимость открытого перенаправления
В этой лаборатории на каждой странице есть опция «посмотреть следующий продукт». При клике мы видим следующий 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)
Baby Cached Web
В этом задании мы рассматриваем веб-приложение на Flask с двумя основными endpoint’ами:
/cache
: Принимает URL через JSON POST-запрос, загружает страницу с помощью headless-браузера (Selenium с Firefox), делает скриншот и кэширует изображение./flag
: Возвращает секретное изображение с флагом, доступное только с localhost.
На первый взгляд, приложение кажется безопасным благодаря следующим проверкам (см. CachedWeb/web_cached_web/challenge/application/util.py
):
- Проверка схемы URL
Код разрешает только URL с протоколамиhttp
илиhttps
: if scheme not in ['http', 'https']: return flash('Invalid scheme', 'danger') - Проверка внутреннего 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') - Защита 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), несмотря на защитные меры. Вот почему:
- Начальная проверка DNS
Функцияcache_web
используетsocket.gethostbyname(domain)
для разрешения домена, проверяя, что IP не внутренний. Эта проверка происходит один раз, до передачи URL в Selenium. - Отдельное разрешение DNS в Selenium
Браузер выполняет отдельные DNS-запросы при выполнении командыdriver.get(url)
в функцииserve_screenshot_from
. Злоумышленники, использующие DNS-ребиндинг, могут обойти начальную проверку IP, перенаправляя домен на127.0.0.1
или внутренние адреса позже. - Окно 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.
Аналогия: если вы проверяете, заперта ли дверь, перед выходом, но кто-то успевает её открыть между проверкой и вашим уходом, ваша уверенность в безопасности становится недействительной.
Шаги эксплуатации
- Создание вредоносного URL
Злоумышленник использует сервис DNS-ребиндинга, который сначала указывает на безопасный IP, а затем перенаправляет на127.0.0.1
перед выполнением URL в Selenium. - Обход проверки IP
Проверка DNS вcache_web
происходит в момент, когда IP-адрес ещё не изменён, поэтому она успешно проходит. - Запуск уязвимости 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
- Валидация и очистка ввода
Используйте библиотеку валидации, например Zod, чтобы проверять URL. Схема должна требовать валидные URL и ограничивать протоколы HTTPS: const urlSchema = z.string().url().startsWith("https://"); try { urlSchema.parse(userInput); } catch (e) { blockRequest(); } - Принудительное использование схем URL
ФункцияstartsWith("https://")
блокирует домены без HTTPS, предотвращая доступ к локальным файлам через протоколы вродеfile://
. Отклоняйте URL с закодированными символами, такими как%0D%0A
, для защиты от CRLF-инъекций. - Белый список доменов
Используйте фиксированный список доверенных доменов, напримерapi.unsplash.com
. ПроверяйтеURL.hostname
для строгого соответствия: const userHost = new URL(userInput).hostname; if (!trustedDomains.includes(userHost)) throw Error("Untrusted domain"); - Веб-брандмауэры приложений (WAF)
Используйте облачные WAF, такие как AWS WAF или Cloudflare, для блокировки запросов к внутренним IP (RFC 1918) и loopback-адресам (127.0.0.1, 169.254.169.254). - Управление зависимостями
Интегрируйте инструменты, такие как Snyk или Dependabot, для автоматического сканирования уязвимостей в библиотеках: # CI/CD шаг snyk test && snyk monitor - Смягчение TOCTOU
Кэшируйте разрешённые IP-адреса после проверки DNS и используйте их для последующих запросов: const resolvedIP = dns.resolve(userHost); if (isInternalIP(resolvedIP)) blockRequest(); fetch(`https://${resolvedIP}/path`); - Усиление сети
Ограничьте исходящий трафик от процесса Node.js только необходимыми доменами/IP через правила брандмауэра (например, iptables, группы безопасности в облаке).
Оригинальная статья | Перевод: THREAD