随着互联网技术的飞速发展,数据库作为信息存储与管理的核心,其安全性问题日益凸显。近年来,NoSQL数据库因其灵活性和高性能逐渐成为许多企业的首选,其中MongoDB以其文档存储和JSON-like查询语言在开发社区中广受欢迎。然而,与传统SQL数据库类似,NoSQL数据库同样面临注入攻击的风险。
一、NoSQL注入的基础与背景
在进入技术细节之前,我们先来回顾NoSQL注入的基本概念。与SQL注入通过操控SQL语句实现非法数据访问类似,NoSQL注入针对的是NoSQL数据库的查询逻辑。MongoDB作为典型的NoSQL数据库,其查询通常以JSON格式的过滤器(query filter)呈现。例如:
db.users.find({"username": "John"})
这条查询的作用是从users
集合中检索所有username
键值为John
的文档。查询过滤器{"username": "John"}
定义了匹配条件。如果需要更复杂的条件,可以添加多个键值对,例如:
db.users.find({"username": "John", "title": "Analyst"})
这条查询会返回同时满足username
为John
且title
为Analyst
的文档。此外,MongoDB支持查询操作符(query operators),如$ne
(不等于),以实现更灵活的条件匹配。例如:
db.users.find({"username": {"$ne": "John"}})
这条查询将返回所有username
不为John
的文档。这些特性为开发者提供了便利,但也为攻击者打开了注入攻击的大门。
NoSQL注入主要分为两种类型:操作符注入(Operator Injection)和语法注入(Syntax Injection)。本文将重点探讨这两种注入方式的特点,并分析如何通过语法注入消除查询中的前置与后置条件。
二、操作符注入:局限性与挑战
操作符注入是NoSQL注入中最常见的形式。攻击者通过将查询操作符(如$ne
)注入到查询过滤器的值中,改变查询的逻辑。例如,假设应用程序期望用户输入一个普通的字符串作为username
,但攻击者输入了{"$ne": ""}
,则查询变为:
db.users.find({"username": {"$ne": ""}})
这条查询会返回所有username
不为空的文档,相当于绕过了预期限制,类似于SQL注入中的经典语句or 1=1
。这种方法简单有效,但有一个显著的局限性:攻击者只能影响注入字段的逻辑,无法改变查询中其他字段的条件。
例如,在以下查询中:
db.users.find({"username": {"$ne": ""}, "job": "IT"})
即使攻击者通过操作符注入使username
条件始终为真,查询仍会受到job: "IT"
的后置条件的限制,仅返回job
为IT
的文档。无论攻击者如何调整username
字段的输入,都无法摆脱这一约束。这种局限性使得操作符注入在面对复杂查询时显得力不从心。
三、语法注入:更灵活的攻击手段
与操作符注入相比,语法注入提供了更大的灵活性。这种注入方式利用了应用程序在构造查询时对字符串拼接或插值的不当处理,使攻击者能够直接修改查询的语法结构。语法注入通常出现在以下两种场景中:$where子句注入和JSON查询过滤器注入。
1. $where子句注入
MongoDB支持$where
子句,允许开发者使用JavaScript表达式对文档进行条件评估。例如:
db.users.find({"$where": "this.username == 'John'"})
这条查询会遍历users
集合中的每个文档,检查username
是否为John
。this
对象代表当前处理的文档。然而,由于$where
子句使用JavaScript引擎执行,攻击者可以通过注入恶意代码改变其逻辑。
假设攻击者能够控制$where
子句中的字符串部分,例如输入'||1'
,查询将变为:
db.users.find({"$where": "this.username == '' || 1"})
在JavaScript中,||
是“或”运算符,而1
始终为真。因此,这条查询将对所有文档返回true
,从而检索整个集合。然而,注入后可能会留下未闭合的引号,导致语法错误。为解决这一问题,攻击者可以使用类似'||1||'x'
的载荷,使查询变为:
db.users.find({"$where": "this.username == '' || 1 || 'x'"})
这里,'x'
与末尾的引号合并为'x'
,一个非空字符串,在布尔条件中为真。由于JavaScript的短路求值特性,一旦1
使条件为真,后续部分不会影响结果。这种方法巧妙地消除了前置条件(如this.username == 'John'
)的限制。
更进一步,攻击者还可以使用空字节(%00
)截断后续内容。例如,在复杂查询中:
db.users.find({"$where": "this.username == 'John' && this.password == 'a3b3f8a2f213fbfe…'"})
注入载荷'||1%00'
将使查询变为:
db.users.find({"$where": "this.username == '' || 1"})
空字节会截断后续条件,使攻击者完全控制查询结果。这种技巧在处理复杂$where
子句时尤为有效。
2. JSON查询过滤器注入
JSON查询过滤器注入发生在应用程序通过字符串拼接构造查询过滤器时。例如,假设查询为:
db.users.find({"job": "IT"})
如果job
的值由用户输入控制,攻击者可以注入IT","status":"active
,使查询变为:
db.users.find({"job": "IT", "status": "active"})
这相当于在SQL注入中添加了额外的WHERE
子句,极大地扩展了攻击者的能力。然而,当注入点前后存在条件时,问题变得复杂。例如:
db.users.find({"username": "Bob", "password": "ae34fc6534f2c..."})
如果攻击者注入到username
字段,如何消除前置或后置条件?JSON格式不支持注释,无法直接截断后续内容。Reino Mostert在研究中发现了一种创新方法,利用JSON解析器的“最后值胜出”规则和$where
子句,解决了这一难题。
四、消除前置与后置条件的核心技术
1. 利用“最后值胜出”规则消除前置条件
在JSON中,键名应唯一。如果一个文档包含重复的键,不同的解析器可能采用不同的处理方式,而许多解析器遵循“最后值胜出”原则,即最后一个同名键的值生效。例如:
{"id": "10", "id": "100"}
在遵循此规则的解析器中,id
的值为100
。MongoDB也遵循这一行为。基于此特性,攻击者可以在注入时重定义字段,覆盖前置条件。
例如,在查询:
db.users.find({"username": "Bob"})
如果注入到username
字段,简单的操作符注入如{"$ne": ""}
会被视为字符串:
db.users.find({"username": "{\"$ne\": \"\"}"})
这种情况下注入无效。为摆脱字符串限制,攻击者可以注入","username":{"$ne":""}
,使查询变为:
db.users.find({"username": "", "username": {"$ne": ""}})
由于“最后值胜出”,第二个username
覆盖了第一个,使查询返回所有username
不为空的文档。然而,注入后仍可能留下末尾引号("
),导致语法错误。为解决此问题,可以引入额外的键值对,如:
db.users.find({"username": "", "username": {"$ne": ""}, "status": "active"})
通过注入","username":{"$ne":""},"status":"active
,末尾引号被吸收。然而,这种方法需要知道有效的字段名(如status
),在实际攻击中可能受限。
2. 使用$where子句消除后置条件
更优雅的解决方案是引入$where
子句。例如,注入载荷","username":{"$ne":""},"$where":"1
,使查询变为:
db.users.find({"username": "", "username": {"$ne": ""}, "$where": "1"})
这里的$where: "1"
始终为真,且吸收了末尾引号,无需额外字段名。这种方法不仅消除了前置条件,还能应对部分后置条件的干扰。然而,对于如下查询:
db.users.find({"username": "Bob", "password": "ae34fc6534f2c..."})
即使注入成功,后置条件password
仍会限制结果。Reino Mostert坦言,目前尚未找到完美方法消除所有后置条件,并呼吁读者提供建议。
五、实战意义与防御建议
这种技术在实际渗透测试和安全研究中具有重要意义。对于攻击者而言,消除前置与后置条件意味着更高的灵活性和更大的破坏力;对于防御者而言,理解这些攻击方式是构建安全系统的第一步。
防御建议:
- 参数化查询:避免字符串拼接,使用MongoDB的原生查询对象。
- 输入验证:严格检查用户输入,拒绝包含操作符或特殊字符的内容。
- 禁用$where:除非必要,避免使用
$where
子句,以减少攻击面。 - 权限控制:限制数据库用户的查询权限,降低注入成功的危害。