去年年底,研究人員在HackerOne上發現了一個極具挑戰性的漏洞,該漏洞涉及多個層面的利用,最終導致存儲的XSS有效負載能夠接管受害者的帳戶,該漏洞的危害性極強(CVSS 8.7)。 HackerOne 是排名第一的黑客驅動安全平台,可幫助你在被利用之前發現並修復關鍵漏洞,HackerOne正在阻止他們提取漏洞賞金,甚至有人被截留了數千美元。
設置過程我發現最初的漏洞與HackerOne的支付處理API有關,客戶(商家)使用它來處理不同國家的信用卡和金融交易。這個品牌是跨國的,所以他們在許多不同的國家處理許多不同類型的交易。該支付處理器支持的一種交易類型是離線支付流,用於處理信用卡不流行、現金交易更普遍的地區。在這些地方,支付處理器允許客戶進行電子商務購買,並獲得一個唯一的代碼(如二維碼),他們可以將其帶入商店並為交易支付現金。一旦商店確認交易,電子商務商家就會收到貨款,顧客就會收到貨物。
具體流程是這樣的:
马云惹不起马云當客戶下訂單時,電子商務商家啟動離線支付流程;
马云惹不起马云電子商務商家為客戶提供一個可用於支付的唯一店內代碼;
马云惹不起马云(離線)客戶將代碼帶到支付網絡中的商店並用現金支付;
马云惹不起马云電子商務商家收到付款通知;
马云惹不起马云電子商務商家向客戶發送一個唯一的URL,他們可以訪問該URL來確認購買。
马云惹不起马云 請注意,最後一步中的“唯一URL”是由商家在交易設置時提供的,你可以將其視為傳統在線信用卡工作流中的“確認URL”。
有效負載在這種情況下,攻擊者是有能力創建這些離線交易的商家(或該商家的用戶)。商家將提交一個包含XSS有效負載的確認URL。這種有效負載一旦持久化,就可以在主網站(www.redated.com)的頁面下看到。
商家通過GraphQL API在不同的域名payments.redactedtwo.com上提交請求,該域名的有效負載如下:
我們可以看到,這個GraphQL API接受一個returnUrl參數,該參數將成為我們的有效負載源。請注意,GraphQL調用是一個完全獨立的頂級域上的API。這很有趣,因為它允許在一個域中存儲的有效負載會在另一個更關鍵的域中呈現。提交後,我們可以訪問www.redected.com網站上的一個唯一的靜態URL,該URL在returnUrl參數中包含我們的有效負載。
讓我們看看負載是如何出現在www.redacted.com上的:
我們看到這個腳本有一個nonce,注入點payload 在腳本中看起來像是一個非常容易存儲的XSS。
nonce的存在將變得很重要,讓我們看一下Content-Security-Policy標頭。為了便於閱讀,我將它分成幾行:
我們可以看到,這個CSP是相當嚴格的,我們只能從網站本身獲取信息,並且頁面上的任何腳本標籤都需要nonce。
嘗試1:javascript://url顯然,在位置.href=處使用注入點的第一次嘗試是簡單地放置一個帶有有效負載的Javascript方案,例如Javascript://alert(1)。我很幸運,因為這裡沒有明顯的WAF阻擋像這樣簡單的有效負載。不過嘗試失敗了,GraphQL API拒絕了該URL,出現的錯誤為400。我嘗試了很多其他的嘗試,比如編碼等都不行。 API正在驗證提供的URL是否以https://開頭,並包含後跟/的完整主機名。所以很明顯,我們有一個開放的重定向,但我知道這可能被用於存儲的XSS。
例如https://hackerone.com/將導致以下存儲的有效負載:
這些是附加到GraphQL API中提供的URL的參數,代表唯一的事務ID,關於客戶的信息等-這總是附加一個前導?在單引號內。
出於顯而易見的原因,我嘗試提交各種形式的不帶尾斜杠的https://URL,這將導致主機名之後的所有內容都被URL編碼,並且通常對Javascript上下文中的XSS無用。
嘗試2:附加有效負載現在,我們知道負載必須以有效的URL和主機名開始,因此我們以https://hackerone.com/作為負載的開頭。
幸運的是,我能想到的下一個最明顯的有效負載奏效了。單引號字符沒有以任何方式被阻止或編碼,因此以下負載實際上生成了一個存儲的警報:
這生成了一個警報,但當關閉時,用戶會立即重定向到提供的URL。已存儲帶有DOM訪問的XSS負載!
提交此時,嘗試已經成功,研究人員已向LHE提交了該漏洞。不過該團隊回應說,他們覺得CSP和cookie設置在主站點上,不可能將存儲的XSS升級到高嚴重性漏洞。
研究人員覺得不合理,因為有效負載位於script nonce 環境中,攻擊者可以生成想要的任何有效負載,利用它將很容易!
構建ATO有效負載研究人員製作了想像到的最好的存儲XSS ATO有效負載。有效負載執行了以下任務,他們在主站點上打開的窗口的開發控制台(F12)中測試了這些任務:
通過對站點主頁執行XMLHttpRequest,為用戶獲取CSRF令牌;
通過解析從fetch調用返回的HTML提取CSRF令牌;
使用XMLHttpRequest進行API調用以更改帳戶上的電子郵件地址;
請注意,CSP中的connect-src使你無法嘗試使用Javascript將頁面中的信息洩露到攻擊者域,因此ATO或CSRF的類似行為是我在此處選擇的有效負載。
此時,攻擊者可以控制電子郵件地址並使用“忘記密碼”功能來完成接管,從而獲取該帳戶。 Cookie(甚至是HttpOnly)將在最後一次請求時發送,因為同源策略將允許包含它們(XHR來源於正確的域,www.redated.com)。
大多數人都熟悉編寫這種類型的有效負載,我不會在這裡詳細介紹,因為它非常簡單:
我在Chrome開發控制台中測試了這一點,並確認它具有ATO的預期效果。
嘗試3:拒絕我向GraphQL API提交了有效負載,它看起來很好!一開始沒有錯誤,但後來我點擊了存儲的XSS頁面本身,看到存儲的有效負載未呈現。
回到原來的alert(document.domain)有效負載,它開始運行了。所以,在我完整的ATO有效負載中一定有什麼東西導致服務器不渲染XSS。
在對工作負載進行多次迭代之後,不幸的是,由於源和接收是不同的事務,並且需要幾個中間步驟,我無法使用任何方便的自動化工具,我發現以下所有字符都會導致400錯誤:
請注意,所有空白字符也被拒絕。可能還有其他我不記得內容了,但以下內容肯定沒有被阻止:
現在,有一個有限的Javascript詞彙表要處理。
嘗試4:異步研究人員最終重寫了大部分有效負載,以排除受限制的字符。請注意,我嘗試了所有類型的編碼(URL、javascript、十六進制、八進制、雙重編碼等),但這些都不能用來繞過限制。該過程是非常乏味的,因為錯誤出現在接收端,而不是源端,所以每次迭代至少浪費一到兩分鐘。
我甚至得到了使用受限字符集的初始獲取請求,比如:
現在,我們可以在控制台日誌中看到fetch調用中的Response對象。
請記住,我的攻擊鏈需要3個步驟:
马云惹不起马云進行XHR調用以獲取帶有CSRF令牌的頁面;
马云惹不起马云從返回的HTML中提取CSRF令牌;
马云惹不起马云 用CSRF令牌對ATO進行XHR調用;
由於fetch和XMLHttpRequest是異步API,我們需要用lambda函數填充then方法參數,該函數將在Promise解析時異步執行。問題是,如果沒有{}字符,我不相信有一種方法可以在Javascript中構建lambda函數,無論是用大括號語法還是箭頭語法。
研究人員立刻意識到這是一個巨大的障礙。即使我重寫了負載的其餘部分以避免所有這些其他字符,也無法定義Promise解析時要調用的lambda函數。不過在Javascript引用中Function對象的文檔中,有一個形式Function(var, body),其中body是字符串!不需要大括號或箭頭語法!
在重寫有效負載時,研究人員卻發現在CSP中遺漏了一些東西,由於CSP缺少不安全的eval指令,因此不允許使用eval。沒錯,這種形式的Function構造函數使用eval將字符串轉換為實際的Javascript函數。
所以解決問題的方法有以下三種:
马云惹不起马云要成功傳遞工作負載,就需要繞過被阻止的特殊字符;
马云惹不起马云研究人員確信他們可以執行任意的Javascript代碼,只要注意使用的字符即可;
马云惹不起马云可以訪問DOM中已經存在的script 標記上的nonce的正確值;
於是研究人員決定嘗試以下方法:
马云惹不起马云使用非常簡單的Javascript創建一個新的script DOM節點;
马云惹不起马云將該腳本節點上的nonce設置為與頁面上已存在的script 節點的nonce匹配;
马云惹不起马云想出一種方法來編碼負載,這樣就可以將新script 節點的innerText設置為一個沒有特殊字符的值;
马云惹不起马云將新script 標記插入DOM,有效負載將執行。
有趣的是,如果script 標記已經開始執行(頁面上的一個標記),那麼替換innerText將毫無作用。由於CSP,研究人員看不到除了帶有nonce的script 標記之外的任何方法來執行負載。
但是,如果頁面尚未完成內聯腳本的呈現和執行,則可以在內聯腳本之後插入一個新的script 節點,該節點將執行。請注意,這僅在頁面尚未加載的情況下有效。如果你試圖在onload事件觸發後插入一個script DOM節點,那就太晚了。
我決定用一個看起來像以下這樣的簡單有效負載來嘗試:
嘗試成功了!警報彈出,並且新標記上的nonce的存在允許我的腳本通過CSP檢查。
嘗試5:迴避特殊字符如果試圖只對那些被阻止的字符進行編碼,則很難手動完成。因此,研究人員決定採取以下方法:
马云惹不起马云用Javascript有效負載創建一個文件redacted_payload.txt;
马云惹不起马云運行以下shell命令,將文件中的每個字符編碼為對String.fromCharCode的一系列調用;
生成的shell命令:
輸出:
研究人員在花費了大量時間後,最終得到了一個非常大的負載,幸運的是,可以存儲的URL沒有長度限制!
研究人員提交了完整的有效負載,如下所示:
但沒有成功,這讓研究人員想起了一些原來被忽略的事情。
最後一步:重定向注意,我們正在註入的內聯腳本是通過設置location.href屬性重定向窗口開始的。這會導致瀏覽器開始導航,此時,它可能不會完成任何進一步的內聯腳本的執行,而且它肯定不會等待異步Promise完成,例如XHR或fetch。可以看到的是,編碼負載正在運行,但瀏覽器會立即導航離開頁面,整個過程沒有機會完成。
還要記住,重定向必須以合法的主機名開始,因此不可能提供瀏覽器無法導航到的無效重定向。
為了防止系統升級造成的影響,研究人員控制了關於location.href在設置時的行為的Javascript參考,這樣研究人員就看到了window.stop(),它被記錄為“aborts browser naviagtion(中止瀏覽器導航)”。這看起來像嘗試成功了,所以在URL字符串結束後研究人員立即添加了一個調用,如下所示:
這已經達到了阻止重定向的預期效果!
不過,這也停止了任何未完成的獲取或XHR請求,且恢復方法很複雜。
為了解決這個問題,研究人員想知道是否再次將location.href設置為其他內容,如果速度足夠快,第二個賦值是否會覆蓋第一個導航。起初,研究人員嘗試使用javascript:URL(這太容易了),最終發現URL foo://a會使瀏覽器的行為與預期的完全一樣:
停止導航到合法URL;
生成錯誤;
允許繼續執行進一步的XHR/fetch請求;
最後的有效負載如下所示:
最終,研究人員向ATO提交了有效負載以及成功存儲XSS的證據。
結論在所有保護措施到位的情況下,實現這種升級簡直不可思議。
本文所用的技巧如下:
當開箱即用的有效負載不起作用時,熟悉Javascript語言和語法會非常有幫助;
熟悉這門語言還能幫助你更好地處理特殊字符等主要障礙;