NoSQL注入实战指南:从原理到防御的完整攻防手册

NoSQL注入实战指南:从原理到防御的完整攻防手册
1. 项目概述为什么NoSQL注入值得你投入精力如果你还在把数据库安全的目光仅仅锁定在传统的SQL注入上那可能已经落后了。随着MongoDB、Redis、Cassandra等NoSQL数据库在Web应用、移动后端和物联网平台中的大规模应用一种新的攻击面——NoSQL注入正悄然成为渗透测试人员和开发者必须正视的威胁。我最初接触这个概念时也以为它只是SQL注入的一个“变种”但深入实践后发现它的攻击手法、利用场景和防御策略都自成体系甚至在某些场景下比SQL注入更隐蔽、更危险。这个“终极指南”项目就是把我过去几年在渗透测试和代码审计中针对MongoDB、Redis等主流NoSQL数据库的注入实战经验进行一次系统性的梳理和输出。它不仅仅是一份漏洞列表更是一套从环境搭建、原理理解、手工探测到自动化利用的完整方法论。你会发现很多应用明明做了完善的SQL注入防护却因为一个$where操作符或一个JSON解析逻辑在NoSQL层面门户大开。通过这个教程无论是安全研究员想丰富自己的武器库还是后端开发者想加固自己的应用都能获得即学即用的干货。接下来我会从NoSQL注入的独特之处讲起带你一步步构建完整的知识体系和实操能力。2. NoSQL注入的核心原理与独特攻击面要理解NoSQL注入首先得跳出SQL注入的思维定式。SQL注入的本质是攻击者通过注入特殊字符改变了原SQL语句的结构和逻辑。而NoSQL注入尤其是针对像MongoDB这类基于JSON查询的数据库攻击者往往是在尝试改变查询的语义或操作符逻辑。2.1 与SQL注入的本质区别SQL数据库使用结构化查询语言语句有清晰的语法边界如引号、分号。注入的关键在于“逃逸”这些边界。而许多NoSQL查询使用API调用或类JSON对象如MongoDB的BSON来构建攻击者注入的是一个操作符或一个完整的查询片段。举个例子一个用户登录的SQL查询可能是SELECT * FROM users WHERE username ‘$username’ AND password ‘$password’注入admin’--可以注释掉密码验证。而在MongoDB中一个类似的查询可能由后端代码这样构建以Node.js为例db.users.findOne({username: req.body.username, password: req.body.password})看起来更安全如果后端没有对req.body进行类型强校验攻击者发送的POST body可以是{ “username”: {“$ne”: null}, “password”: {“$ne”: null} }这个JSON对象会被直接传入findOne方法。MongoDB会将其解释为查找username字段不等于null并且password字段不等于null的记录。这很可能返回数据库中的第一个用户比如管理员从而实现未授权登录。这里攻击者没有“逃逸”而是“覆盖”或“扩展”了查询条件。2.2 主要攻击向量分类根据注入点和利用方式NoSQL注入主要可以分为以下几类理解它们是后续实战的基础操作符注入这是最常见的一类。攻击者将数据输入篡改为包含查询操作符如MongoDB的$ne,$gt,$regex,$where的对象而非简单的字符串或数字。上面登录绕过的例子就是典型的操作符注入。JSON注入当应用层将用户输入字符串解析为JSON对象再传递给数据库查询时如果解析过程不安全就会产生此问题。例如用户输入“username”: “admin”, “$where”: “sleep(5000)”如果整个字符串被JSON.parse()后传入查询就可能触发基于时间的盲注。数组注入利用某些操作符如$in,$nin接受数组作为参数的特性。例如一个根据ID列表查询产品的API预期输入是{“id”: [“1”, “2”]}但攻击者可能注入{“id”: {“$in”: [“1”, “2”]}}来尝试探测逻辑差异或结合其他操作符进行布尔盲注。命令注入主要针对Redis这类兼具数据库和命令执行能力的服务。如果应用不当通过Redis协议执行的用户输入可能直接导致远程代码执行RCE。例如通过注入换行符\n来拼接多条Redis命令。ORM/ODM注入即使使用了像MongooseMongoDB的ODM如果开发者不安全地使用$where、mapReduce或直接将用户对象传入查询方法风险依然存在。例如Model.find(req.query)这种写法就是高危的。注意很多现代Web框架如Express.js的body-parser默认会解析JSON请求体这本身不是漏洞。漏洞的根源在于开发者盲目信任了客户端传来的、已解析好的对象并直接将其用于数据库查询而没有进行白名单过滤或类型转换。2.3 漏洞产生的根本原因归根结底NoSQL注入漏洞源于两个“信任”问题对客户端数据的过度信任认为前端传来的JSON结构或参数值一定是符合预期的标量值字符串、数字而忽略了客户端可以被完全操控的事实。对NoSQL“更安全”的误解认为不使用SQL拼接就万事大吉却忽略了NoSQL查询API同样接受复杂的查询对象这些对象如果来自不可信的源其危险性与SQL字符串拼接无异。3. 实战环境搭建与靶场配置光说不练假把式。要真正掌握NoSQL注入一个隔离的、可随意破坏的实战环境必不可少。我推荐使用Docker Compose来一键搭建一个包含漏洞应用和数据库的完整靶场这比在本地直接安装配置要干净和方便得多。3.1 基于Docker的靶场部署这里我以最经典的MongoDB和Node.js漏洞应用为例。首先确保你的系统已经安装了Docker和Docker Compose。创建一个项目目录比如nosql-injection-lab并在其中创建docker-compose.yml文件version: ‘3.8’ services: mongodb: image: mongo:latest container_name: nosql-mongodb restart: unless-stopped ports: - “27017:27017” volumes: - ./mongo-data:/data/db environment: - MONGO_INITDB_ROOT_USERNAMEadmin - MONGO_INITDB_ROOT_PASSWORDsecretpassword vulnerable-app: build: ./app container_name: nosql-vuln-app restart: unless-stopped ports: - “3000:3000” depends_on: - mongodb environment: - MONGO_URImongodb://admin:secretpasswordmongodb:27017/vulndb?authSourceadmin这个配置定义了两个服务一个MongoDB数据库实例和一个待会我们要构建的漏洞应用。数据库设置了初始根用户并将数据持久化到本地的mongo-data目录。接下来在app目录下创建漏洞应用。这里是一个极简的、存在NoSQL注入漏洞的Node.js Express应用。创建app/DockerfileFROM node:18-alpine WORKDIR /usr/src/app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD [“node”, “server.js”]创建app/package.json{ “name”: “vulnerable-nosql-app”, “version”: “1.0.0”, “description”: “A vulnerable app for NoSQL injection practice”, “main”: “server.js”, “scripts”: { “start”: “node server.js” }, “dependencies”: { “express”: “^4.18.2”, “mongoose”: “^7.5.0”, “body-parser”: “^1.20.2” } }最后创建存在漏洞的核心文件app/server.jsconst express require(‘express’); const mongoose require(‘mongoose’); const bodyParser require(‘body-parser’); const app express(); app.use(bodyParser.json()); // 关键这里会自动解析JSON请求体 // 连接MongoDB mongoose.connect(process.env.MONGO_URI); // 定义用户模型 const UserSchema new mongoose.Schema({ username: String, password: String, isAdmin: Boolean }); const User mongoose.model(‘User’, UserSchema); // 漏洞1登录接口 - 直接使用req.body进行查询 app.post(‘/login’, async (req, res) { try { // 高危直接将客户端传来的整个对象作为查询条件 const user await User.findOne(req.body); if (user) { res.json({ message: ‘Login successful’, user: { username: user.username, isAdmin: user.isAdmin } }); } else { res.status(401).json({ message: ‘Invalid credentials’ }); } } catch (err) { res.status(500).json({ error: err.message }); } }); // 漏洞2用户搜索接口 - 使用$where操作符且未过滤输入 app.get(‘/search’, async (req, res) { const username req.query.username || ‘’; try { // 高危将用户输入直接拼接进$where的JavaScript字符串中 const users await User.find({ $where: this.username ‘${username}’ }); res.json(users); } catch (err) { res.status(500).json({ error: err.message }); } }); // 初始化数据库插入一些测试数据 async function initDB() { await User.deleteMany({}); await User.create([ { username: ‘alice’, password: ‘alicepass123’, isAdmin: false }, { username: ‘bob’, password: ‘bobpass456’, isAdmin: false }, { username: ‘admin’, password: ‘StrongAdminPss!’, isAdmin: true } ]); console.log(‘Test data inserted.’); } app.listen(3000, async () { console.log(‘Vulnerable app listening on port 3000’); await mongoose.connection.asPromise(); await initDB(); });3.2 启动与初始化在项目根目录nosql-injection-lab下执行命令docker-compose up --build等待构建和启动完成。你应该能看到MongoDB和Node.js应用成功运行并且测试数据alice, bob, admin被插入数据库。现在访问http://localhost:3000你的漏洞靶场就已经准备就绪了。这个应用故意留下了两个典型的NoSQL注入漏洞一个是登录接口的直接对象注入另一个是搜索接口的$where子句注入。实操心得使用Docker搭建靶场的好处是环境完全隔离练习结束后一句docker-compose down -v就能清理所有痕迹包括数据库数据。--build参数确保每次启动都使用最新的代码。在实际学习或教学时我强烈建议采用这种方式避免污染本地环境。4. 手工探测与利用技巧详解有了靶场我们就可以开始“进攻”了。手工探测是理解漏洞本质的最佳方式它能让你直观地感受到攻击是如何生效的。我们将针对靶场中的两个漏洞点进行实战。4.1 漏洞点一登录接口的操作符注入这个接口的预期请求是POST /login Content-Type: application/json { “username”: “alice”, “password”: “alicepass123” }探测步骤基础绕过我们尝试不使用正确的密码而是注入一个查询操作符。发送以下请求{ “username”: {“$ne”: null}, “password”: {“$ne”: null} }这个查询条件的意思是“查找username字段不为null且password字段不为null的用户”。由于我们数据库中的所有用户都满足这个条件findOne方法会返回它找到的第一个文档可能是alice也可能是admin取决于存储顺序。这通常能绕过登录验证。精准定位管理员如果我们想直接登录管理员账户可以结合$regex操作符进行猜测。例如我们知道管理员用户名可能是“admin”。可以发送{ “username”: {“$regex”: “^admin$”}, “password”: {“$ne”: null} }或者如果不知道具体用户名但知道管理员账户的isAdmin字段为true可以尝试{ “username”: {“$ne”: null}, “password”: {“$ne”: null}, “isAdmin”: true }注意这能成功的前提是开发者错误地将整个req.body传入查询并且模型允许查询isAdmin字段。如果后端代码是User.findOne({username: req.body.username, password: req.body.password})那么注入isAdmin字段是无效的因为它不在查询条件范围内。这就是为什么手工测试时需要不断尝试和观察响应。利用布尔逻辑MongoDB的查询对象支持复杂的逻辑操作符如$or。可以尝试{ “$or”: [ {“username”: “admin”, “password”: {“$ne”: null}}, {“username”: “alice”, “password”: “alicepass123”} ] }这个查询会尝试匹配两个条件中的任意一个增加了绕过成功率。手工探测时我常用的工具是Burp Suite的Repeater模块或者命令行下的curl。使用curl的示例curl -X POST http://localhost:3000/login \ -H “Content-Type: application/json” \ -d ‘{“username”: {“$ne”: null}, “password”: {“$ne”: null}}’4.2 漏洞点二搜索接口的$where注入这个接口的预期请求是GET /search?usernamealice$where操作符允许执行JavaScript字符串这给了我们巨大的操作空间可以实现类似SQL注入中联合查询、条件判断等复杂攻击。探测与利用步骤基础确认首先测试注入点是否有效。尝试访问/search?usernamealice‘%2b’1或者更直接地利用JavaScript注释/search?usernamealice‘ // 注释掉后面如果应用报错显示JavaScript语法错误则基本确认存在注入。我们的靶场构造的查询是this.username ‘${username}’所以注入一个单引号会破坏字符串语法。布尔盲注$where可以执行任意JS代码我们可以构造条件语句来逐字符提取信息。例如判断管理员密码的第一个字符的ASCII码是否大于100/search?usernamealice‘ this.username ‘admin’ this.password.charCodeAt(0) 100 || ‘a’’b注入后的查询逻辑是this.username ‘alice’ this.username ‘admin’ this.password.charCodeAt(0) 100 || ‘a’’b。由于alice不是adminthis.username ‘admin’为false导致整个表达式短路为false。接着执行|| ‘a’’b’这也是false。最终整个$where条件为false查询不到任何用户返回空数组[]。 如果我们将条件改为this.password.charCodeAt(0) 100并且这个条件为真那么false true仍然是false结果不变。为了构造一个能根据条件返回不同结果的查询需要更巧妙的逻辑。一个更可靠的方法是让注入的代码直接影响返回结果本身但这在$where中较难实现。通常$where注入更适合时间盲注。时间盲注这是$where注入最强大的利用方式之一。我们可以使用sleep或循环函数来制造延迟通过响应时间判断条件真假。/search?usernamealice‘ (function(){ var d new Date(); while(new Date() - d 5000){;} return true; })() ‘1‘’1这个注入会执行一个空循环5秒钟。如果页面响应大约延迟了5秒说明注入的JS代码被执行了。我们可以利用这个特性进行盲注/search?usernamealice‘ (function(){ if(this.username ‘admin’ this.password.charCodeAt(0) 100){ var d new Date(); while(new Date() - d 3000){;} } return true; })() ‘1‘’1这个Payload的意思是如果当前文档的用户名是admin并且其密码的第一个字符ASCII码大于100则延迟3秒。通过测量响应时间我们就可以逐位推断出管理员密码。注意事项$where注入的利用非常灵活但也受到MongoDB服务器端JavaScript引擎的限制通常是V8。一些复杂的操作或过长的循环可能导致查询被终止。在实际测试中时间延迟要设置得合理如2-5秒并注意目标应用的超时设置。5. 自动化工具链与高级利用手工探测能加深理解但在真实的安全评估中效率至关重要。我们需要借助自动化工具来加速漏洞发现和利用过程。这里介绍几个我常用的工具和技巧。5.1 专用扫描工具NoSQLMap与NoSQLInjectionAttackNoSQLMap这是一个用Python编写的开源工具灵感来源于SQLMap但专门针对NoSQL数据库。它可以自动化进行注入检测、数据提取甚至接管数据库服务器。安装git clone https://github.com/codingo/NoSQLMap.git基本使用对于我们的登录接口可以这样使用python nosqlmap.py -u “http://localhost:3000/login” --data ‘{“username”: “test”, “password”: “test”}’ --method POST -H “Content-Type: application/json” --json工具会自动将test替换为各种NoSQL操作符Payload进行测试。优点Payload库丰富支持MongoDB、CouchDB等能自动识别数据库类型。缺点项目活跃度一般对复杂JSON结构的处理有时不够智能。NoSQLInjectionAttack这是一个较新的工具有时能检测出其他工具遗漏的漏洞点。它可以作为Burp Suite的插件使用集成到工作流中非常方便。我的经验是不要完全依赖自动化工具。它们对于快速筛选潜在注入点非常有用但误报和漏报时有发生。工具报告漏洞后一定要用手工复现一遍理解其触发原理并尝试挖掘工具未能发现的更深层次的利用方式。5.2 集成到Burp Suite工作流Burp Suite是Web安全测试的瑞士军刀。我们可以通过以下方式将NoSQL注入测试集成进去使用Intruder进行模糊测试对于JSON参数可以设置多个插入点。例如对{“username”: “§test§”, “password”: “§test§”}使用Intruder的“Cluster bomb”攻击类型加载两份Payload列表一份是普通字符串另一份是NoSQL操作符Payload如{“$ne”: null},{“$gt”: “”},{“$regex”: “^a”}等。通过观察响应长度、状态码和内容的变化快速识别出可能存在注入的参数。定制Scanner检查项Burp的主动扫描器主要针对SQL注入和XSS。对于NoSQL注入我们可以编写自定义的扫描检查需要Burp Extender API。思路是向JSON参数中插入特定的操作符Payload然后检查响应中是否出现了非预期的数据如其他用户信息、逻辑绕过如使用错误密码返回成功或错误信息泄露。利用Logger和Match/Replace这是一个强大的Burp插件。可以配置规则自动将所有出站的JSON请求中的特定参数值替换为测试Payload。例如将所有password字段的值自动替换为{“$ne”: null}这样在手动浏览测试网站时所有的登录尝试都会自动变成注入测试极大提高了效率。5.3 针对Redis的命令注入利用Redis的注入是另一大主题。假设一个应用将用户输入直接用于EVAL命令或通过redis.call()执行。一个经典的测试Payload是注入换行符来分隔命令。例如一个脆弱的代码可能这样写伪代码user_input get_param(‘key’) result redis_client.eval“return redis.call(‘GET’, ARGV[1])”, 0, user_input)攻击者可以提交key值为mykey\”)\nredis.call(‘SET’, ‘hacked’, ‘yes’)\nreturn(“这可能导致执行的Lua脚本变为return redis.call(‘GET’, ARGV[1]) redis.call(‘SET’, ‘hacked’, ‘yes’) return(从而执行了额外的SET命令。自动化测试Redis注入可以使用redis-cli手动连接或者编写简单的Python脚本使用redis库来发送包含恶意换行符或括号的Payload观察返回结果或检查Redis中是否写入了未经授权的键值。6. 防御策略与安全编码实践知道了如何攻击才能更好地防御。防御NoSQL注入的核心思想与SQL注入类似永远不要信任用户输入但具体做法因NoSQL的特性而有所不同。6.1 输入验证与类型强制转换这是第一道也是最重要的防线。白名单验证对于期望为简单类型的字段如用户名、ID在后端代码中强制将其转换为字符串或数字。// 错误示范 const user await User.findOne(req.body); // 正确示范 const username String(req.body.username); // 强制转为字符串 const password String(req.body.password); const user await User.findOne({ username, password });这样即使攻击者传入{“$ne”: null}经过String()转换后查询条件就变成了{ username: “[object Object]”, password: “[object Object]” }无法匹配任何记录。使用严格的Schema验证在Mongoose中充分利用Schema的验证功能。可以为字段指定类型、枚举值等。const UserSchema new mongoose.Schema({ username: { type: String, required: true, match: /^[a-zA-Z0-9_]$/ }, // 只允许字母数字下划线 password: { type: String, required: true }, age: { type: Number, min: 0 } });在查询前使用Mongoose的cast或验证功能可以过滤掉不符合Schema的数据结构。6.2 安全的查询构建方式避免直接传递用户对象这是最根本的原则。永远不要将req.body、req.query或req.params直接传递给查询方法。显式指定查询字段明确列出查询中要使用的字段和值。// 安全 const { id, category } req.query; const query {}; if (id) query._id mongoose.Types.ObjectId(id); // 注意ObjectId转换 if (category) query.category category; const products await Product.find(query);谨慎使用$where和mapReduce尽量避免使用$where操作符。如果非用不可绝对不要将用户输入拼接进JavaScript字符串。可以考虑使用Function构造函数在沙盒环境如果存在中执行但最佳实践是寻找等价的、不依赖JS的查询操作符如$expr来替代。// 高危 const users await User.find({ $where: this.username ‘${username}’ }); // 相对安全如果username是严格验证过的字符串 const users await User.find({ username: username }); // 如果需要复杂逻辑使用$expr const users await User.find({ $expr: { $eq: [“$username”, username] } });6.3 使用ORM/ODM的安全功能以Mongoose为例它提供了一些内置的保护机制但需要正确使用。sanitizeFilter选项Mongoose 6这是一个强大的安全特性。启用后Mongoose会默认过滤掉查询中所有非Schema定义的字段即以$开头或包含.的操作符除非显式允许。mongoose.set(‘sanitizeFilter’, true); // 全局启用 // 现在即使req.body包含{“username”: {“$ne”: null}}传入findOne后$ne操作符会被自动过滤掉查询变成{username: {}}是无效的。注意这不能完全防御所有情况例如用户输入一个合法的Schema字段名但值是一个操作符对象它可能不会被过滤。因此它应作为深度防御的一环而非唯一手段。使用Model的静态方法相比于直接使用db.collection通过Model进行查询能更好地利用Schema的约束和中间件。6.4 其他深度防御措施最小权限原则为应用程序连接数据库的账户分配最小必需的权限。例如一个只读的查询应用就不要给它写权限。在MongoDB中创建只能访问特定数据库、只有find权限的角色。网络层隔离将数据库部署在内网禁止公网直接访问。只允许应用服务器通过特定的端口访问。日志与监控启用数据库的审计日志记录所有查询操作。监控查询模式对短时间内出现大量异常操作符如大量$where、$regex的查询进行告警。定期安全评估将NoSQL注入测试纳入应用的常规安全测试SAST/DAST和渗透测试范围。使用前面提到的自动化工具进行扫描。7. 常见问题与排查技巧实录在实际测试和防御中你会遇到各种各样的问题。这里记录了一些我踩过的坑和总结的技巧。7.1 测试时遇到的典型问题及解决问题现象可能原因排查思路与解决方案发送操作符Payload后返回的是[object Object]或类似错误。后端对输入进行了JSON.parse()但后续又进行了字符串转换如String()或隐式转换或者框架层进行了处理。检查响应头确认后端是否真的接收到了JSON。尝试发送Content-Type: text/plain但Body是JSON看是否被解析。使用Burp的Match and Replace临时修改请求头进行测试。$where注入Payload没有导致延迟。1. 注入的JS语法错误被MongoDB静默忽略。2. 目标MongoDB实例禁用了服务器端JavaScript执行通过--noscripting参数启动。3. 应用有查询超时设置短于你的延迟时间。1. 先在MongoDB Shell中测试Payload语法是否正确。2. 尝试一个简单的$where: “11”看是否生效。如果无效可能$where被禁用。3. 尝试一个更短的延迟如1000毫秒或结合布尔盲注而非时间盲注。工具扫描报告漏洞但手工无法复现。1. 工具Payload触发了应用异常如500错误被误判为漏洞。2. 漏洞存在但依赖于特定条件如登录状态、特定参数组合。3. 应用有WAF或输入过滤工具Payload可能被变形绕过但手工Payload没有。1. 仔细查看工具发送的原始请求和应用的完整响应包括Headers。2. 在工具提示的漏洞点手动重放完全相同的请求。3. 尝试在已认证的会话中测试或组合其他参数。登录绕过成功但返回的不是目标用户。findOne()方法在没有排序的情况下返回的文档顺序是不确定的。尝试结合其他已知条件缩小范围如注入{“username”: “admin”, “password”: {“$ne”: null}}。或者尝试使用find()方法看返回的用户列表。7.2 防御实施中的陷阱过度依赖黑名单试图过滤掉所有$开头的键名是徒劳的。攻击者可能使用编码、嵌套对象或其他方式绕过。白名单是唯一可靠的方法。忽略了嵌套对象和数组验证和过滤不能只做一层。如果允许用户提交嵌套的JSON对象必须递归地对所有层级进行类型检查。// 用户输入: {“filter”: {“tags”: {“$in”: [“popular”, “vip”]}}} // 如果只检查了第一层filter.tags.$in 就会被放过。错误使用ObjectId转换试图用mongoose.Types.ObjectId(id)来防御注入是危险的。如果转换失败例如id不是合法的24位十六进制字符串它会抛出一个异常可能导致应用崩溃DoS。一定要先验证格式再尝试转换。const { id } req.params; if (!id || !/^[0-9a-fA-F]{24}$/.test(id)) { return res.status(400).json({ error: ‘Invalid ID format’ }); } const query { _id: new mongoose.Types.ObjectId(id) }; // 现在安全了7.3 高级技巧从注入到信息泄露与RCE在某些复杂场景下NoSQL注入可能只是跳板。结合SSRF如果注入点允许控制数据库连接字符串极少见但在某些配置工具或调试接口中可能存在可以尝试利用MongoDB连接字符串的某些特性进行服务器端请求伪造SSRF攻击内网其他服务。MongoDB的$function与$accumulator在聚合管道中这些操作符也可能执行JavaScript。它们的利用方式与$where类似但出现在不同的上下文中。从数据泄露到代码执行通过注入提取出应用源码如果错误地存储在数据库中、配置文件或密钥再结合其他漏洞如反序列化、模板注入实现RCE。例如窃取了Redis中存储的Session序列化数据或敏感配置。8. 实战案例深度剖析一个真实的逻辑漏洞组合拳最后我想分享一个在内部渗透测试中遇到的真实案例已脱敏它展示了NoSQL注入如何与其他逻辑缺陷结合造成严重破坏。目标一个使用MongoDB的Node.js用户管理后台提供了用户查询API。接口POST /api/users/query接受一个复杂的JSON查询对象。预期请求{ “filters”: { “status”: “active”, “department”: “engineering” }, “sort”: {“joinDate”: -1} }漏洞发现初步测试发现filters参数直接传入Model.find()。尝试注入{“status”: {“$ne”: “inactive”}}成功返回了所有非inactive状态的用户确认存在操作符注入。进一步测试发现sort参数也被直接传入Model.find().sort()。MongoDB的sort()方法同样接受对象。我尝试了{“$ne”: null}但无效。关键突破我注意到除了用户数据这个应用还有一个SystemConfig集合存储了邮件服务器SMTP密码、API密钥等敏感配置。默认情况下查询API只能查询User集合。利用聚合管道注入我研究了Mongoose的aggregate()方法发现如果应用使用了类似Model.aggregate(req.body.pipeline)的不安全代码攻击者可能控制整个聚合管道。但这里没有。最终利用我回到filters参数。我发现当查询条件中包含$where时在$where的JavaScript上下文中this指向当前文档。我能否通过this.constructor访问到其他模型经过测试在Mongoose的$where中this是普通的JS对象没有Mongoose文档的方法。但是我发现了另一个特性如果应用全局启用了lean()选项返回纯JS对象而非Mongoose文档并且在查询时没有正确处理可能会暴露更多信息。然而这并未直接通向目标。逻辑缺陷组合最终我通过另一个完全不同的路径——一个未授权访问的调试端点GET /api/debug/config直接获取到了SystemConfig的完整内容。这个端点本应只在开发环境开启但错误地部署到了生产环境。而发现这个端点正是因为我通过用户查询接口的注入提取到了一个内部错误信息其中包含了一个不常见的API路径片段/debug/给了我枚举的线索。教训NoSQL注入点本身可能无法直接“脱裤”但它往往是发现系统内部结构、错误信息和其他脆弱接口的绝佳入口。安全是一个整体。一个地方的不安全查询可能暴露其他更严重漏洞的蛛丝马迹。永远不要在生产环境开启调试接口或输出详细错误。