Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

NoSQL 注入

| , 3 minutes reading.

1. 定义

NoSQL 注入 发生在应用在未经校验的情况下,将用户输入直接传递给 NoSQL 数据库(如 MongoDB, CouchDB 或 Cassandra)查询时。

与使用字符串拼接的 SQLi 不同,NoSQLi 通常利用的是 对象注入 (Object Injection)。如果攻击者能注入一个 JSON 对象来代替预期的字符串,他们就可以利用数据库操作符(如 $ne, $gt, $where)来改变查询逻辑。

2. 技术原理

以一个检查登录的 Node.js/Express 应用为例:

// 易受攻击的代码
db.collection('users').findOne({
  username: req.body.username,
  password: req.body.password
}, callback);

开发者预期 req.body.password 是一个字符串。然而,像 body-parser 这样的中间件会处理 JSON。攻击者可以发送:

{
  "username": "admin",
  "password": { "$ne": null }
}

查询变成了:“查找用户名为 ‘admin’ 且密码 不为 NULL 的用户。” 由于管理员拥有密码,该条件评估为真,从而实现在不知道密码的情况下绕过认证。

3. 攻击流程 (绕过认证)

sequenceDiagram
    participant Attacker
    participant API as Node.js 应用
    participant DB as MongoDB

    Attacker->>API: 发送 POST /login (JSON Body)
    Note right of Attacker: { "username": "admin", "password": {"$ne": ""} }

    API->>DB: db.users.findOne({username: "admin", password: {$ne: ""}})
    
    Note over DB: 执行查询操作符 '$ne' (不等于)。
    Note over DB: 逻辑:管理员的密码是否 != ""? 是。
    
    DB-->>API: 返回管理员用户对象
    API-->>Attacker: 200 OK (返回认证 Token)

4. 真实案例:Rocket.Chat (CVE-2021-22911)

目标: Rocket.Chat (开源聊天平台)。 漏洞类别: 盲 NoSQL 注入。

漏洞描述: 一个密码重置功能在查询数据库前未对用户提供的令牌进行适当清洗。 Users.findOne({ "services.password.reset.token": token })

攻击技术:

  1. 攻击者可以利用 $regex 操作符对重置令牌进行逐字符爆破。
  2. 通过发送 { "$regex": "^A" },他们可以检查令牌是否以 “A” 开头。
  3. 如果服务器响应不同(例如,“令牌有效”与“令牌无效”),他们就能确认该字符。
  4. 这使得攻击者能提取有效的重置令牌并接管任何管理员账户。

影响: 导致关键的权限提升,允许完全控制服务器。

5. 深度防御策略

A. 严格的输入类型校验

在将输入传递给数据库之前,确保它是预期的原始类型(字符串、数字)。

  • 机制: 如果 typeof req.body.password !== 'string',则拒绝请求。
  • 库: 使用模式校验库,如 Joi, Zodclass-validator
// 防御示例
const { username, password } = req.body;
if (typeof password !== 'string') {
  return res.status(400).send('无效输入');
}

B. 使用具有严格模式验证的 ODM

使用像 Mongoose 这样的对象数据建模 (ODM) 库会有所帮助,但它不是“万灵药”。

  • 风险: 在某些情况下,Mongoose 查询过滤器仍可能接受 MongoDB 操作符,除非明确进行了清洗或指定了类型。
  • 防御: 使用显式指定类型的严格模式(例如 password: String)。此外,在将用户输入传递给查询之前,应始终将其显式转换为字符串,以确保不会处理嵌套的对象或操作符。
// 防御示例
const username = String(req.body.username);
const password = String(req.body.password);
User.findOne({ username, password });

C. 清洗输入键名

从用户输入中移除以 $ 开头的键名,以防止操作符注入。

  • 工具: mongo-sanitize npm 包。

    const sanitize = require('mongo-sanitize');
    const cleanUser = sanitize(req.body.username);

D. 最小权限原则

限制数据库用户的权限。除非绝对必要,否则不允许执行 Javascript(在 MongoDB 配置中禁用 $where, mapReducegroup 命令)。

6. 参考资料