简述
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。
这里的 params 是用户传入的参数,正常来说,params.token 是一串随机字符串,但在这里可以传一个包含正则表达式的查询语句 {'$regex':'^A'},例如下面这个例子意为查找一处 token 是以大写字母 A 为开头的数据。通过这个漏洞就可以逐字符的爆破修改密码所需的 token。
第二处漏洞在 app/api/server/v1/users.js,需要登陆后的用户才能访问,通过这处注入攻击者可以获得包括 admin 在内的所有用户的信息。注入点代码如下:
这处注入需要了解的知识点是,mongo 中的 $where 语句,根据文档,查询语句以这种形式展现 { $where:
攻击者可以传入这样的 query:{"$where":"this.username==='admin' && (()=>{ throw this.secret })()"},就会构成下面这样的查询语句,意为查询 username 为 admin 的用户并将他的信息通过报错输出。
通过这个漏洞,就可以获得 admin 的修改密码的 token 和 2FA 的密钥,即可修改 admin 的密码,达到了提权的目的。Rocket.Chat 还为管理员账户提供了创建 web hooks 的功能,这个功能用到了 Node.js 的 vm 模块,而 vm 模块可以通过简单的原型链操作被逃逸,达到任意命令执行的效果。至此,我们了解到了这一个命令执行漏洞的所有细节,接下来就通过分析漏洞发现者提供的 exp 来讲一下漏洞利用的过程。
漏洞利用
这部分内容基于漏洞发现者给出的 exp,并结合我在复现过程中遇到的问题提出改进意见。
首先通过 getPasswordPolicy() 处的 token 泄露漏洞,修改普通用户的密码。然而需要注意的是,修改密码的 token 长度为 43 个字符,这个爆破的工作量是很大的,且耗时非常长。因此在获取普通用户权限这一步,可以直接通过注册功能完成,而不需要爆破验证的 token。试想若是攻击目标关闭了注册功能,那意味着我们无法获取到已注册用户的信息,也就无计可施了。
第二步是获取管理员账号的 2FA 密钥,其中的 twofactor() 利用了第二处漏洞。
在这个函数中直接默认了管理员账号的 username 为 “admin”,但是经过测试,并不是所有可攻击的目标都以 “admin” 作为 username,那么就需要一种方法来获取管理员账号的 username。观察 mongodb 中存储的用户数据:
每一个用户字段中都有一条{"roles":[""]},通过{"$where":"this.roles.indexOf('admin')>=0"}来查询管理员账号的信息,随后便可获取管理员的 username。
第三步是修改管理员账号的密码,以获得 admin 的权限。
其中 forgotpassword() 这一步不可缺少,因为每次通过 reset token 来修改密码以后,后台会自动删除该 token。在本地测试的时候,因为没有 forgotpassword() 这一步,所以每次执行过 changingadminpassword() 以后,都会因为缺少 reset token 导致下一次 PoC 执行失败。通过断点调试找到了问题所在。
在.meteor/local/build/programs/server/packages/accounts-password.js line 1016
每一次执行 resetPassword() 以后,都会清空 token。同样在这个文件中,可以找到用于生成 reset.token 的函数 generateResetToken()。在此文件中共有三次出现,其中一次是函数定义,两次是调用,分别于第 898 行和第 938 行被 sendResetPasswordEmail() 和 sendEnrollmentEmail() 调用。
sendResetPasswordEmail() 在申请重置密码的时候被调用,sendEnrollmentEmail() 在用户刚注册的时候被调用。因此,想要获得 reset.token 的值,就要先发起一个重置密码的请求,让后台发送一封重置密码的邮件。
最后一步就是执行任意命令了。
由于命令执行没有回显,因此我的做法是在本地监听一个端口起一个 HTTP 服务器,然后执行 wget HTTP服务器地址/${random_str},如果 HTTP 服务器收到了路由为 /${random_str}的请求,则证明该服务存在漏洞。
后记
这次复现经过了挺长的时间,主要是由于这个漏洞利用的条件比较苛刻,需要满足各种限制条件,比如需要开放注册功能、管理员账号开启了 2FA、被攻击目标的版本满足要求。不过通过耐心的分析,把复现过程中遇到的问题一一解决,我还是很高兴的。
防护方案
1、更新 Rocket.Chat 至官方发布的最新版。
关注我,将持续更新!!!