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. 參考資料